forked from auracaster/bumble_mirror
Compare commits
70 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4bd8c24f54 | |||
| 8d09693654 | |||
| 7d7534928f | |||
| e9bf5757c4 | |||
| f9f694dfcf | |||
| 022c23500a | |||
| 5d4f811a65 | |||
| 3c81b248a3 | |||
| fdee5ecf70 | |||
| 29bd693bab | |||
| 30934969b8 | |||
| 4a333b6c0f | |||
| dad7957d92 | |||
| 4ffc14482f | |||
| 63794981b7 | |||
| 5f86cddc85 | |||
| b5cc167e31 | |||
| 51d3a869a4 | |||
| 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 |
@@ -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.
|
||||||
|
|||||||
+4
-2
@@ -558,11 +558,13 @@ class GattServer:
|
|||||||
# Setup the GATT service
|
# Setup the GATT service
|
||||||
self.speed_tx = Characteristic(
|
self.speed_tx = Characteristic(
|
||||||
SPEED_TX_UUID,
|
SPEED_TX_UUID,
|
||||||
Characteristic.WRITE,
|
Characteristic.Properties.WRITE,
|
||||||
Characteristic.WRITEABLE,
|
Characteristic.WRITEABLE,
|
||||||
CharacteristicValue(write=self.on_tx_write),
|
CharacteristicValue(write=self.on_tx_write),
|
||||||
)
|
)
|
||||||
self.speed_rx = Characteristic(SPEED_RX_UUID, Characteristic.NOTIFY, 0)
|
self.speed_rx = Characteristic(
|
||||||
|
SPEED_RX_UUID, Characteristic.Properties.NOTIFY, 0
|
||||||
|
)
|
||||||
|
|
||||||
speed_service = Service(
|
speed_service = Service(
|
||||||
SPEED_SERVICE_UUID,
|
SPEED_SERVICE_UUID,
|
||||||
|
|||||||
+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
|
||||||
|
|||||||
+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,
|
||||||
|
|||||||
+21
-11
@@ -28,8 +28,8 @@ 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, get_dict_key_by_value
|
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:
|
||||||
@@ -185,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}'
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -739,11 +744,16 @@ class Attribute(EventEmitter):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def string_to_permissions(permissions_str: str):
|
def string_to_permissions(permissions_str: str):
|
||||||
return functools.reduce(
|
try:
|
||||||
lambda x, y: x | get_dict_key_by_value(Attribute.PERMISSION_NAMES, y),
|
return functools.reduce(
|
||||||
permissions_str.split(","),
|
lambda x, y: x | get_dict_key_by_value(Attribute.PERMISSION_NAMES, y),
|
||||||
0,
|
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)
|
||||||
|
|||||||
+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
|
||||||
|
|||||||
+24
-35
@@ -152,7 +152,12 @@ class UUID:
|
|||||||
BASE_UUID = bytes.fromhex('00001000800000805F9B34FB')[::-1] # little-endian
|
BASE_UUID = bytes.fromhex('00001000800000805F9B34FB')[::-1] # little-endian
|
||||||
UUIDS: List[UUID] = [] # Registry of all instances created
|
UUIDS: List[UUID] = [] # Registry of all instances created
|
||||||
|
|
||||||
def __init__(self, uuid_str_or_int, name=None):
|
uuid_bytes: bytes
|
||||||
|
name: Optional[str]
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, uuid_str_or_int: Union[str, int], name: Optional[str] = None
|
||||||
|
) -> None:
|
||||||
if isinstance(uuid_str_or_int, int):
|
if isinstance(uuid_str_or_int, int):
|
||||||
self.uuid_bytes = struct.pack('<H', uuid_str_or_int)
|
self.uuid_bytes = struct.pack('<H', uuid_str_or_int)
|
||||||
else:
|
else:
|
||||||
@@ -172,7 +177,7 @@ class UUID:
|
|||||||
self.uuid_bytes = bytes(reversed(bytes.fromhex(uuid_str)))
|
self.uuid_bytes = bytes(reversed(bytes.fromhex(uuid_str)))
|
||||||
self.name = name
|
self.name = name
|
||||||
|
|
||||||
def register(self):
|
def register(self) -> UUID:
|
||||||
# Register this object in the class registry, and update the entry's name if
|
# Register this object in the class registry, and update the entry's name if
|
||||||
# it wasn't set already
|
# it wasn't set already
|
||||||
for uuid in self.UUIDS:
|
for uuid in self.UUIDS:
|
||||||
@@ -196,22 +201,22 @@ class UUID:
|
|||||||
raise ValueError('only 2, 4 and 16 bytes are allowed')
|
raise ValueError('only 2, 4 and 16 bytes are allowed')
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_16_bits(cls, uuid_16, name=None):
|
def from_16_bits(cls, uuid_16: int, name: Optional[str] = None) -> UUID:
|
||||||
return cls.from_bytes(struct.pack('<H', uuid_16), name)
|
return cls.from_bytes(struct.pack('<H', uuid_16), name)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_32_bits(cls, uuid_32, name=None):
|
def from_32_bits(cls, uuid_32: int, name: Optional[str] = None) -> UUID:
|
||||||
return cls.from_bytes(struct.pack('<I', uuid_32), name)
|
return cls.from_bytes(struct.pack('<I', uuid_32), name)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def parse_uuid(cls, uuid_as_bytes, offset):
|
def parse_uuid(cls, uuid_as_bytes: bytes, offset: int) -> Tuple[int, UUID]:
|
||||||
return len(uuid_as_bytes), cls.from_bytes(uuid_as_bytes[offset:])
|
return len(uuid_as_bytes), cls.from_bytes(uuid_as_bytes[offset:])
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def parse_uuid_2(cls, uuid_as_bytes, offset):
|
def parse_uuid_2(cls, uuid_as_bytes: bytes, offset: int) -> Tuple[int, UUID]:
|
||||||
return offset + 2, cls.from_bytes(uuid_as_bytes[offset : offset + 2])
|
return offset + 2, cls.from_bytes(uuid_as_bytes[offset : offset + 2])
|
||||||
|
|
||||||
def to_bytes(self, force_128=False):
|
def to_bytes(self, force_128: bool = False) -> bytes:
|
||||||
'''
|
'''
|
||||||
Serialize UUID in little-endian byte-order
|
Serialize UUID in little-endian byte-order
|
||||||
'''
|
'''
|
||||||
@@ -227,7 +232,7 @@ class UUID:
|
|||||||
else:
|
else:
|
||||||
assert False, "unreachable"
|
assert False, "unreachable"
|
||||||
|
|
||||||
def to_pdu_bytes(self):
|
def to_pdu_bytes(self) -> bytes:
|
||||||
'''
|
'''
|
||||||
Convert to bytes for use in an ATT PDU.
|
Convert to bytes for use in an ATT PDU.
|
||||||
According to Vol 3, Part F - 3.2.1 Attribute Type:
|
According to Vol 3, Part F - 3.2.1 Attribute Type:
|
||||||
@@ -236,11 +241,11 @@ class UUID:
|
|||||||
'''
|
'''
|
||||||
return self.to_bytes(force_128=(len(self.uuid_bytes) == 4))
|
return self.to_bytes(force_128=(len(self.uuid_bytes) == 4))
|
||||||
|
|
||||||
def to_hex_str(self) -> str:
|
def to_hex_str(self, separator: str = '') -> str:
|
||||||
if len(self.uuid_bytes) == 2 or len(self.uuid_bytes) == 4:
|
if len(self.uuid_bytes) == 2 or len(self.uuid_bytes) == 4:
|
||||||
return bytes(reversed(self.uuid_bytes)).hex().upper()
|
return bytes(reversed(self.uuid_bytes)).hex().upper()
|
||||||
|
|
||||||
return ''.join(
|
return separator.join(
|
||||||
[
|
[
|
||||||
bytes(reversed(self.uuid_bytes[12:16])).hex(),
|
bytes(reversed(self.uuid_bytes[12:16])).hex(),
|
||||||
bytes(reversed(self.uuid_bytes[10:12])).hex(),
|
bytes(reversed(self.uuid_bytes[10:12])).hex(),
|
||||||
@@ -250,10 +255,10 @@ class UUID:
|
|||||||
]
|
]
|
||||||
).upper()
|
).upper()
|
||||||
|
|
||||||
def __bytes__(self):
|
def __bytes__(self) -> bytes:
|
||||||
return self.to_bytes()
|
return self.to_bytes()
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other: object) -> bool:
|
||||||
if isinstance(other, UUID):
|
if isinstance(other, UUID):
|
||||||
return self.to_bytes(force_128=True) == other.to_bytes(force_128=True)
|
return self.to_bytes(force_128=True) == other.to_bytes(force_128=True)
|
||||||
|
|
||||||
@@ -262,35 +267,19 @@ class UUID:
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def __hash__(self):
|
def __hash__(self) -> int:
|
||||||
return hash(self.uuid_bytes)
|
return hash(self.uuid_bytes)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
|
result = self.to_hex_str(separator='-')
|
||||||
if len(self.uuid_bytes) == 2:
|
if len(self.uuid_bytes) == 2:
|
||||||
uuid = struct.unpack('<H', self.uuid_bytes)[0]
|
result = 'UUID-16:' + result
|
||||||
result = f'UUID-16:{uuid:04X}'
|
|
||||||
elif len(self.uuid_bytes) == 4:
|
elif len(self.uuid_bytes) == 4:
|
||||||
uuid = struct.unpack('<I', self.uuid_bytes)[0]
|
result = 'UUID-32:' + result
|
||||||
result = f'UUID-32:{uuid:08X}'
|
|
||||||
else:
|
|
||||||
result = '-'.join(
|
|
||||||
[
|
|
||||||
bytes(reversed(self.uuid_bytes[12:16])).hex(),
|
|
||||||
bytes(reversed(self.uuid_bytes[10:12])).hex(),
|
|
||||||
bytes(reversed(self.uuid_bytes[8:10])).hex(),
|
|
||||||
bytes(reversed(self.uuid_bytes[6:8])).hex(),
|
|
||||||
bytes(reversed(self.uuid_bytes[0:6])).hex(),
|
|
||||||
]
|
|
||||||
).upper()
|
|
||||||
|
|
||||||
if self.name is not None:
|
if self.name is not None:
|
||||||
return result + f' ({self.name})'
|
result += f' ({self.name})'
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return str(self)
|
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Common UUID constants
|
# Common UUID constants
|
||||||
@@ -773,7 +762,7 @@ class AdvertisingData:
|
|||||||
def uuid_list_to_objects(ad_data: bytes, uuid_size: int) -> List[UUID]:
|
def uuid_list_to_objects(ad_data: bytes, uuid_size: int) -> List[UUID]:
|
||||||
uuids = []
|
uuids = []
|
||||||
offset = 0
|
offset = 0
|
||||||
while (uuid_size * (offset + 1)) <= len(ad_data):
|
while (offset + uuid_size) <= len(ad_data):
|
||||||
uuids.append(UUID.from_bytes(ad_data[offset : offset + uuid_size]))
|
uuids.append(UUID.from_bytes(ad_data[offset : offset + uuid_size]))
|
||||||
offset += uuid_size
|
offset += uuid_size
|
||||||
return uuids
|
return uuids
|
||||||
|
|||||||
+266
-158
@@ -23,17 +23,19 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
from contextlib import asynccontextmanager, AsyncExitStack
|
from contextlib import asynccontextmanager, AsyncExitStack
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, ClassVar, Dict, List, Optional, Tuple, Union
|
from typing import Any, Callable, ClassVar, Dict, List, Optional, Tuple, Type, Union
|
||||||
|
|
||||||
from .colors import color
|
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,
|
||||||
@@ -101,6 +103,7 @@ from .hci import (
|
|||||||
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,
|
||||||
@@ -140,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
|
||||||
@@ -197,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
|
||||||
@@ -523,11 +528,15 @@ class Connection(CompositeEventEmitter):
|
|||||||
transport: int
|
transport: int
|
||||||
self_address: Address
|
self_address: Address
|
||||||
peer_address: Address
|
peer_address: Address
|
||||||
|
peer_resolvable_address: Optional[Address]
|
||||||
role: int
|
role: int
|
||||||
encryption: int
|
encryption: int
|
||||||
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:
|
||||||
@@ -591,10 +600,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).
|
||||||
@@ -607,26 +618,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):
|
||||||
@@ -634,7 +649,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)
|
||||||
@@ -669,6 +684,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"""
|
||||||
|
|
||||||
@@ -744,10 +762,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_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
|
||||||
@@ -782,6 +801,9 @@ class DeviceConfiguration:
|
|||||||
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
|
||||||
)
|
)
|
||||||
@@ -867,12 +889,12 @@ def host_event_handler(function):
|
|||||||
# List of host event handlers for the Device class.
|
# List of host event handlers for the Device class.
|
||||||
# (we define this list outside the class, because referencing a class in method
|
# (we define this list outside the class, because referencing a class in method
|
||||||
# decorators is not straightforward)
|
# decorators is not straightforward)
|
||||||
device_host_event_handlers: list[str] = []
|
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
|
||||||
@@ -887,6 +909,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:
|
||||||
@@ -974,9 +997,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
|
||||||
@@ -984,13 +1008,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.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
|
||||||
@@ -1000,14 +1025,21 @@ 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(
|
||||||
|
characteristic["properties"]
|
||||||
|
),
|
||||||
permissions=characteristic["permissions"],
|
permissions=characteristic["permissions"],
|
||||||
descriptors=descriptors,
|
descriptors=descriptors,
|
||||||
)
|
)
|
||||||
@@ -1026,12 +1058,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)
|
||||||
|
|
||||||
@@ -1154,6 +1186,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(
|
||||||
@@ -1161,6 +1194,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(
|
||||||
@@ -1208,7 +1252,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,
|
||||||
@@ -1589,7 +1633,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
|
||||||
|
|
||||||
@@ -1658,9 +1702,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
|
||||||
@@ -1782,7 +1824,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
|
||||||
@@ -1919,9 +1961,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:
|
||||||
@@ -2152,13 +2197,23 @@ class Device(CompositeEventEmitter):
|
|||||||
await self.stop_discovery()
|
await self.stop_discovery()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def pairing_config_factory(self):
|
def pairing_config_factory(self) -> Callable[[Connection], PairingConfig]:
|
||||||
return self.smp_manager.pairing_config_factory
|
return self.smp_manager.pairing_config_factory
|
||||||
|
|
||||||
@pairing_config_factory.setter
|
@pairing_config_factory.setter
|
||||||
def pairing_config_factory(self, pairing_config_factory):
|
def pairing_config_factory(
|
||||||
|
self, pairing_config_factory: Callable[[Connection], PairingConfig]
|
||||||
|
) -> None:
|
||||||
self.smp_manager.pairing_config_factory = pairing_config_factory
|
self.smp_manager.pairing_config_factory = pairing_config_factory
|
||||||
|
|
||||||
|
@property
|
||||||
|
def smp_session_proxy(self) -> Type[smp.Session]:
|
||||||
|
return self.smp_manager.session_proxy
|
||||||
|
|
||||||
|
@smp_session_proxy.setter
|
||||||
|
def smp_session_proxy(self, session_proxy: Type[smp.Session]) -> None:
|
||||||
|
self.smp_manager.session_proxy = session_proxy
|
||||||
|
|
||||||
async def pair(self, connection):
|
async def pair(self, connection):
|
||||||
return await self.smp_manager.pair(connection)
|
return await self.smp_manager.pair(connection)
|
||||||
|
|
||||||
@@ -2188,13 +2243,18 @@ class Device(CompositeEventEmitter):
|
|||||||
if connection.role == BT_PERIPHERAL_ROLE and keys.ltk_peripheral:
|
if connection.role == BT_PERIPHERAL_ROLE and keys.ltk_peripheral:
|
||||||
return keys.ltk_peripheral.value
|
return keys.ltk_peripheral.value
|
||||||
|
|
||||||
async def get_link_key(self, address):
|
async def get_link_key(self, address: Address) -> Optional[bytes]:
|
||||||
# Look for the key in the keystore
|
# Look for the key in the keystore
|
||||||
if self.keystore is not None:
|
if self.keystore is not None:
|
||||||
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
|
||||||
|
return None
|
||||||
|
|
||||||
# [Classic only]
|
# [Classic only]
|
||||||
async def authenticate(self, connection):
|
async def authenticate(self, connection):
|
||||||
@@ -2313,6 +2373,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
|
||||||
@@ -2370,8 +2458,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:
|
||||||
@@ -2415,25 +2509,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
|
||||||
@@ -2545,7 +2638,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(
|
||||||
@@ -2636,7 +2731,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}] '
|
||||||
@@ -2651,22 +2746,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
|
||||||
@@ -2685,7 +2764,7 @@ 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,
|
||||||
)
|
)
|
||||||
@@ -2694,129 +2773,145 @@ class Device(CompositeEventEmitter):
|
|||||||
# [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_io_capability_response(
|
||||||
# Ask what the pairing config should be for this connection
|
self, connection, io_capability, authentication_requirements
|
||||||
pairing_config = self.pairing_config_factory(connection)
|
):
|
||||||
|
connection.peer_pairing_io_capability = io_capability
|
||||||
can_compare = pairing_config.delegate.io_capability not in (
|
connection.peer_pairing_authentication_requirements = (
|
||||||
smp.SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
|
authentication_requirements
|
||||||
smp.SMP_DISPLAY_ONLY_IO_CAPABILITY,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Respond
|
|
||||||
if can_compare:
|
|
||||||
|
|
||||||
async def compare_numbers():
|
|
||||||
numbers_match = await connection.abort_on(
|
|
||||||
'disconnection',
|
|
||||||
pairing_config.delegate.compare_numbers(code, digits=6),
|
|
||||||
)
|
|
||||||
if numbers_match:
|
|
||||||
await self.host.send_command(
|
|
||||||
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:
|
|
||||||
|
|
||||||
async def confirm():
|
|
||||||
confirm = await connection.abort_on(
|
|
||||||
'disconnection', pairing_config.delegate.confirm()
|
|
||||||
)
|
|
||||||
if confirm:
|
|
||||||
await self.host.send_command(
|
|
||||||
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(confirm())
|
|
||||||
|
|
||||||
# [Classic only]
|
# [Classic only]
|
||||||
@host_event_handler
|
@host_event_handler
|
||||||
@with_connection_from_address
|
@with_connection_from_address
|
||||||
def on_authentication_user_passkey_request(self, connection):
|
def on_authentication_user_confirmation_request(self, connection, code) -> None:
|
||||||
|
# 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
|
||||||
|
peer_io_capability = connection.peer_pairing_io_capability
|
||||||
|
|
||||||
|
async def confirm() -> bool:
|
||||||
|
# Ask the user to confirm the pairing, without display
|
||||||
|
return await pairing_config.delegate.confirm()
|
||||||
|
|
||||||
|
async def auto_confirm() -> bool:
|
||||||
|
# Ask the user to auto-confirm the pairing, without display
|
||||||
|
return await pairing_config.delegate.confirm(auto=True)
|
||||||
|
|
||||||
|
async def display_confirm() -> bool:
|
||||||
|
# Display the code and ask the user to compare
|
||||||
|
return await pairing_config.delegate.compare_numbers(code, digits=6)
|
||||||
|
|
||||||
|
async def display_auto_confirm() -> bool:
|
||||||
|
# Display the code to the user and ask the delegate to auto-confirm
|
||||||
|
await pairing_config.delegate.display_number(code, digits=6)
|
||||||
|
return await pairing_config.delegate.confirm(auto=True)
|
||||||
|
|
||||||
|
async def na() -> bool:
|
||||||
|
assert False, "N/A: unreachable"
|
||||||
|
|
||||||
|
# See Bluetooth spec @ Vol 3, Part C 5.2.2.6
|
||||||
|
methods = {
|
||||||
|
HCI_DISPLAY_ONLY_IO_CAPABILITY: {
|
||||||
|
HCI_DISPLAY_ONLY_IO_CAPABILITY: display_auto_confirm,
|
||||||
|
HCI_DISPLAY_YES_NO_IO_CAPABILITY: display_confirm,
|
||||||
|
HCI_KEYBOARD_ONLY_IO_CAPABILITY: na,
|
||||||
|
HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: auto_confirm,
|
||||||
|
},
|
||||||
|
HCI_DISPLAY_YES_NO_IO_CAPABILITY: {
|
||||||
|
HCI_DISPLAY_ONLY_IO_CAPABILITY: display_auto_confirm,
|
||||||
|
HCI_DISPLAY_YES_NO_IO_CAPABILITY: display_confirm,
|
||||||
|
HCI_KEYBOARD_ONLY_IO_CAPABILITY: na,
|
||||||
|
HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: auto_confirm,
|
||||||
|
},
|
||||||
|
HCI_KEYBOARD_ONLY_IO_CAPABILITY: {
|
||||||
|
HCI_DISPLAY_ONLY_IO_CAPABILITY: na,
|
||||||
|
HCI_DISPLAY_YES_NO_IO_CAPABILITY: na,
|
||||||
|
HCI_KEYBOARD_ONLY_IO_CAPABILITY: na,
|
||||||
|
HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: auto_confirm,
|
||||||
|
},
|
||||||
|
HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: {
|
||||||
|
HCI_DISPLAY_ONLY_IO_CAPABILITY: confirm,
|
||||||
|
HCI_DISPLAY_YES_NO_IO_CAPABILITY: confirm,
|
||||||
|
HCI_KEYBOARD_ONLY_IO_CAPABILITY: auto_confirm,
|
||||||
|
HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: auto_confirm,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
method = methods[peer_io_capability][io_capability]
|
||||||
|
|
||||||
|
async def reply() -> None:
|
||||||
|
if await connection.abort_on('disconnection', method()):
|
||||||
|
await self.host.send_command(
|
||||||
|
HCI_User_Confirmation_Request_Reply_Command( # type: ignore[call-arg]
|
||||||
|
bd_addr=connection.peer_address
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await self.host.send_command(
|
||||||
|
HCI_User_Confirmation_Request_Negative_Reply_Command( # type: ignore[call-arg]
|
||||||
|
bd_addr=connection.peer_address
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
AsyncRunner.spawn(reply())
|
||||||
|
|
||||||
|
# [Classic only]
|
||||||
|
@host_event_handler
|
||||||
|
@with_connection_from_address
|
||||||
|
def on_authentication_user_passkey_request(self, connection) -> None:
|
||||||
# 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)
|
||||||
|
|
||||||
can_input = pairing_config.delegate.io_capability in (
|
async def reply() -> None:
|
||||||
smp.SMP_KEYBOARD_ONLY_IO_CAPABILITY,
|
number = await connection.abort_on(
|
||||||
smp.SMP_KEYBOARD_DISPLAY_IO_CAPABILITY,
|
'disconnection', pairing_config.delegate.get_number()
|
||||||
)
|
|
||||||
|
|
||||||
# Respond
|
|
||||||
if can_input:
|
|
||||||
|
|
||||||
async def get_number():
|
|
||||||
number = await connection.abort_on(
|
|
||||||
'disconnection', pairing_config.delegate.get_number()
|
|
||||||
)
|
|
||||||
if number is not None:
|
|
||||||
await self.host.send_command(
|
|
||||||
HCI_User_Passkey_Request_Reply_Command(
|
|
||||||
bd_addr=connection.peer_address, numeric_value=number
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
await self.host.send_command(
|
|
||||||
HCI_User_Passkey_Request_Negative_Reply_Command(
|
|
||||||
bd_addr=connection.peer_address
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
asyncio.create_task(get_number())
|
|
||||||
else:
|
|
||||||
self.host.send_command_sync(
|
|
||||||
HCI_User_Passkey_Request_Negative_Reply_Command(
|
|
||||||
bd_addr=connection.peer_address
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
if number is not None:
|
||||||
|
await self.host.send_command(
|
||||||
|
HCI_User_Passkey_Request_Reply_Command( # type: ignore[call-arg]
|
||||||
|
bd_addr=connection.peer_address, numeric_value=number
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await self.host.send_command(
|
||||||
|
HCI_User_Passkey_Request_Negative_Reply_Command( # type: ignore[call-arg]
|
||||||
|
bd_addr=connection.peer_address
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
AsyncRunner.spawn(reply())
|
||||||
|
|
||||||
# [Classic only]
|
# [Classic only]
|
||||||
@host_event_handler
|
@host_event_handler
|
||||||
@with_connection_from_address
|
@with_connection_from_address
|
||||||
def on_pin_code_request(self, connection):
|
def on_pin_code_request(self, connection):
|
||||||
# classic legacy pairing
|
# Classic legacy pairing
|
||||||
# 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 (
|
# Respond
|
||||||
smp.SMP_KEYBOARD_ONLY_IO_CAPABILITY,
|
if io_capability == HCI_KEYBOARD_ONLY_IO_CAPABILITY:
|
||||||
smp.SMP_KEYBOARD_DISPLAY_IO_CAPABILITY,
|
# Ask the user to enter a string
|
||||||
)
|
|
||||||
|
|
||||||
# respond the pin code
|
|
||||||
if can_input:
|
|
||||||
|
|
||||||
async def get_pin_code():
|
async def get_pin_code():
|
||||||
pin_code = await connection.abort_on(
|
pin_code = await connection.abort_on(
|
||||||
'disconnection', pairing_config.delegate.get_number()
|
'disconnection', pairing_config.delegate.get_string(16)
|
||||||
)
|
)
|
||||||
|
|
||||||
if pin_code is not None:
|
if pin_code is not None:
|
||||||
pin_code = bytes(str(pin_code).zfill(6))
|
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(
|
await self.host.send_command(
|
||||||
HCI_PIN_Code_Request_Reply_Command(
|
HCI_PIN_Code_Request_Reply_Command(
|
||||||
bd_addr=connection.peer_address,
|
bd_addr=connection.peer_address,
|
||||||
pin_code_length=len(pin_code),
|
pin_code_length=pin_code_len,
|
||||||
pin_code=pin_code,
|
pin_code=pin_code,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
logger.debug("delegate.get_string() returned None")
|
||||||
await self.host.send_command(
|
await self.host.send_command(
|
||||||
HCI_PIN_Code_Request_Negative_Reply_Command(
|
HCI_PIN_Code_Request_Negative_Reply_Command(
|
||||||
bd_addr=connection.peer_address
|
bd_addr=connection.peer_address
|
||||||
@@ -2838,6 +2933,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)
|
||||||
)
|
)
|
||||||
@@ -2974,18 +3070,30 @@ class Device(CompositeEventEmitter):
|
|||||||
)
|
)
|
||||||
connection.emit('connection_data_length_change')
|
connection.emit('connection_data_length_change')
|
||||||
|
|
||||||
@with_connection_from_handle
|
# [Classic only]
|
||||||
def on_pairing_start(self, connection):
|
@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)
|
||||||
|
|
||||||
|
def on_pairing_start(self, connection: Connection) -> None:
|
||||||
connection.emit('pairing_start')
|
connection.emit('pairing_start')
|
||||||
|
|
||||||
@with_connection_from_handle
|
def on_pairing(self, connection: Connection, keys: PairingKeys, sc: bool) -> None:
|
||||||
def on_pairing(self, connection, keys, sc):
|
|
||||||
connection.sc = sc
|
connection.sc = sc
|
||||||
connection.authenticated = True
|
connection.authenticated = True
|
||||||
connection.emit('pairing', keys)
|
connection.emit('pairing', keys)
|
||||||
|
|
||||||
@with_connection_from_handle
|
def on_pairing_failure(self, connection: Connection, reason: int) -> None:
|
||||||
def on_pairing_failure(self, connection, reason):
|
|
||||||
connection.emit('pairing_failure', reason)
|
connection.emit('pairing_failure', reason)
|
||||||
|
|
||||||
@with_connection_from_handle
|
@with_connection_from_handle
|
||||||
|
|||||||
+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]),
|
||||||
)
|
)
|
||||||
|
|||||||
+95
-50
@@ -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
|
||||||
@@ -205,8 +205,16 @@ class Service(Attribute):
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
uuid: UUID
|
uuid: UUID
|
||||||
|
characteristics: List[Characteristic]
|
||||||
|
included_services: List[Service]
|
||||||
|
|
||||||
def __init__(self, uuid, characteristics: list[Characteristic], primary=True):
|
def __init__(
|
||||||
|
self,
|
||||||
|
uuid,
|
||||||
|
characteristics: List[Characteristic],
|
||||||
|
included_services: List[Service] = [],
|
||||||
|
primary=True,
|
||||||
|
):
|
||||||
# Convert the uuid to a UUID object if it isn't already
|
# Convert the uuid to a UUID object if it isn't already
|
||||||
if isinstance(uuid, str):
|
if isinstance(uuid, str):
|
||||||
uuid = UUID(uuid)
|
uuid = UUID(uuid)
|
||||||
@@ -219,7 +227,7 @@ class Service(Attribute):
|
|||||||
uuid.to_pdu_bytes(),
|
uuid.to_pdu_bytes(),
|
||||||
)
|
)
|
||||||
self.uuid = uuid
|
self.uuid = uuid
|
||||||
# self.included_services = []
|
self.included_services = included_services[:]
|
||||||
self.characteristics = characteristics[:]
|
self.characteristics = characteristics[:]
|
||||||
self.primary = primary
|
self.primary = primary
|
||||||
|
|
||||||
@@ -247,75 +255,107 @@ class TemplateService(Service):
|
|||||||
to expose their UUID as a class property
|
to expose their UUID as a class property
|
||||||
'''
|
'''
|
||||||
|
|
||||||
UUID = None
|
UUID: Optional[UUID] = None
|
||||||
|
|
||||||
def __init__(self, characteristics, primary=True):
|
def __init__(self, characteristics, primary=True):
|
||||||
super().__init__(self.UUID, characteristics, primary)
|
super().__init__(self.UUID, characteristics, primary)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class IncludedServiceDeclaration(Attribute):
|
||||||
|
'''
|
||||||
|
See Vol 3, Part G - 3.2 INCLUDE DEFINITION
|
||||||
|
'''
|
||||||
|
|
||||||
|
service: Service
|
||||||
|
|
||||||
|
def __init__(self, service):
|
||||||
|
declaration_bytes = struct.pack(
|
||||||
|
'<HH2s', service.handle, service.end_group_handle, service.uuid.to_bytes()
|
||||||
|
)
|
||||||
|
super().__init__(
|
||||||
|
GATT_INCLUDE_ATTRIBUTE_TYPE, Attribute.READABLE, declaration_bytes
|
||||||
|
)
|
||||||
|
self.service = service
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return (
|
||||||
|
f'IncludedServiceDefinition(handle=0x{self.handle:04X}, '
|
||||||
|
f'group_starting_handle=0x{self.service.handle:04X}, '
|
||||||
|
f'group_ending_handle=0x{self.service.end_group_handle:04X}, '
|
||||||
|
f'uuid={self.service.uuid}, '
|
||||||
|
f'{self.service.properties!s})'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class Characteristic(Attribute):
|
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 +365,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 +383,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 +400,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})'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+157
-23
@@ -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
|
||||||
@@ -59,6 +63,7 @@ from .gatt import (
|
|||||||
GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
|
GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
|
||||||
GATT_REQUEST_TIMEOUT,
|
GATT_REQUEST_TIMEOUT,
|
||||||
GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE,
|
GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE,
|
||||||
|
GATT_INCLUDE_ATTRIBUTE_TYPE,
|
||||||
Characteristic,
|
Characteristic,
|
||||||
ClientCharacteristicConfigurationBits,
|
ClientCharacteristicConfigurationBits,
|
||||||
)
|
)
|
||||||
@@ -73,6 +78,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 +108,10 @@ class AttributeProxy(EventEmitter):
|
|||||||
|
|
||||||
|
|
||||||
class ServiceProxy(AttributeProxy):
|
class ServiceProxy(AttributeProxy):
|
||||||
|
uuid: UUID
|
||||||
|
characteristics: List[CharacteristicProxy]
|
||||||
|
included_services: List[ServiceProxy]
|
||||||
|
|
||||||
@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 +141,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 +170,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 +199,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 +225,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 +239,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 +324,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 +363,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 +389,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 +408,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(
|
||||||
@@ -445,14 +504,73 @@ class Client:
|
|||||||
|
|
||||||
return services
|
return services
|
||||||
|
|
||||||
async def discover_included_services(self, _service):
|
async def discover_included_services(
|
||||||
|
self, service: ServiceProxy
|
||||||
|
) -> List[ServiceProxy]:
|
||||||
'''
|
'''
|
||||||
See Vol 3, Part G - 4.5.1 Find Included Services
|
See Vol 3, Part G - 4.5.1 Find Included Services
|
||||||
'''
|
'''
|
||||||
# TODO
|
|
||||||
return []
|
|
||||||
|
|
||||||
async def discover_characteristics(self, uuids, service):
|
starting_handle = service.handle
|
||||||
|
ending_handle = service.end_group_handle
|
||||||
|
|
||||||
|
included_services: List[ServiceProxy] = []
|
||||||
|
while starting_handle <= ending_handle:
|
||||||
|
response = await self.send_request(
|
||||||
|
ATT_Read_By_Type_Request(
|
||||||
|
starting_handle=starting_handle,
|
||||||
|
ending_handle=ending_handle,
|
||||||
|
attribute_type=GATT_INCLUDE_ATTRIBUTE_TYPE,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if response is None:
|
||||||
|
# TODO raise appropriate exception
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Check if we reached the end of the iteration
|
||||||
|
if response.op_code == ATT_ERROR_RESPONSE:
|
||||||
|
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||||
|
# Unexpected end
|
||||||
|
logger.warning(
|
||||||
|
'!!! unexpected error while discovering included services: '
|
||||||
|
f'{HCI_Constant.error_name(response.error_code)}'
|
||||||
|
)
|
||||||
|
raise ATT_Error(
|
||||||
|
error_code=response.error_code,
|
||||||
|
message='Unexpected error while discovering included services',
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
|
# Stop if for some reason the list was empty
|
||||||
|
if not response.attributes:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Process all included services returned in this iteration
|
||||||
|
for attribute_handle, attribute_value in response.attributes:
|
||||||
|
if attribute_handle < starting_handle:
|
||||||
|
# Something's not right
|
||||||
|
logger.warning(f'bogus handle value: {attribute_handle}')
|
||||||
|
return []
|
||||||
|
|
||||||
|
group_starting_handle, group_ending_handle = struct.unpack_from(
|
||||||
|
'<HH', attribute_value
|
||||||
|
)
|
||||||
|
service_uuid = UUID.from_bytes(attribute_value[4:])
|
||||||
|
included_service = ServiceProxy(
|
||||||
|
self, group_starting_handle, group_ending_handle, service_uuid, True
|
||||||
|
)
|
||||||
|
|
||||||
|
included_services.append(included_service)
|
||||||
|
|
||||||
|
# Move on to the next included services
|
||||||
|
starting_handle = response.attributes[-1][0] + 1
|
||||||
|
|
||||||
|
service.included_services = included_services
|
||||||
|
return included_services
|
||||||
|
|
||||||
|
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 +583,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 +609,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 +655,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 +672,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 +779,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 +788,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 +901,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 +1036,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 +1049,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 +1059,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,
|
||||||
|
)
|
||||||
|
|||||||
+31
-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
|
||||||
@@ -68,6 +68,7 @@ from .gatt import (
|
|||||||
Characteristic,
|
Characteristic,
|
||||||
CharacteristicDeclaration,
|
CharacteristicDeclaration,
|
||||||
CharacteristicValue,
|
CharacteristicValue,
|
||||||
|
IncludedServiceDeclaration,
|
||||||
Descriptor,
|
Descriptor,
|
||||||
Service,
|
Service,
|
||||||
)
|
)
|
||||||
@@ -94,6 +95,7 @@ class Server(EventEmitter):
|
|||||||
def __init__(self, device):
|
def __init__(self, device):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.device = device
|
self.device = device
|
||||||
|
self.services = []
|
||||||
self.attributes = [] # Attributes, ordered by increasing handle values
|
self.attributes = [] # Attributes, ordered by increasing handle values
|
||||||
self.attributes_by_handle = {} # Map for fast attribute access by handle
|
self.attributes_by_handle = {} # Map for fast attribute access by handle
|
||||||
self.max_mtu = (
|
self.max_mtu = (
|
||||||
@@ -135,6 +137,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(
|
||||||
(
|
(
|
||||||
@@ -207,7 +224,14 @@ class Server(EventEmitter):
|
|||||||
# Add the service attribute to the DB
|
# Add the service attribute to the DB
|
||||||
self.add_attribute(service)
|
self.add_attribute(service)
|
||||||
|
|
||||||
# TODO: add included services
|
# Add all included service
|
||||||
|
for included_service in service.included_services:
|
||||||
|
# Not registered yet, register the included service first.
|
||||||
|
if included_service not in self.services:
|
||||||
|
self.add_service(included_service)
|
||||||
|
# TODO: Handle circular service reference
|
||||||
|
include_declaration = IncludedServiceDeclaration(included_service)
|
||||||
|
self.add_attribute(include_declaration)
|
||||||
|
|
||||||
# Add all characteristics
|
# Add all characteristics
|
||||||
for characteristic in service.characteristics:
|
for characteristic in service.characteristics:
|
||||||
@@ -228,7 +252,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
|
||||||
)
|
)
|
||||||
@@ -256,6 +283,7 @@ class Server(EventEmitter):
|
|||||||
|
|
||||||
# Update the service group end
|
# Update the service group end
|
||||||
service.end_group_handle = self.attributes[-1].handle
|
service.end_group_handle = self.attributes[-1].handle
|
||||||
|
self.services.append(service)
|
||||||
|
|
||||||
def add_services(self, services):
|
def add_services(self, services):
|
||||||
for service in services:
|
for service in services:
|
||||||
|
|||||||
+1
-1
@@ -2102,7 +2102,7 @@ class HCI_Link_Key_Request_Negative_Reply_Command(HCI_Command):
|
|||||||
fields=[
|
fields=[
|
||||||
('bd_addr', Address.parse_address),
|
('bd_addr', Address.parse_address),
|
||||||
('pin_code_length', 1),
|
('pin_code_length', 1),
|
||||||
('pin_code', '*'),
|
('pin_code', 16),
|
||||||
],
|
],
|
||||||
return_parameters_fields=[
|
return_parameters_fields=[
|
||||||
('status', STATUS_SPEC),
|
('status', STATUS_SPEC),
|
||||||
|
|||||||
+35
-14
@@ -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,
|
||||||
@@ -91,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
|
||||||
@@ -141,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()
|
||||||
@@ -375,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:
|
||||||
@@ -389,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:
|
||||||
@@ -513,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)}'
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -522,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,
|
||||||
)
|
)
|
||||||
@@ -539,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,
|
||||||
)
|
)
|
||||||
@@ -568,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,
|
||||||
)
|
)
|
||||||
@@ -581,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:
|
||||||
@@ -601,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]
|
||||||
@@ -718,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(
|
||||||
@@ -823,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(
|
||||||
|
|||||||
+56
-19
@@ -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, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
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
|
||||||
@@ -135,19 +139,19 @@ class PairingKeys:
|
|||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class KeyStore:
|
class KeyStore:
|
||||||
async def delete(self, name):
|
async def delete(self, name: str):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def update(self, name, keys):
|
async def update(self, name: str, keys: PairingKeys) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def get(self, _name):
|
async def get(self, _name: str) -> Optional[PairingKeys]:
|
||||||
return PairingKeys()
|
return None
|
||||||
|
|
||||||
async def get_all(self):
|
async def get_all(self) -> List[Tuple[str, PairingKeys]]:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
async def delete_all(self):
|
async def delete_all(self) -> None:
|
||||||
all_keys = await self.get_all()
|
all_keys = await self.get_all()
|
||||||
await asyncio.gather(*(self.delete(name) for (name, _) in all_keys))
|
await asyncio.gather(*(self.delete(name) for (name, _) in all_keys))
|
||||||
|
|
||||||
@@ -173,15 +177,15 @@ class KeyStore:
|
|||||||
separator = '\n'
|
separator = '\n'
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_for_device(device_config):
|
def create_for_device(device: Device) -> KeyStore:
|
||||||
if device_config.keystore is None:
|
if device.config.keystore is None:
|
||||||
return None
|
return MemoryKeyStore()
|
||||||
|
|
||||||
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 MemoryKeyStore()
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -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)
|
||||||
|
|
||||||
@@ -291,3 +307,24 @@ class JsonKeyStore(KeyStore):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
return PairingKeys.from_dict(keys)
|
return PairingKeys.from_dict(keys)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class MemoryKeyStore(KeyStore):
|
||||||
|
all_keys: Dict[str, PairingKeys]
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.all_keys = {}
|
||||||
|
|
||||||
|
async def delete(self, name: str) -> None:
|
||||||
|
if name in self.all_keys:
|
||||||
|
del self.all_keys[name]
|
||||||
|
|
||||||
|
async def update(self, name: str, keys: PairingKeys) -> None:
|
||||||
|
self.all_keys[name] = keys
|
||||||
|
|
||||||
|
async def get(self, name: str) -> Optional[PairingKeys]:
|
||||||
|
return self.all_keys.get(name)
|
||||||
|
|
||||||
|
async def get_all(self) -> List[Tuple[str, PairingKeys]]:
|
||||||
|
return list(self.all_keys.items())
|
||||||
|
|||||||
+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,188 @@
|
|||||||
|
# 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: KeyDistribution = (
|
||||||
|
KeyDistribution.DISTRIBUTE_ENCRYPTION_KEY
|
||||||
|
| KeyDistribution.DISTRIBUTE_IDENTITY_KEY
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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: IoCapability = NO_OUTPUT_NO_INPUT,
|
||||||
|
local_initiator_key_distribution: KeyDistribution = DEFAULT_KEY_DISTRIBUTION,
|
||||||
|
local_responder_key_distribution: KeyDistribution = 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, auto: bool = False) -> bool:
|
||||||
|
"""
|
||||||
|
Respond yes or no to a Pairing confirmation question.
|
||||||
|
The `auto` parameter stands for automatic confirmation.
|
||||||
|
"""
|
||||||
|
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: int) -> 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,7 +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
|
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,
|
||||||
@@ -60,12 +60,12 @@ class AshaService(TemplateService):
|
|||||||
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', 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:
|
||||||
@@ -79,6 +79,7 @@ class AshaService(TemplateService):
|
|||||||
)
|
)
|
||||||
self.emit(
|
self.emit(
|
||||||
'start',
|
'start',
|
||||||
|
connection,
|
||||||
{
|
{
|
||||||
'codec': value[1],
|
'codec': value[1],
|
||||||
'audiotype': value[2],
|
'audiotype': value[2],
|
||||||
@@ -88,7 +89,7 @@ class AshaService(TemplateService):
|
|||||||
)
|
)
|
||||||
elif opcode == AshaService.OPCODE_STOP:
|
elif opcode == AshaService.OPCODE_STOP:
|
||||||
logger.info('### STOP')
|
logger.info('### STOP')
|
||||||
self.emit('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]}')
|
||||||
|
|
||||||
@@ -102,7 +103,7 @@ class AshaService(TemplateService):
|
|||||||
|
|
||||||
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(
|
||||||
[
|
[
|
||||||
@@ -119,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),
|
||||||
)
|
)
|
||||||
@@ -141,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('data', 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
|
||||||
@@ -150,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),
|
||||||
),
|
),
|
||||||
|
|||||||
+1
-1
@@ -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:]
|
||||||
|
|
||||||
|
|||||||
+265
-194
File diff suppressed because it is too large
Load Diff
@@ -145,6 +145,11 @@ async def _open_transport(name: str) -> Transport:
|
|||||||
|
|
||||||
return await open_android_emulator_transport(spec[0] if spec else None)
|
return await open_android_emulator_transport(spec[0] if spec else None)
|
||||||
|
|
||||||
|
if scheme == 'android-netsim':
|
||||||
|
from .android_netsim import open_android_netsim_transport
|
||||||
|
|
||||||
|
return await open_android_netsim_transport(spec[0] if spec else None)
|
||||||
|
|
||||||
raise ValueError('unknown transport scheme')
|
raise ValueError('unknown transport scheme')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -16,14 +16,14 @@
|
|||||||
# Imports
|
# Imports
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
import logging
|
import logging
|
||||||
import grpc
|
import grpc.aio
|
||||||
|
|
||||||
from .common import PumpedTransport, PumpedPacketSource, PumpedPacketSink
|
from .common import PumpedTransport, PumpedPacketSource, PumpedPacketSink
|
||||||
from .emulated_bluetooth_pb2_grpc import EmulatedBluetoothServiceStub
|
|
||||||
from .emulated_bluetooth_vhci_pb2_grpc import VhciForwardingServiceStub
|
|
||||||
|
|
||||||
# pylint: disable-next=no-name-in-module
|
# pylint: disable=no-name-in-module
|
||||||
from .emulated_bluetooth_packets_pb2 import HCIPacket
|
from .grpc_protobuf.emulated_bluetooth_pb2_grpc import EmulatedBluetoothServiceStub
|
||||||
|
from .grpc_protobuf.emulated_bluetooth_packets_pb2 import HCIPacket
|
||||||
|
from .grpc_protobuf.emulated_bluetooth_vhci_pb2_grpc import VhciForwardingServiceStub
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -0,0 +1,410 @@
|
|||||||
|
# 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 asyncio
|
||||||
|
import atexit
|
||||||
|
import logging
|
||||||
|
import grpc.aio
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
import sys
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from .common import (
|
||||||
|
ParserSource,
|
||||||
|
PumpedTransport,
|
||||||
|
PumpedPacketSource,
|
||||||
|
PumpedPacketSink,
|
||||||
|
Transport,
|
||||||
|
)
|
||||||
|
|
||||||
|
# pylint: disable=no-name-in-module
|
||||||
|
from .grpc_protobuf.packet_streamer_pb2_grpc import PacketStreamerStub
|
||||||
|
from .grpc_protobuf.packet_streamer_pb2_grpc import (
|
||||||
|
PacketStreamerServicer,
|
||||||
|
add_PacketStreamerServicer_to_server,
|
||||||
|
)
|
||||||
|
from .grpc_protobuf.packet_streamer_pb2 import PacketRequest, PacketResponse
|
||||||
|
from .grpc_protobuf.hci_packet_pb2 import HCIPacket
|
||||||
|
from .grpc_protobuf.startup_pb2 import Chip, ChipInfo
|
||||||
|
from .grpc_protobuf.common_pb2 import ChipKind
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Logging
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Constants
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
DEFAULT_NAME = 'bumble0'
|
||||||
|
DEFAULT_MANUFACTURER = 'Bumble'
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def get_ini_dir() -> Optional[pathlib.Path]:
|
||||||
|
if sys.platform == 'darwin':
|
||||||
|
if tmpdir := os.getenv('TMPDIR', None):
|
||||||
|
return pathlib.Path(tmpdir)
|
||||||
|
if home := os.getenv('HOME', None):
|
||||||
|
return pathlib.Path(home) / 'Library/Caches/TemporaryItems'
|
||||||
|
elif sys.platform == 'linux':
|
||||||
|
if xdg_runtime_dir := os.environ.get('XDG_RUNTIME_DIR', None):
|
||||||
|
return pathlib.Path(xdg_runtime_dir)
|
||||||
|
elif sys.platform == 'win32':
|
||||||
|
if local_app_data_dir := os.environ.get('LOCALAPPDATA', None):
|
||||||
|
return pathlib.Path(local_app_data_dir) / 'Temp'
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def find_grpc_port() -> int:
|
||||||
|
if not (ini_dir := get_ini_dir()):
|
||||||
|
logger.debug('no known directory for .ini file')
|
||||||
|
return 0
|
||||||
|
|
||||||
|
ini_file = ini_dir / 'netsim.ini'
|
||||||
|
if ini_file.is_file():
|
||||||
|
logger.debug(f'Found .ini file at {ini_file}')
|
||||||
|
with open(ini_file, 'r') as ini_file_data:
|
||||||
|
for line in ini_file_data.readlines():
|
||||||
|
if '=' in line:
|
||||||
|
key, value = line.split('=')
|
||||||
|
if key == 'grpc.port':
|
||||||
|
logger.debug(f'gRPC port = {value}')
|
||||||
|
return int(value)
|
||||||
|
|
||||||
|
# Not found
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def publish_grpc_port(grpc_port) -> bool:
|
||||||
|
if not (ini_dir := get_ini_dir()):
|
||||||
|
logger.debug('no known directory for .ini file')
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not ini_dir.is_dir():
|
||||||
|
logger.debug('ini directory does not exist')
|
||||||
|
return False
|
||||||
|
|
||||||
|
ini_file = ini_dir / 'netsim.ini'
|
||||||
|
try:
|
||||||
|
ini_file.write_text(f'grpc.port={grpc_port}\n')
|
||||||
|
logger.debug(f"published gRPC port at {ini_file}")
|
||||||
|
|
||||||
|
def cleanup():
|
||||||
|
logger.debug("removing .ini file")
|
||||||
|
ini_file.unlink()
|
||||||
|
|
||||||
|
atexit.register(cleanup)
|
||||||
|
return True
|
||||||
|
except OSError:
|
||||||
|
logger.debug('failed to write to .ini file')
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
async def open_android_netsim_controller_transport(server_host, server_port):
|
||||||
|
if not server_port:
|
||||||
|
raise ValueError('invalid port')
|
||||||
|
if server_host == '_' or not server_host:
|
||||||
|
server_host = 'localhost'
|
||||||
|
|
||||||
|
if not publish_grpc_port(server_port):
|
||||||
|
logger.warning("unable to publish gRPC port")
|
||||||
|
|
||||||
|
class HciDevice:
|
||||||
|
def __init__(self, context, on_data_received):
|
||||||
|
self.context = context
|
||||||
|
self.on_data_received = on_data_received
|
||||||
|
self.name = None
|
||||||
|
self.loop = asyncio.get_running_loop()
|
||||||
|
self.done = self.loop.create_future()
|
||||||
|
self.task = self.loop.create_task(self.pump())
|
||||||
|
|
||||||
|
async def pump(self):
|
||||||
|
try:
|
||||||
|
await self.pump_loop()
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
logger.debug('Pump task canceled')
|
||||||
|
self.done.set_result(None)
|
||||||
|
|
||||||
|
async def pump_loop(self):
|
||||||
|
while True:
|
||||||
|
request = await self.context.read()
|
||||||
|
if request == grpc.aio.EOF:
|
||||||
|
logger.debug('End of request stream')
|
||||||
|
self.done.set_result(None)
|
||||||
|
return
|
||||||
|
|
||||||
|
# If we're not initialized yet, wait for a init packet.
|
||||||
|
if self.name is None:
|
||||||
|
if request.WhichOneof('request_type') == 'initial_info':
|
||||||
|
logger.debug(f'Received initial info: {request}')
|
||||||
|
|
||||||
|
# We only accept BLUETOOTH
|
||||||
|
if request.initial_info.chip.kind != ChipKind.BLUETOOTH:
|
||||||
|
logger.warning('Unsupported chip type')
|
||||||
|
error = PacketResponse(error='Unsupported chip type')
|
||||||
|
await self.context.write(error)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.name = request.initial_info.name
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Expect a data packet
|
||||||
|
request_type = request.WhichOneof('request_type')
|
||||||
|
if request_type != 'hci_packet':
|
||||||
|
logger.warning(f'Unexpected request type: {request_type}')
|
||||||
|
error = PacketResponse(error='Unexpected request type')
|
||||||
|
await self.context.write(error)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Process the packet
|
||||||
|
data = (
|
||||||
|
bytes([request.hci_packet.packet_type]) + request.hci_packet.packet
|
||||||
|
)
|
||||||
|
logger.debug(f'<<< PACKET: {data.hex()}')
|
||||||
|
self.on_data_received(data)
|
||||||
|
|
||||||
|
def send_packet(self, data):
|
||||||
|
async def send():
|
||||||
|
await self.context.write(
|
||||||
|
PacketResponse(
|
||||||
|
hci_packet=HCIPacket(packet_type=data[0], packet=data[1:])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.loop.create_task(send())
|
||||||
|
|
||||||
|
def terminate(self):
|
||||||
|
self.task.cancel()
|
||||||
|
|
||||||
|
async def wait_for_termination(self):
|
||||||
|
await self.done
|
||||||
|
|
||||||
|
class Server(PacketStreamerServicer, ParserSource):
|
||||||
|
def __init__(self):
|
||||||
|
PacketStreamerServicer.__init__(self)
|
||||||
|
ParserSource.__init__(self)
|
||||||
|
self.device = None
|
||||||
|
|
||||||
|
# Create a gRPC server with `so_reuseport=0` so that if there's already
|
||||||
|
# a server listening on that port, we get an exception.
|
||||||
|
self.grpc_server = grpc.aio.server(options=(('grpc.so_reuseport', 0),))
|
||||||
|
add_PacketStreamerServicer_to_server(self, self.grpc_server)
|
||||||
|
self.grpc_server.add_insecure_port(f'{server_host}:{server_port}')
|
||||||
|
logger.debug(f'gRPC server listening on {server_host}:{server_port}')
|
||||||
|
|
||||||
|
async def start(self):
|
||||||
|
logger.debug('Starting gRPC server')
|
||||||
|
await self.grpc_server.start()
|
||||||
|
|
||||||
|
async def serve(self):
|
||||||
|
# Keep serving until terminated.
|
||||||
|
try:
|
||||||
|
await self.grpc_server.wait_for_termination()
|
||||||
|
logger.debug('gRPC server terminated')
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
logger.debug('gRPC server cancelled')
|
||||||
|
await self.grpc_server.stop(None)
|
||||||
|
|
||||||
|
def on_packet(self, packet):
|
||||||
|
if not self.device:
|
||||||
|
logger.debug('no device, dropping packet')
|
||||||
|
return
|
||||||
|
|
||||||
|
self.device.send_packet(packet)
|
||||||
|
|
||||||
|
async def StreamPackets(self, _request_iterator, context):
|
||||||
|
logger.debug('StreamPackets request')
|
||||||
|
|
||||||
|
# Check that we won't already have a device
|
||||||
|
if self.device:
|
||||||
|
logger.debug('busy, already serving a device')
|
||||||
|
return PacketResponse(error='Busy')
|
||||||
|
|
||||||
|
# Instantiate a new device
|
||||||
|
self.device = HciDevice(context, self.parser.feed_data)
|
||||||
|
|
||||||
|
# Wait for the device to terminate
|
||||||
|
logger.debug('Waiting for device to terminate')
|
||||||
|
try:
|
||||||
|
await self.device.wait_for_termination()
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
logger.debug('Request canceled')
|
||||||
|
self.device.terminate()
|
||||||
|
|
||||||
|
logger.debug('Device terminated')
|
||||||
|
self.device = None
|
||||||
|
|
||||||
|
server = Server()
|
||||||
|
await server.start()
|
||||||
|
asyncio.get_running_loop().create_task(server.serve())
|
||||||
|
|
||||||
|
class GrpcServerTransport(Transport):
|
||||||
|
async def close(self):
|
||||||
|
await super().close()
|
||||||
|
|
||||||
|
return GrpcServerTransport(server, server)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
async def open_android_netsim_host_transport(server_host, server_port, options):
|
||||||
|
# Wrapper for I/O operations
|
||||||
|
class HciDevice:
|
||||||
|
def __init__(self, name, manufacturer, hci_device):
|
||||||
|
self.name = name
|
||||||
|
self.manufacturer = manufacturer
|
||||||
|
self.hci_device = hci_device
|
||||||
|
|
||||||
|
async def start(self): # Send the startup info
|
||||||
|
chip_info = ChipInfo(
|
||||||
|
name=self.name,
|
||||||
|
chip=Chip(kind=ChipKind.BLUETOOTH, manufacturer=self.manufacturer),
|
||||||
|
)
|
||||||
|
logger.debug(f'Sending chip info to netsim: {chip_info}')
|
||||||
|
await self.hci_device.write(PacketRequest(initial_info=chip_info))
|
||||||
|
|
||||||
|
async def read(self):
|
||||||
|
response = await self.hci_device.read()
|
||||||
|
response_type = response.WhichOneof('response_type')
|
||||||
|
if response_type == 'error':
|
||||||
|
logger.warning(f'received error: {response.error}')
|
||||||
|
raise RuntimeError(response.error)
|
||||||
|
elif response_type == 'hci_packet':
|
||||||
|
return (
|
||||||
|
bytes([response.hci_packet.packet_type])
|
||||||
|
+ response.hci_packet.packet
|
||||||
|
)
|
||||||
|
|
||||||
|
raise ValueError('unsupported response type')
|
||||||
|
|
||||||
|
async def write(self, packet):
|
||||||
|
await self.hci_device.write(
|
||||||
|
PacketRequest(
|
||||||
|
hci_packet=HCIPacket(packet_type=packet[0], packet=packet[1:])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
name = options.get('name', DEFAULT_NAME)
|
||||||
|
manufacturer = DEFAULT_MANUFACTURER
|
||||||
|
|
||||||
|
if server_host == '_' or not server_host:
|
||||||
|
server_host = 'localhost'
|
||||||
|
|
||||||
|
if not server_port:
|
||||||
|
# Look for the gRPC config in a .ini file
|
||||||
|
server_host = 'localhost'
|
||||||
|
server_port = find_grpc_port()
|
||||||
|
if not server_port:
|
||||||
|
raise RuntimeError('gRPC server port not found')
|
||||||
|
|
||||||
|
# Connect to the gRPC server
|
||||||
|
server_address = f'{server_host}:{server_port}'
|
||||||
|
logger.debug(f'Connecting to gRPC server at {server_address}')
|
||||||
|
channel = grpc.aio.insecure_channel(server_address)
|
||||||
|
|
||||||
|
# Connect as a host
|
||||||
|
service = PacketStreamerStub(channel)
|
||||||
|
hci_device = HciDevice(
|
||||||
|
name=name,
|
||||||
|
manufacturer=manufacturer,
|
||||||
|
hci_device=service.StreamPackets(),
|
||||||
|
)
|
||||||
|
await hci_device.start()
|
||||||
|
|
||||||
|
# Create the transport object
|
||||||
|
transport = PumpedTransport(
|
||||||
|
PumpedPacketSource(hci_device.read),
|
||||||
|
PumpedPacketSink(hci_device.write),
|
||||||
|
channel.close,
|
||||||
|
)
|
||||||
|
transport.start()
|
||||||
|
|
||||||
|
return transport
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
async def open_android_netsim_transport(spec):
|
||||||
|
'''
|
||||||
|
Open a transport connection as a client or server, implementing Android's `netsim`
|
||||||
|
simulator protocol over gRPC.
|
||||||
|
The parameter string has this syntax:
|
||||||
|
[<host>:<port>][<options>]
|
||||||
|
Where <options> is a ','-separated list of <name>=<value> pairs.
|
||||||
|
|
||||||
|
General options:
|
||||||
|
mode=host|controller (default: host)
|
||||||
|
Specifies whether the transport is used
|
||||||
|
to connect *to* a netsim server (netsim is the controller), or accept
|
||||||
|
connections *as* a netsim-compatible server.
|
||||||
|
|
||||||
|
In `host` mode:
|
||||||
|
The <host>:<port> part is optional. When not specified, the transport
|
||||||
|
looks for a netsim .ini file, from which it will read the `grpc.backend.port`
|
||||||
|
property.
|
||||||
|
Options for this mode are:
|
||||||
|
name=<name>
|
||||||
|
The "chip" name, used to identify the "chip" instance. This
|
||||||
|
may be useful when several clients are connected, since each needs to use a
|
||||||
|
different name.
|
||||||
|
|
||||||
|
In `controller` mode:
|
||||||
|
The <host>:<port> part is required. <host> may be the address of a local network
|
||||||
|
interface, or '_' to accept connections on all local network interfaces.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
(empty string) --> connect to netsim on the port specified in the .ini file
|
||||||
|
localhost:8555 --> connect to netsim on localhost:8555
|
||||||
|
name=bumble1 --> connect to netsim, using `bumble1` as the "chip" name.
|
||||||
|
localhost:8555,name=bumble1 --> connect to netsim on localhost:8555, using
|
||||||
|
`bumble1` as the "chip" name.
|
||||||
|
_:8877,mode=controller --> accept connections as a controller on any interface
|
||||||
|
on port 8877.
|
||||||
|
'''
|
||||||
|
|
||||||
|
# Parse the parameters
|
||||||
|
params = spec.split(',') if spec else []
|
||||||
|
if params and ':' in params[0]:
|
||||||
|
# Explicit <host>:<port>
|
||||||
|
host, port = params[0].split(':')
|
||||||
|
params_offset = 1
|
||||||
|
else:
|
||||||
|
host = None
|
||||||
|
port = 0
|
||||||
|
params_offset = 0
|
||||||
|
|
||||||
|
options = {}
|
||||||
|
for param in params[params_offset:]:
|
||||||
|
if '=' not in param:
|
||||||
|
raise ValueError('invalid parameter, expected <name>=<value>')
|
||||||
|
option_name, option_value = param.split('=')
|
||||||
|
options[option_name] = option_value
|
||||||
|
|
||||||
|
mode = options.get('mode', 'host')
|
||||||
|
if mode == 'host':
|
||||||
|
return await open_android_netsim_host_transport(host, port, options)
|
||||||
|
if mode == 'controller':
|
||||||
|
if host is None:
|
||||||
|
raise ValueError('<host>:<port> missing')
|
||||||
|
return await open_android_netsim_controller_transport(host, port)
|
||||||
|
|
||||||
|
raise ValueError('invalid mode option')
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
# 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.
|
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
||||||
# source: emulated_bluetooth_packets.proto
|
|
||||||
"""Generated protocol buffer code."""
|
|
||||||
from google.protobuf.internal import builder as _builder
|
|
||||||
from google.protobuf import descriptor as _descriptor
|
|
||||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
|
||||||
from google.protobuf import symbol_database as _symbol_database
|
|
||||||
|
|
||||||
# @@protoc_insertion_point(imports)
|
|
||||||
|
|
||||||
_sym_db = _symbol_database.Default()
|
|
||||||
|
|
||||||
|
|
||||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(
|
|
||||||
b'\n emulated_bluetooth_packets.proto\x12\x1b\x61ndroid.emulation.bluetooth\"\xfb\x01\n\tHCIPacket\x12?\n\x04type\x18\x01 \x01(\x0e\x32\x31.android.emulation.bluetooth.HCIPacket.PacketType\x12\x0e\n\x06packet\x18\x02 \x01(\x0c\"\x9c\x01\n\nPacketType\x12\x1b\n\x17PACKET_TYPE_UNSPECIFIED\x10\x00\x12\x1b\n\x17PACKET_TYPE_HCI_COMMAND\x10\x01\x12\x13\n\x0fPACKET_TYPE_ACL\x10\x02\x12\x13\n\x0fPACKET_TYPE_SCO\x10\x03\x12\x15\n\x11PACKET_TYPE_EVENT\x10\x04\x12\x13\n\x0fPACKET_TYPE_ISO\x10\x05\x42J\n\x1f\x63om.android.emulation.bluetoothP\x01\xf8\x01\x01\xa2\x02\x03\x41\x45\x42\xaa\x02\x1b\x41ndroid.Emulation.Bluetoothb\x06proto3'
|
|
||||||
)
|
|
||||||
|
|
||||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
|
|
||||||
_builder.BuildTopDescriptorsAndMessages(
|
|
||||||
DESCRIPTOR, 'emulated_bluetooth_packets_pb2', globals()
|
|
||||||
)
|
|
||||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
|
||||||
|
|
||||||
DESCRIPTOR._options = None
|
|
||||||
DESCRIPTOR._serialized_options = b'\n\037com.android.emulation.bluetoothP\001\370\001\001\242\002\003AEB\252\002\033Android.Emulation.Bluetooth'
|
|
||||||
_HCIPACKET._serialized_start = 66
|
|
||||||
_HCIPACKET._serialized_end = 317
|
|
||||||
_HCIPACKET_PACKETTYPE._serialized_start = 161
|
|
||||||
_HCIPACKET_PACKETTYPE._serialized_end = 317
|
|
||||||
# @@protoc_insertion_point(module_scope)
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# 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.
|
|
||||||
|
|
||||||
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
|
||||||
"""Client and server classes corresponding to protobuf-defined services."""
|
|
||||||
import grpc
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
# 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.
|
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
||||||
# source: emulated_bluetooth.proto
|
|
||||||
"""Generated protocol buffer code."""
|
|
||||||
from google.protobuf.internal import builder as _builder
|
|
||||||
from google.protobuf import descriptor as _descriptor
|
|
||||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
|
||||||
from google.protobuf import symbol_database as _symbol_database
|
|
||||||
|
|
||||||
# @@protoc_insertion_point(imports)
|
|
||||||
|
|
||||||
_sym_db = _symbol_database.Default()
|
|
||||||
|
|
||||||
|
|
||||||
from . import emulated_bluetooth_packets_pb2 as emulated__bluetooth__packets__pb2
|
|
||||||
|
|
||||||
|
|
||||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(
|
|
||||||
b'\n\x18\x65mulated_bluetooth.proto\x12\x1b\x61ndroid.emulation.bluetooth\x1a emulated_bluetooth_packets.proto\"\x19\n\x07RawData\x12\x0e\n\x06packet\x18\x01 \x01(\x0c\x32\xcb\x02\n\x18\x45mulatedBluetoothService\x12\x64\n\x12registerClassicPhy\x12$.android.emulation.bluetooth.RawData\x1a$.android.emulation.bluetooth.RawData(\x01\x30\x01\x12`\n\x0eregisterBlePhy\x12$.android.emulation.bluetooth.RawData\x1a$.android.emulation.bluetooth.RawData(\x01\x30\x01\x12g\n\x11registerHCIDevice\x12&.android.emulation.bluetooth.HCIPacket\x1a&.android.emulation.bluetooth.HCIPacket(\x01\x30\x01\x42\"\n\x1e\x63om.android.emulator.bluetoothP\x01\x62\x06proto3'
|
|
||||||
)
|
|
||||||
|
|
||||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
|
|
||||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'emulated_bluetooth_pb2', globals())
|
|
||||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
|
||||||
|
|
||||||
DESCRIPTOR._options = None
|
|
||||||
DESCRIPTOR._serialized_options = b'\n\036com.android.emulator.bluetoothP\001'
|
|
||||||
_RAWDATA._serialized_start = 91
|
|
||||||
_RAWDATA._serialized_end = 116
|
|
||||||
_EMULATEDBLUETOOTHSERVICE._serialized_start = 119
|
|
||||||
_EMULATEDBLUETOOTHSERVICE._serialized_end = 450
|
|
||||||
# @@protoc_insertion_point(module_scope)
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
# 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.
|
|
||||||
|
|
||||||
import emulated_bluetooth_packets_pb2 as _emulated_bluetooth_packets_pb2
|
|
||||||
from google.protobuf import descriptor as _descriptor
|
|
||||||
from google.protobuf import message as _message
|
|
||||||
from typing import ClassVar as _ClassVar, Optional as _Optional
|
|
||||||
|
|
||||||
DESCRIPTOR: _descriptor.FileDescriptor
|
|
||||||
|
|
||||||
class RawData(_message.Message):
|
|
||||||
__slots__ = ["packet"]
|
|
||||||
PACKET_FIELD_NUMBER: _ClassVar[int]
|
|
||||||
packet: bytes
|
|
||||||
def __init__(self, packet: _Optional[bytes] = ...) -> None: ...
|
|
||||||
@@ -1,244 +0,0 @@
|
|||||||
# 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.
|
|
||||||
|
|
||||||
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
|
||||||
"""Client and server classes corresponding to protobuf-defined services."""
|
|
||||||
import grpc
|
|
||||||
|
|
||||||
from . import emulated_bluetooth_packets_pb2 as emulated__bluetooth__packets__pb2
|
|
||||||
from . import emulated_bluetooth_pb2 as emulated__bluetooth__pb2
|
|
||||||
|
|
||||||
|
|
||||||
class EmulatedBluetoothServiceStub(object):
|
|
||||||
"""An Emulated Bluetooth Service exposes the emulated bluetooth chip from the
|
|
||||||
android emulator. It allows you to register emulated bluetooth devices and
|
|
||||||
control the packets that are exchanged between the device and the world.
|
|
||||||
|
|
||||||
This service enables you to establish a "virtual network" of emulated
|
|
||||||
bluetooth devices that can interact with each other.
|
|
||||||
|
|
||||||
Note: This is not yet finalized, it is likely that these definitions will
|
|
||||||
evolve.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, channel):
|
|
||||||
"""Constructor.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
channel: A grpc.Channel.
|
|
||||||
"""
|
|
||||||
self.registerClassicPhy = channel.stream_stream(
|
|
||||||
'/android.emulation.bluetooth.EmulatedBluetoothService/registerClassicPhy',
|
|
||||||
request_serializer=emulated__bluetooth__pb2.RawData.SerializeToString,
|
|
||||||
response_deserializer=emulated__bluetooth__pb2.RawData.FromString,
|
|
||||||
)
|
|
||||||
self.registerBlePhy = channel.stream_stream(
|
|
||||||
'/android.emulation.bluetooth.EmulatedBluetoothService/registerBlePhy',
|
|
||||||
request_serializer=emulated__bluetooth__pb2.RawData.SerializeToString,
|
|
||||||
response_deserializer=emulated__bluetooth__pb2.RawData.FromString,
|
|
||||||
)
|
|
||||||
self.registerHCIDevice = channel.stream_stream(
|
|
||||||
'/android.emulation.bluetooth.EmulatedBluetoothService/registerHCIDevice',
|
|
||||||
request_serializer=emulated__bluetooth__packets__pb2.HCIPacket.SerializeToString,
|
|
||||||
response_deserializer=emulated__bluetooth__packets__pb2.HCIPacket.FromString,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class EmulatedBluetoothServiceServicer(object):
|
|
||||||
"""An Emulated Bluetooth Service exposes the emulated bluetooth chip from the
|
|
||||||
android emulator. It allows you to register emulated bluetooth devices and
|
|
||||||
control the packets that are exchanged between the device and the world.
|
|
||||||
|
|
||||||
This service enables you to establish a "virtual network" of emulated
|
|
||||||
bluetooth devices that can interact with each other.
|
|
||||||
|
|
||||||
Note: This is not yet finalized, it is likely that these definitions will
|
|
||||||
evolve.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def registerClassicPhy(self, request_iterator, context):
|
|
||||||
"""Connect device to link layer. This will establish a direct connection
|
|
||||||
to the emulated bluetooth chip and configure the following:
|
|
||||||
|
|
||||||
- Each connection creates a new device and attaches it to the link layer
|
|
||||||
- Link Layer packets are transmitted directly to the phy
|
|
||||||
|
|
||||||
This should be used for classic connections.
|
|
||||||
|
|
||||||
This is used to directly connect various android emulators together.
|
|
||||||
For example a wear device can connect to an android emulator through
|
|
||||||
this.
|
|
||||||
"""
|
|
||||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
|
||||||
context.set_details('Method not implemented!')
|
|
||||||
raise NotImplementedError('Method not implemented!')
|
|
||||||
|
|
||||||
def registerBlePhy(self, request_iterator, context):
|
|
||||||
"""Connect device to link layer. This will establish a direct connection
|
|
||||||
to root canal and execute the following:
|
|
||||||
|
|
||||||
- Each connection creates a new device and attaches it to the link layer
|
|
||||||
- Link Layer packets are transmitted directly to the phy
|
|
||||||
|
|
||||||
This should be used for BLE connections.
|
|
||||||
|
|
||||||
This is used to directly connect various android emulators together.
|
|
||||||
For example a wear device can connect to an android emulator through
|
|
||||||
this.
|
|
||||||
"""
|
|
||||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
|
||||||
context.set_details('Method not implemented!')
|
|
||||||
raise NotImplementedError('Method not implemented!')
|
|
||||||
|
|
||||||
def registerHCIDevice(self, request_iterator, context):
|
|
||||||
"""Connect the device to the emulated bluetooth chip. The device will
|
|
||||||
participate in the network. You can configure the chip to scan, advertise
|
|
||||||
and setup connections with other devices that are connected to the
|
|
||||||
network.
|
|
||||||
|
|
||||||
This is usually used when you have a need for an emulated bluetooth chip
|
|
||||||
and have a bluetooth stack that can interpret and handle the packets
|
|
||||||
correctly.
|
|
||||||
|
|
||||||
For example the apache nimble stack can use this endpoint as the
|
|
||||||
transport layer.
|
|
||||||
"""
|
|
||||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
|
||||||
context.set_details('Method not implemented!')
|
|
||||||
raise NotImplementedError('Method not implemented!')
|
|
||||||
|
|
||||||
|
|
||||||
def add_EmulatedBluetoothServiceServicer_to_server(servicer, server):
|
|
||||||
rpc_method_handlers = {
|
|
||||||
'registerClassicPhy': grpc.stream_stream_rpc_method_handler(
|
|
||||||
servicer.registerClassicPhy,
|
|
||||||
request_deserializer=emulated__bluetooth__pb2.RawData.FromString,
|
|
||||||
response_serializer=emulated__bluetooth__pb2.RawData.SerializeToString,
|
|
||||||
),
|
|
||||||
'registerBlePhy': grpc.stream_stream_rpc_method_handler(
|
|
||||||
servicer.registerBlePhy,
|
|
||||||
request_deserializer=emulated__bluetooth__pb2.RawData.FromString,
|
|
||||||
response_serializer=emulated__bluetooth__pb2.RawData.SerializeToString,
|
|
||||||
),
|
|
||||||
'registerHCIDevice': grpc.stream_stream_rpc_method_handler(
|
|
||||||
servicer.registerHCIDevice,
|
|
||||||
request_deserializer=emulated__bluetooth__packets__pb2.HCIPacket.FromString,
|
|
||||||
response_serializer=emulated__bluetooth__packets__pb2.HCIPacket.SerializeToString,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
generic_handler = grpc.method_handlers_generic_handler(
|
|
||||||
'android.emulation.bluetooth.EmulatedBluetoothService', rpc_method_handlers
|
|
||||||
)
|
|
||||||
server.add_generic_rpc_handlers((generic_handler,))
|
|
||||||
|
|
||||||
|
|
||||||
# This class is part of an EXPERIMENTAL API.
|
|
||||||
class EmulatedBluetoothService(object):
|
|
||||||
"""An Emulated Bluetooth Service exposes the emulated bluetooth chip from the
|
|
||||||
android emulator. It allows you to register emulated bluetooth devices and
|
|
||||||
control the packets that are exchanged between the device and the world.
|
|
||||||
|
|
||||||
This service enables you to establish a "virtual network" of emulated
|
|
||||||
bluetooth devices that can interact with each other.
|
|
||||||
|
|
||||||
Note: This is not yet finalized, it is likely that these definitions will
|
|
||||||
evolve.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def registerClassicPhy(
|
|
||||||
request_iterator,
|
|
||||||
target,
|
|
||||||
options=(),
|
|
||||||
channel_credentials=None,
|
|
||||||
call_credentials=None,
|
|
||||||
insecure=False,
|
|
||||||
compression=None,
|
|
||||||
wait_for_ready=None,
|
|
||||||
timeout=None,
|
|
||||||
metadata=None,
|
|
||||||
):
|
|
||||||
return grpc.experimental.stream_stream(
|
|
||||||
request_iterator,
|
|
||||||
target,
|
|
||||||
'/android.emulation.bluetooth.EmulatedBluetoothService/registerClassicPhy',
|
|
||||||
emulated__bluetooth__pb2.RawData.SerializeToString,
|
|
||||||
emulated__bluetooth__pb2.RawData.FromString,
|
|
||||||
options,
|
|
||||||
channel_credentials,
|
|
||||||
insecure,
|
|
||||||
call_credentials,
|
|
||||||
compression,
|
|
||||||
wait_for_ready,
|
|
||||||
timeout,
|
|
||||||
metadata,
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def registerBlePhy(
|
|
||||||
request_iterator,
|
|
||||||
target,
|
|
||||||
options=(),
|
|
||||||
channel_credentials=None,
|
|
||||||
call_credentials=None,
|
|
||||||
insecure=False,
|
|
||||||
compression=None,
|
|
||||||
wait_for_ready=None,
|
|
||||||
timeout=None,
|
|
||||||
metadata=None,
|
|
||||||
):
|
|
||||||
return grpc.experimental.stream_stream(
|
|
||||||
request_iterator,
|
|
||||||
target,
|
|
||||||
'/android.emulation.bluetooth.EmulatedBluetoothService/registerBlePhy',
|
|
||||||
emulated__bluetooth__pb2.RawData.SerializeToString,
|
|
||||||
emulated__bluetooth__pb2.RawData.FromString,
|
|
||||||
options,
|
|
||||||
channel_credentials,
|
|
||||||
insecure,
|
|
||||||
call_credentials,
|
|
||||||
compression,
|
|
||||||
wait_for_ready,
|
|
||||||
timeout,
|
|
||||||
metadata,
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def registerHCIDevice(
|
|
||||||
request_iterator,
|
|
||||||
target,
|
|
||||||
options=(),
|
|
||||||
channel_credentials=None,
|
|
||||||
call_credentials=None,
|
|
||||||
insecure=False,
|
|
||||||
compression=None,
|
|
||||||
wait_for_ready=None,
|
|
||||||
timeout=None,
|
|
||||||
metadata=None,
|
|
||||||
):
|
|
||||||
return grpc.experimental.stream_stream(
|
|
||||||
request_iterator,
|
|
||||||
target,
|
|
||||||
'/android.emulation.bluetooth.EmulatedBluetoothService/registerHCIDevice',
|
|
||||||
emulated__bluetooth__packets__pb2.HCIPacket.SerializeToString,
|
|
||||||
emulated__bluetooth__packets__pb2.HCIPacket.FromString,
|
|
||||||
options,
|
|
||||||
channel_credentials,
|
|
||||||
insecure,
|
|
||||||
call_credentials,
|
|
||||||
compression,
|
|
||||||
wait_for_ready,
|
|
||||||
timeout,
|
|
||||||
metadata,
|
|
||||||
)
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
# 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.
|
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
||||||
# source: emulated_bluetooth_vhci.proto
|
|
||||||
"""Generated protocol buffer code."""
|
|
||||||
from google.protobuf.internal import builder as _builder
|
|
||||||
from google.protobuf import descriptor as _descriptor
|
|
||||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
|
||||||
from google.protobuf import symbol_database as _symbol_database
|
|
||||||
|
|
||||||
# @@protoc_insertion_point(imports)
|
|
||||||
|
|
||||||
_sym_db = _symbol_database.Default()
|
|
||||||
|
|
||||||
|
|
||||||
from . import emulated_bluetooth_packets_pb2 as emulated__bluetooth__packets__pb2
|
|
||||||
|
|
||||||
|
|
||||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(
|
|
||||||
b'\n\x1d\x65mulated_bluetooth_vhci.proto\x12\x1b\x61ndroid.emulation.bluetooth\x1a emulated_bluetooth_packets.proto2y\n\x15VhciForwardingService\x12`\n\nattachVhci\x12&.android.emulation.bluetooth.HCIPacket\x1a&.android.emulation.bluetooth.HCIPacket(\x01\x30\x01\x42J\n\x1f\x63om.android.emulation.bluetoothP\x01\xf8\x01\x01\xa2\x02\x03\x41\x45\x42\xaa\x02\x1b\x41ndroid.Emulation.Bluetoothb\x06proto3'
|
|
||||||
)
|
|
||||||
|
|
||||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
|
|
||||||
_builder.BuildTopDescriptorsAndMessages(
|
|
||||||
DESCRIPTOR, 'emulated_bluetooth_vhci_pb2', globals()
|
|
||||||
)
|
|
||||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
|
||||||
|
|
||||||
DESCRIPTOR._options = None
|
|
||||||
DESCRIPTOR._serialized_options = b'\n\037com.android.emulation.bluetoothP\001\370\001\001\242\002\003AEB\252\002\033Android.Emulation.Bluetooth'
|
|
||||||
_VHCIFORWARDINGSERVICE._serialized_start = 96
|
|
||||||
_VHCIFORWARDINGSERVICE._serialized_end = 217
|
|
||||||
# @@protoc_insertion_point(module_scope)
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# 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.
|
|
||||||
|
|
||||||
import emulated_bluetooth_packets_pb2 as _emulated_bluetooth_packets_pb2
|
|
||||||
from google.protobuf import descriptor as _descriptor
|
|
||||||
from typing import ClassVar as _ClassVar
|
|
||||||
|
|
||||||
DESCRIPTOR: _descriptor.FileDescriptor
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||||
|
# source: common.proto
|
||||||
|
"""Generated protocol buffer code."""
|
||||||
|
from google.protobuf.internal import builder as _builder
|
||||||
|
from google.protobuf import descriptor as _descriptor
|
||||||
|
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||||
|
from google.protobuf import symbol_database as _symbol_database
|
||||||
|
# @@protoc_insertion_point(imports)
|
||||||
|
|
||||||
|
_sym_db = _symbol_database.Default()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0c\x63ommon.proto\x12\rnetsim.common*=\n\x08\x43hipKind\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\r\n\tBLUETOOTH\x10\x01\x12\x08\n\x04WIFI\x10\x02\x12\x07\n\x03UWB\x10\x03\x62\x06proto3')
|
||||||
|
|
||||||
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
|
||||||
|
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'common_pb2', globals())
|
||||||
|
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||||
|
|
||||||
|
DESCRIPTOR._options = None
|
||||||
|
_CHIPKIND._serialized_start=31
|
||||||
|
_CHIPKIND._serialized_end=92
|
||||||
|
# @@protoc_insertion_point(module_scope)
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
|
||||||
|
from google.protobuf import descriptor as _descriptor
|
||||||
|
from typing import ClassVar as _ClassVar
|
||||||
|
|
||||||
|
BLUETOOTH: ChipKind
|
||||||
|
DESCRIPTOR: _descriptor.FileDescriptor
|
||||||
|
UNSPECIFIED: ChipKind
|
||||||
|
UWB: ChipKind
|
||||||
|
WIFI: ChipKind
|
||||||
|
|
||||||
|
class ChipKind(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||||
|
__slots__ = []
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
||||||
|
"""Client and server classes corresponding to protobuf-defined services."""
|
||||||
|
import grpc
|
||||||
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,158 @@
|
|||||||
|
from . import grpc_endpoint_description_pb2 as _grpc_endpoint_description_pb2
|
||||||
|
from google.protobuf import empty_pb2 as _empty_pb2
|
||||||
|
from google.protobuf.internal import containers as _containers
|
||||||
|
from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
|
||||||
|
from google.protobuf import descriptor as _descriptor
|
||||||
|
from google.protobuf import message as _message
|
||||||
|
from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union
|
||||||
|
|
||||||
|
DESCRIPTOR: _descriptor.FileDescriptor
|
||||||
|
|
||||||
|
class Advertisement(_message.Message):
|
||||||
|
__slots__ = ["connection_mode", "device_name", "discovery_mode"]
|
||||||
|
class ConnectionMode(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||||
|
__slots__ = []
|
||||||
|
class DiscoveryMode(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||||
|
__slots__ = []
|
||||||
|
CONNECTION_MODE_DIRECTED: Advertisement.ConnectionMode
|
||||||
|
CONNECTION_MODE_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
CONNECTION_MODE_NON_CONNECTABLE: Advertisement.ConnectionMode
|
||||||
|
CONNECTION_MODE_UNDIRECTED: Advertisement.ConnectionMode
|
||||||
|
CONNECTION_MODE_UNSPECIFIED: Advertisement.ConnectionMode
|
||||||
|
DEVICE_NAME_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
DISCOVERY_MODE_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
DISCOVERY_MODE_GENERAL: Advertisement.DiscoveryMode
|
||||||
|
DISCOVERY_MODE_LIMITED: Advertisement.DiscoveryMode
|
||||||
|
DISCOVERY_MODE_NON_DISCOVERABLE: Advertisement.DiscoveryMode
|
||||||
|
DISCOVERY_MODE_UNSPECIFIED: Advertisement.DiscoveryMode
|
||||||
|
connection_mode: Advertisement.ConnectionMode
|
||||||
|
device_name: str
|
||||||
|
discovery_mode: Advertisement.DiscoveryMode
|
||||||
|
def __init__(self, device_name: _Optional[str] = ..., connection_mode: _Optional[_Union[Advertisement.ConnectionMode, str]] = ..., discovery_mode: _Optional[_Union[Advertisement.DiscoveryMode, str]] = ...) -> None: ...
|
||||||
|
|
||||||
|
class CallbackIdentifier(_message.Message):
|
||||||
|
__slots__ = ["identity"]
|
||||||
|
IDENTITY_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
identity: str
|
||||||
|
def __init__(self, identity: _Optional[str] = ...) -> None: ...
|
||||||
|
|
||||||
|
class CharacteristicValueRequest(_message.Message):
|
||||||
|
__slots__ = ["callback_device_id", "callback_id", "data", "from_device"]
|
||||||
|
CALLBACK_DEVICE_ID_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
CALLBACK_ID_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
DATA_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
FROM_DEVICE_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
callback_device_id: CallbackIdentifier
|
||||||
|
callback_id: Uuid
|
||||||
|
data: bytes
|
||||||
|
from_device: DeviceIdentifier
|
||||||
|
def __init__(self, callback_device_id: _Optional[_Union[CallbackIdentifier, _Mapping]] = ..., from_device: _Optional[_Union[DeviceIdentifier, _Mapping]] = ..., callback_id: _Optional[_Union[Uuid, _Mapping]] = ..., data: _Optional[bytes] = ...) -> None: ...
|
||||||
|
|
||||||
|
class CharacteristicValueResponse(_message.Message):
|
||||||
|
__slots__ = ["data", "status"]
|
||||||
|
class GattStatus(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||||
|
__slots__ = []
|
||||||
|
DATA_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
GATT_STATUS_FAILURE: CharacteristicValueResponse.GattStatus
|
||||||
|
GATT_STATUS_SUCCESS: CharacteristicValueResponse.GattStatus
|
||||||
|
GATT_STATUS_UNSPECIFIED: CharacteristicValueResponse.GattStatus
|
||||||
|
STATUS_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
data: bytes
|
||||||
|
status: CharacteristicValueResponse.GattStatus
|
||||||
|
def __init__(self, status: _Optional[_Union[CharacteristicValueResponse.GattStatus, str]] = ..., data: _Optional[bytes] = ...) -> None: ...
|
||||||
|
|
||||||
|
class ConnectionStateChange(_message.Message):
|
||||||
|
__slots__ = ["callback_device_id", "from_device", "new_state"]
|
||||||
|
class ConnectionState(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||||
|
__slots__ = []
|
||||||
|
CALLBACK_DEVICE_ID_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
CONNECTION_STATE_CONNECTED: ConnectionStateChange.ConnectionState
|
||||||
|
CONNECTION_STATE_DISCONNECTED: ConnectionStateChange.ConnectionState
|
||||||
|
CONNECTION_STATE_UNDEFINED: ConnectionStateChange.ConnectionState
|
||||||
|
FROM_DEVICE_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
NEW_STATE_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
callback_device_id: CallbackIdentifier
|
||||||
|
from_device: DeviceIdentifier
|
||||||
|
new_state: ConnectionStateChange.ConnectionState
|
||||||
|
def __init__(self, callback_device_id: _Optional[_Union[CallbackIdentifier, _Mapping]] = ..., from_device: _Optional[_Union[DeviceIdentifier, _Mapping]] = ..., new_state: _Optional[_Union[ConnectionStateChange.ConnectionState, str]] = ...) -> None: ...
|
||||||
|
|
||||||
|
class DeviceIdentifier(_message.Message):
|
||||||
|
__slots__ = ["address"]
|
||||||
|
ADDRESS_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
address: str
|
||||||
|
def __init__(self, address: _Optional[str] = ...) -> None: ...
|
||||||
|
|
||||||
|
class GattCharacteristic(_message.Message):
|
||||||
|
__slots__ = ["callback_id", "permissions", "properties", "uuid"]
|
||||||
|
class Permissions(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||||
|
__slots__ = []
|
||||||
|
class Properties(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||||
|
__slots__ = []
|
||||||
|
CALLBACK_ID_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
PERMISSIONS_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
PERMISSION_READ: GattCharacteristic.Permissions
|
||||||
|
PERMISSION_READ_ENCRYPTED: GattCharacteristic.Permissions
|
||||||
|
PERMISSION_READ_ENCRYPTED_MITM: GattCharacteristic.Permissions
|
||||||
|
PERMISSION_UNSPECIFIED: GattCharacteristic.Permissions
|
||||||
|
PERMISSION_WRITE: GattCharacteristic.Permissions
|
||||||
|
PERMISSION_WRITE_ENCRYPTED: GattCharacteristic.Permissions
|
||||||
|
PERMISSION_WRITE_ENCRYPTED_MITM: GattCharacteristic.Permissions
|
||||||
|
PERMISSION_WRITE_SIGNED: GattCharacteristic.Permissions
|
||||||
|
PERMISSION_WRITE_SIGNED_MITM: GattCharacteristic.Permissions
|
||||||
|
PROPERTIES_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
PROPERTY_BROADCAST: GattCharacteristic.Properties
|
||||||
|
PROPERTY_EXTENDED_PROPS: GattCharacteristic.Properties
|
||||||
|
PROPERTY_INDICATE: GattCharacteristic.Properties
|
||||||
|
PROPERTY_NOTIFY: GattCharacteristic.Properties
|
||||||
|
PROPERTY_READ: GattCharacteristic.Properties
|
||||||
|
PROPERTY_SIGNED_WRITE: GattCharacteristic.Properties
|
||||||
|
PROPERTY_UNSPECIFIED: GattCharacteristic.Properties
|
||||||
|
PROPERTY_WRITE: GattCharacteristic.Properties
|
||||||
|
PROPERTY_WRITE_NO_RESPONSE: GattCharacteristic.Properties
|
||||||
|
UUID_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
callback_id: Uuid
|
||||||
|
permissions: int
|
||||||
|
properties: int
|
||||||
|
uuid: Uuid
|
||||||
|
def __init__(self, uuid: _Optional[_Union[Uuid, _Mapping]] = ..., properties: _Optional[int] = ..., permissions: _Optional[int] = ..., callback_id: _Optional[_Union[Uuid, _Mapping]] = ...) -> None: ...
|
||||||
|
|
||||||
|
class GattDevice(_message.Message):
|
||||||
|
__slots__ = ["advertisement", "endpoint", "profile"]
|
||||||
|
ADVERTISEMENT_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
ENDPOINT_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
PROFILE_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
advertisement: Advertisement
|
||||||
|
endpoint: _grpc_endpoint_description_pb2.Endpoint
|
||||||
|
profile: GattProfile
|
||||||
|
def __init__(self, endpoint: _Optional[_Union[_grpc_endpoint_description_pb2.Endpoint, _Mapping]] = ..., advertisement: _Optional[_Union[Advertisement, _Mapping]] = ..., profile: _Optional[_Union[GattProfile, _Mapping]] = ...) -> None: ...
|
||||||
|
|
||||||
|
class GattProfile(_message.Message):
|
||||||
|
__slots__ = ["services"]
|
||||||
|
SERVICES_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
services: _containers.RepeatedCompositeFieldContainer[GattService]
|
||||||
|
def __init__(self, services: _Optional[_Iterable[_Union[GattService, _Mapping]]] = ...) -> None: ...
|
||||||
|
|
||||||
|
class GattService(_message.Message):
|
||||||
|
__slots__ = ["characteristics", "service_type", "uuid"]
|
||||||
|
class ServiceType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||||
|
__slots__ = []
|
||||||
|
CHARACTERISTICS_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
SERVICE_TYPE_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
SERVICE_TYPE_PRIMARY: GattService.ServiceType
|
||||||
|
SERVICE_TYPE_SECONDARY: GattService.ServiceType
|
||||||
|
SERVICE_TYPE_UNSPECIFIED: GattService.ServiceType
|
||||||
|
UUID_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
characteristics: _containers.RepeatedCompositeFieldContainer[GattCharacteristic]
|
||||||
|
service_type: GattService.ServiceType
|
||||||
|
uuid: Uuid
|
||||||
|
def __init__(self, uuid: _Optional[_Union[Uuid, _Mapping]] = ..., service_type: _Optional[_Union[GattService.ServiceType, str]] = ..., characteristics: _Optional[_Iterable[_Union[GattCharacteristic, _Mapping]]] = ...) -> None: ...
|
||||||
|
|
||||||
|
class Uuid(_message.Message):
|
||||||
|
__slots__ = ["id", "lsb", "msb"]
|
||||||
|
ID_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
LSB_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
MSB_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
id: int
|
||||||
|
lsb: int
|
||||||
|
msb: int
|
||||||
|
def __init__(self, id: _Optional[int] = ..., lsb: _Optional[int] = ..., msb: _Optional[int] = ...) -> None: ...
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
||||||
|
"""Client and server classes corresponding to protobuf-defined services."""
|
||||||
|
import grpc
|
||||||
|
|
||||||
|
from . import emulated_bluetooth_device_pb2 as emulated__bluetooth__device__pb2
|
||||||
|
from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2
|
||||||
|
|
||||||
|
|
||||||
|
class GattDeviceServiceStub(object):
|
||||||
|
"""You can provide your own GattDevice by implementing this service
|
||||||
|
and registering it with the android emulator.
|
||||||
|
|
||||||
|
The device will appear as a real bluetooth device, and you will
|
||||||
|
receive callbacks when the bluetooth system wants to
|
||||||
|
read, write or observe a characteristic.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, channel):
|
||||||
|
"""Constructor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
channel: A grpc.Channel.
|
||||||
|
"""
|
||||||
|
self.OnCharacteristicReadRequest = channel.unary_unary(
|
||||||
|
'/android.emulation.bluetooth.GattDeviceService/OnCharacteristicReadRequest',
|
||||||
|
request_serializer=emulated__bluetooth__device__pb2.CharacteristicValueRequest.SerializeToString,
|
||||||
|
response_deserializer=emulated__bluetooth__device__pb2.CharacteristicValueResponse.FromString,
|
||||||
|
)
|
||||||
|
self.OnCharacteristicWriteRequest = channel.unary_unary(
|
||||||
|
'/android.emulation.bluetooth.GattDeviceService/OnCharacteristicWriteRequest',
|
||||||
|
request_serializer=emulated__bluetooth__device__pb2.CharacteristicValueRequest.SerializeToString,
|
||||||
|
response_deserializer=emulated__bluetooth__device__pb2.CharacteristicValueResponse.FromString,
|
||||||
|
)
|
||||||
|
self.OnCharacteristicObserveRequest = channel.unary_stream(
|
||||||
|
'/android.emulation.bluetooth.GattDeviceService/OnCharacteristicObserveRequest',
|
||||||
|
request_serializer=emulated__bluetooth__device__pb2.CharacteristicValueRequest.SerializeToString,
|
||||||
|
response_deserializer=emulated__bluetooth__device__pb2.CharacteristicValueResponse.FromString,
|
||||||
|
)
|
||||||
|
self.OnConnectionStateChange = channel.unary_unary(
|
||||||
|
'/android.emulation.bluetooth.GattDeviceService/OnConnectionStateChange',
|
||||||
|
request_serializer=emulated__bluetooth__device__pb2.ConnectionStateChange.SerializeToString,
|
||||||
|
response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GattDeviceServiceServicer(object):
|
||||||
|
"""You can provide your own GattDevice by implementing this service
|
||||||
|
and registering it with the android emulator.
|
||||||
|
|
||||||
|
The device will appear as a real bluetooth device, and you will
|
||||||
|
receive callbacks when the bluetooth system wants to
|
||||||
|
read, write or observe a characteristic.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def OnCharacteristicReadRequest(self, request, context):
|
||||||
|
"""A remote client has requested to read a local characteristic.
|
||||||
|
|
||||||
|
Return the current observed value.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def OnCharacteristicWriteRequest(self, request, context):
|
||||||
|
"""A remote client has requested to write to a local characteristic.
|
||||||
|
|
||||||
|
Return the current observed value.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def OnCharacteristicObserveRequest(self, request, context):
|
||||||
|
"""Listens for notifications from the emulated device, the device should
|
||||||
|
write to the stream with a response when a change has occurred.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def OnConnectionStateChange(self, request, context):
|
||||||
|
"""A remote device has been connected or disconnected.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
|
||||||
|
def add_GattDeviceServiceServicer_to_server(servicer, server):
|
||||||
|
rpc_method_handlers = {
|
||||||
|
'OnCharacteristicReadRequest': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.OnCharacteristicReadRequest,
|
||||||
|
request_deserializer=emulated__bluetooth__device__pb2.CharacteristicValueRequest.FromString,
|
||||||
|
response_serializer=emulated__bluetooth__device__pb2.CharacteristicValueResponse.SerializeToString,
|
||||||
|
),
|
||||||
|
'OnCharacteristicWriteRequest': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.OnCharacteristicWriteRequest,
|
||||||
|
request_deserializer=emulated__bluetooth__device__pb2.CharacteristicValueRequest.FromString,
|
||||||
|
response_serializer=emulated__bluetooth__device__pb2.CharacteristicValueResponse.SerializeToString,
|
||||||
|
),
|
||||||
|
'OnCharacteristicObserveRequest': grpc.unary_stream_rpc_method_handler(
|
||||||
|
servicer.OnCharacteristicObserveRequest,
|
||||||
|
request_deserializer=emulated__bluetooth__device__pb2.CharacteristicValueRequest.FromString,
|
||||||
|
response_serializer=emulated__bluetooth__device__pb2.CharacteristicValueResponse.SerializeToString,
|
||||||
|
),
|
||||||
|
'OnConnectionStateChange': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.OnConnectionStateChange,
|
||||||
|
request_deserializer=emulated__bluetooth__device__pb2.ConnectionStateChange.FromString,
|
||||||
|
response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
generic_handler = grpc.method_handlers_generic_handler(
|
||||||
|
'android.emulation.bluetooth.GattDeviceService', rpc_method_handlers)
|
||||||
|
server.add_generic_rpc_handlers((generic_handler,))
|
||||||
|
|
||||||
|
|
||||||
|
# This class is part of an EXPERIMENTAL API.
|
||||||
|
class GattDeviceService(object):
|
||||||
|
"""You can provide your own GattDevice by implementing this service
|
||||||
|
and registering it with the android emulator.
|
||||||
|
|
||||||
|
The device will appear as a real bluetooth device, and you will
|
||||||
|
receive callbacks when the bluetooth system wants to
|
||||||
|
read, write or observe a characteristic.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def OnCharacteristicReadRequest(request,
|
||||||
|
target,
|
||||||
|
options=(),
|
||||||
|
channel_credentials=None,
|
||||||
|
call_credentials=None,
|
||||||
|
insecure=False,
|
||||||
|
compression=None,
|
||||||
|
wait_for_ready=None,
|
||||||
|
timeout=None,
|
||||||
|
metadata=None):
|
||||||
|
return grpc.experimental.unary_unary(request, target, '/android.emulation.bluetooth.GattDeviceService/OnCharacteristicReadRequest',
|
||||||
|
emulated__bluetooth__device__pb2.CharacteristicValueRequest.SerializeToString,
|
||||||
|
emulated__bluetooth__device__pb2.CharacteristicValueResponse.FromString,
|
||||||
|
options, channel_credentials,
|
||||||
|
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def OnCharacteristicWriteRequest(request,
|
||||||
|
target,
|
||||||
|
options=(),
|
||||||
|
channel_credentials=None,
|
||||||
|
call_credentials=None,
|
||||||
|
insecure=False,
|
||||||
|
compression=None,
|
||||||
|
wait_for_ready=None,
|
||||||
|
timeout=None,
|
||||||
|
metadata=None):
|
||||||
|
return grpc.experimental.unary_unary(request, target, '/android.emulation.bluetooth.GattDeviceService/OnCharacteristicWriteRequest',
|
||||||
|
emulated__bluetooth__device__pb2.CharacteristicValueRequest.SerializeToString,
|
||||||
|
emulated__bluetooth__device__pb2.CharacteristicValueResponse.FromString,
|
||||||
|
options, channel_credentials,
|
||||||
|
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def OnCharacteristicObserveRequest(request,
|
||||||
|
target,
|
||||||
|
options=(),
|
||||||
|
channel_credentials=None,
|
||||||
|
call_credentials=None,
|
||||||
|
insecure=False,
|
||||||
|
compression=None,
|
||||||
|
wait_for_ready=None,
|
||||||
|
timeout=None,
|
||||||
|
metadata=None):
|
||||||
|
return grpc.experimental.unary_stream(request, target, '/android.emulation.bluetooth.GattDeviceService/OnCharacteristicObserveRequest',
|
||||||
|
emulated__bluetooth__device__pb2.CharacteristicValueRequest.SerializeToString,
|
||||||
|
emulated__bluetooth__device__pb2.CharacteristicValueResponse.FromString,
|
||||||
|
options, channel_credentials,
|
||||||
|
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def OnConnectionStateChange(request,
|
||||||
|
target,
|
||||||
|
options=(),
|
||||||
|
channel_credentials=None,
|
||||||
|
call_credentials=None,
|
||||||
|
insecure=False,
|
||||||
|
compression=None,
|
||||||
|
wait_for_ready=None,
|
||||||
|
timeout=None,
|
||||||
|
metadata=None):
|
||||||
|
return grpc.experimental.unary_unary(request, target, '/android.emulation.bluetooth.GattDeviceService/OnConnectionStateChange',
|
||||||
|
emulated__bluetooth__device__pb2.ConnectionStateChange.SerializeToString,
|
||||||
|
google_dot_protobuf_dot_empty__pb2.Empty.FromString,
|
||||||
|
options, channel_credentials,
|
||||||
|
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||||
|
# source: emulated_bluetooth_packets.proto
|
||||||
|
"""Generated protocol buffer code."""
|
||||||
|
from google.protobuf.internal import builder as _builder
|
||||||
|
from google.protobuf import descriptor as _descriptor
|
||||||
|
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||||
|
from google.protobuf import symbol_database as _symbol_database
|
||||||
|
# @@protoc_insertion_point(imports)
|
||||||
|
|
||||||
|
_sym_db = _symbol_database.Default()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n emulated_bluetooth_packets.proto\x12\x1b\x61ndroid.emulation.bluetooth\"\xfb\x01\n\tHCIPacket\x12?\n\x04type\x18\x01 \x01(\x0e\x32\x31.android.emulation.bluetooth.HCIPacket.PacketType\x12\x0e\n\x06packet\x18\x02 \x01(\x0c\"\x9c\x01\n\nPacketType\x12\x1b\n\x17PACKET_TYPE_UNSPECIFIED\x10\x00\x12\x1b\n\x17PACKET_TYPE_HCI_COMMAND\x10\x01\x12\x13\n\x0fPACKET_TYPE_ACL\x10\x02\x12\x13\n\x0fPACKET_TYPE_SCO\x10\x03\x12\x15\n\x11PACKET_TYPE_EVENT\x10\x04\x12\x13\n\x0fPACKET_TYPE_ISO\x10\x05\x42J\n\x1f\x63om.android.emulation.bluetoothP\x01\xf8\x01\x01\xa2\x02\x03\x41\x45\x42\xaa\x02\x1b\x41ndroid.Emulation.Bluetoothb\x06proto3')
|
||||||
|
|
||||||
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
|
||||||
|
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'emulated_bluetooth_packets_pb2', globals())
|
||||||
|
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||||
|
|
||||||
|
DESCRIPTOR._options = None
|
||||||
|
DESCRIPTOR._serialized_options = b'\n\037com.android.emulation.bluetoothP\001\370\001\001\242\002\003AEB\252\002\033Android.Emulation.Bluetooth'
|
||||||
|
_HCIPACKET._serialized_start=66
|
||||||
|
_HCIPACKET._serialized_end=317
|
||||||
|
_HCIPACKET_PACKETTYPE._serialized_start=161
|
||||||
|
_HCIPACKET_PACKETTYPE._serialized_end=317
|
||||||
|
# @@protoc_insertion_point(module_scope)
|
||||||
+1
-20
@@ -1,17 +1,3 @@
|
|||||||
# 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.
|
|
||||||
|
|
||||||
from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
|
from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
|
||||||
from google.protobuf import descriptor as _descriptor
|
from google.protobuf import descriptor as _descriptor
|
||||||
from google.protobuf import message as _message
|
from google.protobuf import message as _message
|
||||||
@@ -21,7 +7,6 @@ DESCRIPTOR: _descriptor.FileDescriptor
|
|||||||
|
|
||||||
class HCIPacket(_message.Message):
|
class HCIPacket(_message.Message):
|
||||||
__slots__ = ["packet", "type"]
|
__slots__ = ["packet", "type"]
|
||||||
|
|
||||||
class PacketType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
class PacketType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||||
__slots__ = []
|
__slots__ = []
|
||||||
PACKET_FIELD_NUMBER: _ClassVar[int]
|
PACKET_FIELD_NUMBER: _ClassVar[int]
|
||||||
@@ -34,8 +19,4 @@ class HCIPacket(_message.Message):
|
|||||||
TYPE_FIELD_NUMBER: _ClassVar[int]
|
TYPE_FIELD_NUMBER: _ClassVar[int]
|
||||||
packet: bytes
|
packet: bytes
|
||||||
type: HCIPacket.PacketType
|
type: HCIPacket.PacketType
|
||||||
def __init__(
|
def __init__(self, type: _Optional[_Union[HCIPacket.PacketType, str]] = ..., packet: _Optional[bytes] = ...) -> None: ...
|
||||||
self,
|
|
||||||
type: _Optional[_Union[HCIPacket.PacketType, str]] = ...,
|
|
||||||
packet: _Optional[bytes] = ...,
|
|
||||||
) -> None: ...
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
||||||
|
"""Client and server classes corresponding to protobuf-defined services."""
|
||||||
|
import grpc
|
||||||
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||||
|
# source: emulated_bluetooth.proto
|
||||||
|
"""Generated protocol buffer code."""
|
||||||
|
from google.protobuf.internal import builder as _builder
|
||||||
|
from google.protobuf import descriptor as _descriptor
|
||||||
|
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||||
|
from google.protobuf import symbol_database as _symbol_database
|
||||||
|
# @@protoc_insertion_point(imports)
|
||||||
|
|
||||||
|
_sym_db = _symbol_database.Default()
|
||||||
|
|
||||||
|
|
||||||
|
from . import emulated_bluetooth_packets_pb2 as emulated__bluetooth__packets__pb2
|
||||||
|
from . import emulated_bluetooth_device_pb2 as emulated__bluetooth__device__pb2
|
||||||
|
|
||||||
|
|
||||||
|
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18\x65mulated_bluetooth.proto\x12\x1b\x61ndroid.emulation.bluetooth\x1a emulated_bluetooth_packets.proto\x1a\x1f\x65mulated_bluetooth_device.proto\"\x19\n\x07RawData\x12\x0e\n\x06packet\x18\x01 \x01(\x0c\"a\n\x12RegistrationStatus\x12K\n\x12\x63\x61llback_device_id\x18\x01 \x01(\x0b\x32/.android.emulation.bluetooth.CallbackIdentifier2\xbb\x03\n\x18\x45mulatedBluetoothService\x12\x64\n\x12registerClassicPhy\x12$.android.emulation.bluetooth.RawData\x1a$.android.emulation.bluetooth.RawData(\x01\x30\x01\x12`\n\x0eregisterBlePhy\x12$.android.emulation.bluetooth.RawData\x1a$.android.emulation.bluetooth.RawData(\x01\x30\x01\x12g\n\x11registerHCIDevice\x12&.android.emulation.bluetooth.HCIPacket\x1a&.android.emulation.bluetooth.HCIPacket(\x01\x30\x01\x12n\n\x12registerGattDevice\x12\'.android.emulation.bluetooth.GattDevice\x1a/.android.emulation.bluetooth.RegistrationStatusB\"\n\x1e\x63om.android.emulator.bluetoothP\x01\x62\x06proto3')
|
||||||
|
|
||||||
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
|
||||||
|
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'emulated_bluetooth_pb2', globals())
|
||||||
|
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||||
|
|
||||||
|
DESCRIPTOR._options = None
|
||||||
|
DESCRIPTOR._serialized_options = b'\n\036com.android.emulator.bluetoothP\001'
|
||||||
|
_RAWDATA._serialized_start=124
|
||||||
|
_RAWDATA._serialized_end=149
|
||||||
|
_REGISTRATIONSTATUS._serialized_start=151
|
||||||
|
_REGISTRATIONSTATUS._serialized_end=248
|
||||||
|
_EMULATEDBLUETOOTHSERVICE._serialized_start=251
|
||||||
|
_EMULATEDBLUETOOTHSERVICE._serialized_end=694
|
||||||
|
# @@protoc_insertion_point(module_scope)
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
from . import emulated_bluetooth_packets_pb2 as _emulated_bluetooth_packets_pb2
|
||||||
|
from . import emulated_bluetooth_device_pb2 as _emulated_bluetooth_device_pb2
|
||||||
|
from google.protobuf import descriptor as _descriptor
|
||||||
|
from google.protobuf import message as _message
|
||||||
|
from typing import ClassVar as _ClassVar, Mapping as _Mapping, Optional as _Optional, Union as _Union
|
||||||
|
|
||||||
|
DESCRIPTOR: _descriptor.FileDescriptor
|
||||||
|
|
||||||
|
class RawData(_message.Message):
|
||||||
|
__slots__ = ["packet"]
|
||||||
|
PACKET_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
packet: bytes
|
||||||
|
def __init__(self, packet: _Optional[bytes] = ...) -> None: ...
|
||||||
|
|
||||||
|
class RegistrationStatus(_message.Message):
|
||||||
|
__slots__ = ["callback_device_id"]
|
||||||
|
CALLBACK_DEVICE_ID_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
callback_device_id: _emulated_bluetooth_device_pb2.CallbackIdentifier
|
||||||
|
def __init__(self, callback_device_id: _Optional[_Union[_emulated_bluetooth_device_pb2.CallbackIdentifier, _Mapping]] = ...) -> None: ...
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
||||||
|
"""Client and server classes corresponding to protobuf-defined services."""
|
||||||
|
import grpc
|
||||||
|
|
||||||
|
from . import emulated_bluetooth_device_pb2 as emulated__bluetooth__device__pb2
|
||||||
|
from . import emulated_bluetooth_packets_pb2 as emulated__bluetooth__packets__pb2
|
||||||
|
from . import emulated_bluetooth_pb2 as emulated__bluetooth__pb2
|
||||||
|
|
||||||
|
|
||||||
|
class EmulatedBluetoothServiceStub(object):
|
||||||
|
"""An Emulated Bluetooth Service exposes the emulated bluetooth chip from the
|
||||||
|
android emulator. It allows you to register emulated bluetooth devices and
|
||||||
|
control the packets that are exchanged between the device and the world.
|
||||||
|
|
||||||
|
This service enables you to establish a "virtual network" of emulated
|
||||||
|
bluetooth devices that can interact with each other.
|
||||||
|
|
||||||
|
Note: This is not yet finalized, it is likely that these definitions will
|
||||||
|
evolve.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, channel):
|
||||||
|
"""Constructor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
channel: A grpc.Channel.
|
||||||
|
"""
|
||||||
|
self.registerClassicPhy = channel.stream_stream(
|
||||||
|
'/android.emulation.bluetooth.EmulatedBluetoothService/registerClassicPhy',
|
||||||
|
request_serializer=emulated__bluetooth__pb2.RawData.SerializeToString,
|
||||||
|
response_deserializer=emulated__bluetooth__pb2.RawData.FromString,
|
||||||
|
)
|
||||||
|
self.registerBlePhy = channel.stream_stream(
|
||||||
|
'/android.emulation.bluetooth.EmulatedBluetoothService/registerBlePhy',
|
||||||
|
request_serializer=emulated__bluetooth__pb2.RawData.SerializeToString,
|
||||||
|
response_deserializer=emulated__bluetooth__pb2.RawData.FromString,
|
||||||
|
)
|
||||||
|
self.registerHCIDevice = channel.stream_stream(
|
||||||
|
'/android.emulation.bluetooth.EmulatedBluetoothService/registerHCIDevice',
|
||||||
|
request_serializer=emulated__bluetooth__packets__pb2.HCIPacket.SerializeToString,
|
||||||
|
response_deserializer=emulated__bluetooth__packets__pb2.HCIPacket.FromString,
|
||||||
|
)
|
||||||
|
self.registerGattDevice = channel.unary_unary(
|
||||||
|
'/android.emulation.bluetooth.EmulatedBluetoothService/registerGattDevice',
|
||||||
|
request_serializer=emulated__bluetooth__device__pb2.GattDevice.SerializeToString,
|
||||||
|
response_deserializer=emulated__bluetooth__pb2.RegistrationStatus.FromString,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class EmulatedBluetoothServiceServicer(object):
|
||||||
|
"""An Emulated Bluetooth Service exposes the emulated bluetooth chip from the
|
||||||
|
android emulator. It allows you to register emulated bluetooth devices and
|
||||||
|
control the packets that are exchanged between the device and the world.
|
||||||
|
|
||||||
|
This service enables you to establish a "virtual network" of emulated
|
||||||
|
bluetooth devices that can interact with each other.
|
||||||
|
|
||||||
|
Note: This is not yet finalized, it is likely that these definitions will
|
||||||
|
evolve.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def registerClassicPhy(self, request_iterator, context):
|
||||||
|
"""Connect device to link layer. This will establish a direct connection
|
||||||
|
to the emulated bluetooth chip and configure the following:
|
||||||
|
|
||||||
|
- Each connection creates a new device and attaches it to the link layer
|
||||||
|
- Link Layer packets are transmitted directly to the phy
|
||||||
|
|
||||||
|
This should be used for classic connections.
|
||||||
|
|
||||||
|
This is used to directly connect various android emulators together.
|
||||||
|
For example a wear device can connect to an android emulator through
|
||||||
|
this.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def registerBlePhy(self, request_iterator, context):
|
||||||
|
"""Connect device to link layer. This will establish a direct connection
|
||||||
|
to root canal and execute the following:
|
||||||
|
|
||||||
|
- Each connection creates a new device and attaches it to the link layer
|
||||||
|
- Link Layer packets are transmitted directly to the phy
|
||||||
|
|
||||||
|
This should be used for BLE connections.
|
||||||
|
|
||||||
|
This is used to directly connect various android emulators together.
|
||||||
|
For example a wear device can connect to an android emulator through
|
||||||
|
this.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def registerHCIDevice(self, request_iterator, context):
|
||||||
|
"""Connect the device to the emulated bluetooth chip. The device will
|
||||||
|
participate in the network. You can configure the chip to scan, advertise
|
||||||
|
and setup connections with other devices that are connected to the
|
||||||
|
network.
|
||||||
|
|
||||||
|
This is usually used when you have a need for an emulated bluetooth chip
|
||||||
|
and have a bluetooth stack that can interpret and handle the packets
|
||||||
|
correctly.
|
||||||
|
|
||||||
|
For example the apache nimble stack can use this endpoint as the
|
||||||
|
transport layer.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def registerGattDevice(self, request, context):
|
||||||
|
"""Registers an emulated bluetooth device. The emulator will reach out to
|
||||||
|
the emulated device to read/write and subscribe to properties.
|
||||||
|
|
||||||
|
The following gRPC error codes can be returned:
|
||||||
|
- FAILED_PRECONDITION (code 9):
|
||||||
|
- root canal is not available on this device
|
||||||
|
- unable to reach the endpoint for the GattDevice
|
||||||
|
- INTERNAL (code 13) if there was an internal emulator failure.
|
||||||
|
|
||||||
|
The device will not be discoverable in case of an error.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
|
||||||
|
def add_EmulatedBluetoothServiceServicer_to_server(servicer, server):
|
||||||
|
rpc_method_handlers = {
|
||||||
|
'registerClassicPhy': grpc.stream_stream_rpc_method_handler(
|
||||||
|
servicer.registerClassicPhy,
|
||||||
|
request_deserializer=emulated__bluetooth__pb2.RawData.FromString,
|
||||||
|
response_serializer=emulated__bluetooth__pb2.RawData.SerializeToString,
|
||||||
|
),
|
||||||
|
'registerBlePhy': grpc.stream_stream_rpc_method_handler(
|
||||||
|
servicer.registerBlePhy,
|
||||||
|
request_deserializer=emulated__bluetooth__pb2.RawData.FromString,
|
||||||
|
response_serializer=emulated__bluetooth__pb2.RawData.SerializeToString,
|
||||||
|
),
|
||||||
|
'registerHCIDevice': grpc.stream_stream_rpc_method_handler(
|
||||||
|
servicer.registerHCIDevice,
|
||||||
|
request_deserializer=emulated__bluetooth__packets__pb2.HCIPacket.FromString,
|
||||||
|
response_serializer=emulated__bluetooth__packets__pb2.HCIPacket.SerializeToString,
|
||||||
|
),
|
||||||
|
'registerGattDevice': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.registerGattDevice,
|
||||||
|
request_deserializer=emulated__bluetooth__device__pb2.GattDevice.FromString,
|
||||||
|
response_serializer=emulated__bluetooth__pb2.RegistrationStatus.SerializeToString,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
generic_handler = grpc.method_handlers_generic_handler(
|
||||||
|
'android.emulation.bluetooth.EmulatedBluetoothService', rpc_method_handlers)
|
||||||
|
server.add_generic_rpc_handlers((generic_handler,))
|
||||||
|
|
||||||
|
|
||||||
|
# This class is part of an EXPERIMENTAL API.
|
||||||
|
class EmulatedBluetoothService(object):
|
||||||
|
"""An Emulated Bluetooth Service exposes the emulated bluetooth chip from the
|
||||||
|
android emulator. It allows you to register emulated bluetooth devices and
|
||||||
|
control the packets that are exchanged between the device and the world.
|
||||||
|
|
||||||
|
This service enables you to establish a "virtual network" of emulated
|
||||||
|
bluetooth devices that can interact with each other.
|
||||||
|
|
||||||
|
Note: This is not yet finalized, it is likely that these definitions will
|
||||||
|
evolve.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def registerClassicPhy(request_iterator,
|
||||||
|
target,
|
||||||
|
options=(),
|
||||||
|
channel_credentials=None,
|
||||||
|
call_credentials=None,
|
||||||
|
insecure=False,
|
||||||
|
compression=None,
|
||||||
|
wait_for_ready=None,
|
||||||
|
timeout=None,
|
||||||
|
metadata=None):
|
||||||
|
return grpc.experimental.stream_stream(request_iterator, target, '/android.emulation.bluetooth.EmulatedBluetoothService/registerClassicPhy',
|
||||||
|
emulated__bluetooth__pb2.RawData.SerializeToString,
|
||||||
|
emulated__bluetooth__pb2.RawData.FromString,
|
||||||
|
options, channel_credentials,
|
||||||
|
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def registerBlePhy(request_iterator,
|
||||||
|
target,
|
||||||
|
options=(),
|
||||||
|
channel_credentials=None,
|
||||||
|
call_credentials=None,
|
||||||
|
insecure=False,
|
||||||
|
compression=None,
|
||||||
|
wait_for_ready=None,
|
||||||
|
timeout=None,
|
||||||
|
metadata=None):
|
||||||
|
return grpc.experimental.stream_stream(request_iterator, target, '/android.emulation.bluetooth.EmulatedBluetoothService/registerBlePhy',
|
||||||
|
emulated__bluetooth__pb2.RawData.SerializeToString,
|
||||||
|
emulated__bluetooth__pb2.RawData.FromString,
|
||||||
|
options, channel_credentials,
|
||||||
|
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def registerHCIDevice(request_iterator,
|
||||||
|
target,
|
||||||
|
options=(),
|
||||||
|
channel_credentials=None,
|
||||||
|
call_credentials=None,
|
||||||
|
insecure=False,
|
||||||
|
compression=None,
|
||||||
|
wait_for_ready=None,
|
||||||
|
timeout=None,
|
||||||
|
metadata=None):
|
||||||
|
return grpc.experimental.stream_stream(request_iterator, target, '/android.emulation.bluetooth.EmulatedBluetoothService/registerHCIDevice',
|
||||||
|
emulated__bluetooth__packets__pb2.HCIPacket.SerializeToString,
|
||||||
|
emulated__bluetooth__packets__pb2.HCIPacket.FromString,
|
||||||
|
options, channel_credentials,
|
||||||
|
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def registerGattDevice(request,
|
||||||
|
target,
|
||||||
|
options=(),
|
||||||
|
channel_credentials=None,
|
||||||
|
call_credentials=None,
|
||||||
|
insecure=False,
|
||||||
|
compression=None,
|
||||||
|
wait_for_ready=None,
|
||||||
|
timeout=None,
|
||||||
|
metadata=None):
|
||||||
|
return grpc.experimental.unary_unary(request, target, '/android.emulation.bluetooth.EmulatedBluetoothService/registerGattDevice',
|
||||||
|
emulated__bluetooth__device__pb2.GattDevice.SerializeToString,
|
||||||
|
emulated__bluetooth__pb2.RegistrationStatus.FromString,
|
||||||
|
options, channel_credentials,
|
||||||
|
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||||
|
# source: emulated_bluetooth_vhci.proto
|
||||||
|
"""Generated protocol buffer code."""
|
||||||
|
from google.protobuf.internal import builder as _builder
|
||||||
|
from google.protobuf import descriptor as _descriptor
|
||||||
|
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||||
|
from google.protobuf import symbol_database as _symbol_database
|
||||||
|
# @@protoc_insertion_point(imports)
|
||||||
|
|
||||||
|
_sym_db = _symbol_database.Default()
|
||||||
|
|
||||||
|
|
||||||
|
from . import emulated_bluetooth_packets_pb2 as emulated__bluetooth__packets__pb2
|
||||||
|
|
||||||
|
|
||||||
|
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1d\x65mulated_bluetooth_vhci.proto\x12\x1b\x61ndroid.emulation.bluetooth\x1a emulated_bluetooth_packets.proto2y\n\x15VhciForwardingService\x12`\n\nattachVhci\x12&.android.emulation.bluetooth.HCIPacket\x1a&.android.emulation.bluetooth.HCIPacket(\x01\x30\x01\x42J\n\x1f\x63om.android.emulation.bluetoothP\x01\xf8\x01\x01\xa2\x02\x03\x41\x45\x42\xaa\x02\x1b\x41ndroid.Emulation.Bluetoothb\x06proto3')
|
||||||
|
|
||||||
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
|
||||||
|
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'emulated_bluetooth_vhci_pb2', globals())
|
||||||
|
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||||
|
|
||||||
|
DESCRIPTOR._options = None
|
||||||
|
DESCRIPTOR._serialized_options = b'\n\037com.android.emulation.bluetoothP\001\370\001\001\242\002\003AEB\252\002\033Android.Emulation.Bluetooth'
|
||||||
|
_VHCIFORWARDINGSERVICE._serialized_start=96
|
||||||
|
_VHCIFORWARDINGSERVICE._serialized_end=217
|
||||||
|
# @@protoc_insertion_point(module_scope)
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import emulated_bluetooth_packets_pb2 as _emulated_bluetooth_packets_pb2
|
||||||
|
from google.protobuf import descriptor as _descriptor
|
||||||
|
from typing import ClassVar as _ClassVar
|
||||||
|
|
||||||
|
DESCRIPTOR: _descriptor.FileDescriptor
|
||||||
+23
-50
@@ -1,17 +1,3 @@
|
|||||||
# 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.
|
|
||||||
|
|
||||||
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
||||||
"""Client and server classes corresponding to protobuf-defined services."""
|
"""Client and server classes corresponding to protobuf-defined services."""
|
||||||
import grpc
|
import grpc
|
||||||
@@ -35,10 +21,10 @@ class VhciForwardingServiceStub(object):
|
|||||||
channel: A grpc.Channel.
|
channel: A grpc.Channel.
|
||||||
"""
|
"""
|
||||||
self.attachVhci = channel.stream_stream(
|
self.attachVhci = channel.stream_stream(
|
||||||
'/android.emulation.bluetooth.VhciForwardingService/attachVhci',
|
'/android.emulation.bluetooth.VhciForwardingService/attachVhci',
|
||||||
request_serializer=emulated__bluetooth__packets__pb2.HCIPacket.SerializeToString,
|
request_serializer=emulated__bluetooth__packets__pb2.HCIPacket.SerializeToString,
|
||||||
response_deserializer=emulated__bluetooth__packets__pb2.HCIPacket.FromString,
|
response_deserializer=emulated__bluetooth__packets__pb2.HCIPacket.FromString,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class VhciForwardingServiceServicer(object):
|
class VhciForwardingServiceServicer(object):
|
||||||
@@ -75,19 +61,18 @@ class VhciForwardingServiceServicer(object):
|
|||||||
|
|
||||||
def add_VhciForwardingServiceServicer_to_server(servicer, server):
|
def add_VhciForwardingServiceServicer_to_server(servicer, server):
|
||||||
rpc_method_handlers = {
|
rpc_method_handlers = {
|
||||||
'attachVhci': grpc.stream_stream_rpc_method_handler(
|
'attachVhci': grpc.stream_stream_rpc_method_handler(
|
||||||
servicer.attachVhci,
|
servicer.attachVhci,
|
||||||
request_deserializer=emulated__bluetooth__packets__pb2.HCIPacket.FromString,
|
request_deserializer=emulated__bluetooth__packets__pb2.HCIPacket.FromString,
|
||||||
response_serializer=emulated__bluetooth__packets__pb2.HCIPacket.SerializeToString,
|
response_serializer=emulated__bluetooth__packets__pb2.HCIPacket.SerializeToString,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
generic_handler = grpc.method_handlers_generic_handler(
|
generic_handler = grpc.method_handlers_generic_handler(
|
||||||
'android.emulation.bluetooth.VhciForwardingService', rpc_method_handlers
|
'android.emulation.bluetooth.VhciForwardingService', rpc_method_handlers)
|
||||||
)
|
|
||||||
server.add_generic_rpc_handlers((generic_handler,))
|
server.add_generic_rpc_handlers((generic_handler,))
|
||||||
|
|
||||||
|
|
||||||
# This class is part of an EXPERIMENTAL API.
|
# This class is part of an EXPERIMENTAL API.
|
||||||
class VhciForwardingService(object):
|
class VhciForwardingService(object):
|
||||||
"""This is a service which allows you to directly intercept the VHCI packets
|
"""This is a service which allows you to directly intercept the VHCI packets
|
||||||
that are coming and going to the device before they are delivered to
|
that are coming and going to the device before they are delivered to
|
||||||
@@ -98,30 +83,18 @@ class VhciForwardingService(object):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def attachVhci(
|
def attachVhci(request_iterator,
|
||||||
request_iterator,
|
|
||||||
target,
|
|
||||||
options=(),
|
|
||||||
channel_credentials=None,
|
|
||||||
call_credentials=None,
|
|
||||||
insecure=False,
|
|
||||||
compression=None,
|
|
||||||
wait_for_ready=None,
|
|
||||||
timeout=None,
|
|
||||||
metadata=None,
|
|
||||||
):
|
|
||||||
return grpc.experimental.stream_stream(
|
|
||||||
request_iterator,
|
|
||||||
target,
|
target,
|
||||||
'/android.emulation.bluetooth.VhciForwardingService/attachVhci',
|
options=(),
|
||||||
|
channel_credentials=None,
|
||||||
|
call_credentials=None,
|
||||||
|
insecure=False,
|
||||||
|
compression=None,
|
||||||
|
wait_for_ready=None,
|
||||||
|
timeout=None,
|
||||||
|
metadata=None):
|
||||||
|
return grpc.experimental.stream_stream(request_iterator, target, '/android.emulation.bluetooth.VhciForwardingService/attachVhci',
|
||||||
emulated__bluetooth__packets__pb2.HCIPacket.SerializeToString,
|
emulated__bluetooth__packets__pb2.HCIPacket.SerializeToString,
|
||||||
emulated__bluetooth__packets__pb2.HCIPacket.FromString,
|
emulated__bluetooth__packets__pb2.HCIPacket.FromString,
|
||||||
options,
|
options, channel_credentials,
|
||||||
channel_credentials,
|
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
|
||||||
insecure,
|
|
||||||
call_credentials,
|
|
||||||
compression,
|
|
||||||
wait_for_ready,
|
|
||||||
timeout,
|
|
||||||
metadata,
|
|
||||||
)
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||||
|
# source: grpc_endpoint_description.proto
|
||||||
|
"""Generated protocol buffer code."""
|
||||||
|
from google.protobuf.internal import builder as _builder
|
||||||
|
from google.protobuf import descriptor as _descriptor
|
||||||
|
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||||
|
from google.protobuf import symbol_database as _symbol_database
|
||||||
|
# @@protoc_insertion_point(imports)
|
||||||
|
|
||||||
|
_sym_db = _symbol_database.Default()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1fgrpc_endpoint_description.proto\x12\x18\x61ndroid.emulation.remote\"V\n\x0b\x43redentials\x12\x16\n\x0epem_root_certs\x18\x01 \x01(\t\x12\x17\n\x0fpem_private_key\x18\x02 \x01(\t\x12\x16\n\x0epem_cert_chain\x18\x03 \x01(\t\"$\n\x06Header\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"\x96\x01\n\x08\x45ndpoint\x12\x0e\n\x06target\x18\x01 \x01(\t\x12>\n\x0ftls_credentials\x18\x02 \x01(\x0b\x32%.android.emulation.remote.Credentials\x12:\n\x10required_headers\x18\x03 \x03(\x0b\x32 .android.emulation.remote.HeaderB \n\x1c\x63om.android.emulation.remoteP\x01\x62\x06proto3')
|
||||||
|
|
||||||
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
|
||||||
|
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'grpc_endpoint_description_pb2', globals())
|
||||||
|
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||||
|
|
||||||
|
DESCRIPTOR._options = None
|
||||||
|
DESCRIPTOR._serialized_options = b'\n\034com.android.emulation.remoteP\001'
|
||||||
|
_CREDENTIALS._serialized_start=61
|
||||||
|
_CREDENTIALS._serialized_end=147
|
||||||
|
_HEADER._serialized_start=149
|
||||||
|
_HEADER._serialized_end=185
|
||||||
|
_ENDPOINT._serialized_start=188
|
||||||
|
_ENDPOINT._serialized_end=338
|
||||||
|
# @@protoc_insertion_point(module_scope)
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
from google.protobuf.internal import containers as _containers
|
||||||
|
from google.protobuf import descriptor as _descriptor
|
||||||
|
from google.protobuf import message as _message
|
||||||
|
from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union
|
||||||
|
|
||||||
|
DESCRIPTOR: _descriptor.FileDescriptor
|
||||||
|
|
||||||
|
class Credentials(_message.Message):
|
||||||
|
__slots__ = ["pem_cert_chain", "pem_private_key", "pem_root_certs"]
|
||||||
|
PEM_CERT_CHAIN_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
PEM_PRIVATE_KEY_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
PEM_ROOT_CERTS_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
pem_cert_chain: str
|
||||||
|
pem_private_key: str
|
||||||
|
pem_root_certs: str
|
||||||
|
def __init__(self, pem_root_certs: _Optional[str] = ..., pem_private_key: _Optional[str] = ..., pem_cert_chain: _Optional[str] = ...) -> None: ...
|
||||||
|
|
||||||
|
class Endpoint(_message.Message):
|
||||||
|
__slots__ = ["required_headers", "target", "tls_credentials"]
|
||||||
|
REQUIRED_HEADERS_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
TARGET_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
TLS_CREDENTIALS_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
required_headers: _containers.RepeatedCompositeFieldContainer[Header]
|
||||||
|
target: str
|
||||||
|
tls_credentials: Credentials
|
||||||
|
def __init__(self, target: _Optional[str] = ..., tls_credentials: _Optional[_Union[Credentials, _Mapping]] = ..., required_headers: _Optional[_Iterable[_Union[Header, _Mapping]]] = ...) -> None: ...
|
||||||
|
|
||||||
|
class Header(_message.Message):
|
||||||
|
__slots__ = ["key", "value"]
|
||||||
|
KEY_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
VALUE_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
key: str
|
||||||
|
value: str
|
||||||
|
def __init__(self, key: _Optional[str] = ..., value: _Optional[str] = ...) -> None: ...
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
||||||
|
"""Client and server classes corresponding to protobuf-defined services."""
|
||||||
|
import grpc
|
||||||
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||||
|
# source: hci_packet.proto
|
||||||
|
"""Generated protocol buffer code."""
|
||||||
|
from google.protobuf.internal import builder as _builder
|
||||||
|
from google.protobuf import descriptor as _descriptor
|
||||||
|
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||||
|
from google.protobuf import symbol_database as _symbol_database
|
||||||
|
# @@protoc_insertion_point(imports)
|
||||||
|
|
||||||
|
_sym_db = _symbol_database.Default()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x10hci_packet.proto\x12\rnetsim.packet\"\xb2\x01\n\tHCIPacket\x12\x38\n\x0bpacket_type\x18\x01 \x01(\x0e\x32#.netsim.packet.HCIPacket.PacketType\x12\x0e\n\x06packet\x18\x02 \x01(\x0c\"[\n\nPacketType\x12\x1a\n\x16HCI_PACKET_UNSPECIFIED\x10\x00\x12\x0b\n\x07\x43OMMAND\x10\x01\x12\x07\n\x03\x41\x43L\x10\x02\x12\x07\n\x03SCO\x10\x03\x12\t\n\x05\x45VENT\x10\x04\x12\x07\n\x03ISO\x10\x05\x42J\n\x1f\x63om.android.emulation.bluetoothP\x01\xf8\x01\x01\xa2\x02\x03\x41\x45\x42\xaa\x02\x1b\x41ndroid.Emulation.Bluetoothb\x06proto3')
|
||||||
|
|
||||||
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
|
||||||
|
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'hci_packet_pb2', globals())
|
||||||
|
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||||
|
|
||||||
|
DESCRIPTOR._options = None
|
||||||
|
DESCRIPTOR._serialized_options = b'\n\037com.android.emulation.bluetoothP\001\370\001\001\242\002\003AEB\252\002\033Android.Emulation.Bluetooth'
|
||||||
|
_HCIPACKET._serialized_start=36
|
||||||
|
_HCIPACKET._serialized_end=214
|
||||||
|
_HCIPACKET_PACKETTYPE._serialized_start=123
|
||||||
|
_HCIPACKET_PACKETTYPE._serialized_end=214
|
||||||
|
# @@protoc_insertion_point(module_scope)
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
|
||||||
|
from google.protobuf import descriptor as _descriptor
|
||||||
|
from google.protobuf import message as _message
|
||||||
|
from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union
|
||||||
|
|
||||||
|
DESCRIPTOR: _descriptor.FileDescriptor
|
||||||
|
|
||||||
|
class HCIPacket(_message.Message):
|
||||||
|
__slots__ = ["packet", "packet_type"]
|
||||||
|
class PacketType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||||
|
__slots__ = []
|
||||||
|
ACL: HCIPacket.PacketType
|
||||||
|
COMMAND: HCIPacket.PacketType
|
||||||
|
EVENT: HCIPacket.PacketType
|
||||||
|
HCI_PACKET_UNSPECIFIED: HCIPacket.PacketType
|
||||||
|
ISO: HCIPacket.PacketType
|
||||||
|
PACKET_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
PACKET_TYPE_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
SCO: HCIPacket.PacketType
|
||||||
|
packet: bytes
|
||||||
|
packet_type: HCIPacket.PacketType
|
||||||
|
def __init__(self, packet_type: _Optional[_Union[HCIPacket.PacketType, str]] = ..., packet: _Optional[bytes] = ...) -> None: ...
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
||||||
|
"""Client and server classes corresponding to protobuf-defined services."""
|
||||||
|
import grpc
|
||||||
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||||
|
# source: packet_streamer.proto
|
||||||
|
"""Generated protocol buffer code."""
|
||||||
|
from google.protobuf.internal import builder as _builder
|
||||||
|
from google.protobuf import descriptor as _descriptor
|
||||||
|
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||||
|
from google.protobuf import symbol_database as _symbol_database
|
||||||
|
# @@protoc_insertion_point(imports)
|
||||||
|
|
||||||
|
_sym_db = _symbol_database.Default()
|
||||||
|
|
||||||
|
|
||||||
|
from . import hci_packet_pb2 as hci__packet__pb2
|
||||||
|
from . import startup_pb2 as startup__pb2
|
||||||
|
|
||||||
|
|
||||||
|
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15packet_streamer.proto\x12\rnetsim.packet\x1a\x10hci_packet.proto\x1a\rstartup.proto\"\x93\x01\n\rPacketRequest\x12\x30\n\x0cinitial_info\x18\x01 \x01(\x0b\x32\x18.netsim.startup.ChipInfoH\x00\x12.\n\nhci_packet\x18\x02 \x01(\x0b\x32\x18.netsim.packet.HCIPacketH\x00\x12\x10\n\x06packet\x18\x03 \x01(\x0cH\x00\x42\x0e\n\x0crequest_type\"t\n\x0ePacketResponse\x12\x0f\n\x05\x65rror\x18\x01 \x01(\tH\x00\x12.\n\nhci_packet\x18\x02 \x01(\x0b\x32\x18.netsim.packet.HCIPacketH\x00\x12\x10\n\x06packet\x18\x03 \x01(\x0cH\x00\x42\x0f\n\rresponse_type2b\n\x0ePacketStreamer\x12P\n\rStreamPackets\x12\x1c.netsim.packet.PacketRequest\x1a\x1d.netsim.packet.PacketResponse(\x01\x30\x01\x62\x06proto3')
|
||||||
|
|
||||||
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
|
||||||
|
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'packet_streamer_pb2', globals())
|
||||||
|
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||||
|
|
||||||
|
DESCRIPTOR._options = None
|
||||||
|
_PACKETREQUEST._serialized_start=74
|
||||||
|
_PACKETREQUEST._serialized_end=221
|
||||||
|
_PACKETRESPONSE._serialized_start=223
|
||||||
|
_PACKETRESPONSE._serialized_end=339
|
||||||
|
_PACKETSTREAMER._serialized_start=341
|
||||||
|
_PACKETSTREAMER._serialized_end=439
|
||||||
|
# @@protoc_insertion_point(module_scope)
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
from . import hci_packet_pb2 as _hci_packet_pb2
|
||||||
|
from . import startup_pb2 as _startup_pb2
|
||||||
|
from google.protobuf import descriptor as _descriptor
|
||||||
|
from google.protobuf import message as _message
|
||||||
|
from typing import ClassVar as _ClassVar, Mapping as _Mapping, Optional as _Optional, Union as _Union
|
||||||
|
|
||||||
|
DESCRIPTOR: _descriptor.FileDescriptor
|
||||||
|
|
||||||
|
class PacketRequest(_message.Message):
|
||||||
|
__slots__ = ["hci_packet", "initial_info", "packet"]
|
||||||
|
HCI_PACKET_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
INITIAL_INFO_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
PACKET_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
hci_packet: _hci_packet_pb2.HCIPacket
|
||||||
|
initial_info: _startup_pb2.ChipInfo
|
||||||
|
packet: bytes
|
||||||
|
def __init__(self, initial_info: _Optional[_Union[_startup_pb2.ChipInfo, _Mapping]] = ..., hci_packet: _Optional[_Union[_hci_packet_pb2.HCIPacket, _Mapping]] = ..., packet: _Optional[bytes] = ...) -> None: ...
|
||||||
|
|
||||||
|
class PacketResponse(_message.Message):
|
||||||
|
__slots__ = ["error", "hci_packet", "packet"]
|
||||||
|
ERROR_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
HCI_PACKET_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
PACKET_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
error: str
|
||||||
|
hci_packet: _hci_packet_pb2.HCIPacket
|
||||||
|
packet: bytes
|
||||||
|
def __init__(self, error: _Optional[str] = ..., hci_packet: _Optional[_Union[_hci_packet_pb2.HCIPacket, _Mapping]] = ..., packet: _Optional[bytes] = ...) -> None: ...
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
||||||
|
"""Client and server classes corresponding to protobuf-defined services."""
|
||||||
|
import grpc
|
||||||
|
|
||||||
|
from . import packet_streamer_pb2 as packet__streamer__pb2
|
||||||
|
|
||||||
|
|
||||||
|
class PacketStreamerStub(object):
|
||||||
|
"""*
|
||||||
|
This is the packet service for the network simulator.
|
||||||
|
|
||||||
|
Android Virtual Devices (AVDs) and accessory devices use this service to
|
||||||
|
connect to the network simulator and pass packets back and forth.
|
||||||
|
|
||||||
|
AVDs running in a guest VM are built with virtual controllers for each radio
|
||||||
|
chip. These controllers route chip requests to host emulators (qemu and
|
||||||
|
crosvm) using virtio and from there they are forwarded to this gRpc service.
|
||||||
|
|
||||||
|
This setup provides a transparent radio environment across AVDs and
|
||||||
|
accessories because the network simulator contains libraries to emulate
|
||||||
|
Bluetooth, 80211MAC, UWB, and Rtt chips.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, channel):
|
||||||
|
"""Constructor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
channel: A grpc.Channel.
|
||||||
|
"""
|
||||||
|
self.StreamPackets = channel.stream_stream(
|
||||||
|
'/netsim.packet.PacketStreamer/StreamPackets',
|
||||||
|
request_serializer=packet__streamer__pb2.PacketRequest.SerializeToString,
|
||||||
|
response_deserializer=packet__streamer__pb2.PacketResponse.FromString,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PacketStreamerServicer(object):
|
||||||
|
"""*
|
||||||
|
This is the packet service for the network simulator.
|
||||||
|
|
||||||
|
Android Virtual Devices (AVDs) and accessory devices use this service to
|
||||||
|
connect to the network simulator and pass packets back and forth.
|
||||||
|
|
||||||
|
AVDs running in a guest VM are built with virtual controllers for each radio
|
||||||
|
chip. These controllers route chip requests to host emulators (qemu and
|
||||||
|
crosvm) using virtio and from there they are forwarded to this gRpc service.
|
||||||
|
|
||||||
|
This setup provides a transparent radio environment across AVDs and
|
||||||
|
accessories because the network simulator contains libraries to emulate
|
||||||
|
Bluetooth, 80211MAC, UWB, and Rtt chips.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def StreamPackets(self, request_iterator, context):
|
||||||
|
"""Attach a virtual radio controller to the network simulation.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
|
||||||
|
def add_PacketStreamerServicer_to_server(servicer, server):
|
||||||
|
rpc_method_handlers = {
|
||||||
|
'StreamPackets': grpc.stream_stream_rpc_method_handler(
|
||||||
|
servicer.StreamPackets,
|
||||||
|
request_deserializer=packet__streamer__pb2.PacketRequest.FromString,
|
||||||
|
response_serializer=packet__streamer__pb2.PacketResponse.SerializeToString,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
generic_handler = grpc.method_handlers_generic_handler(
|
||||||
|
'netsim.packet.PacketStreamer', rpc_method_handlers)
|
||||||
|
server.add_generic_rpc_handlers((generic_handler,))
|
||||||
|
|
||||||
|
|
||||||
|
# This class is part of an EXPERIMENTAL API.
|
||||||
|
class PacketStreamer(object):
|
||||||
|
"""*
|
||||||
|
This is the packet service for the network simulator.
|
||||||
|
|
||||||
|
Android Virtual Devices (AVDs) and accessory devices use this service to
|
||||||
|
connect to the network simulator and pass packets back and forth.
|
||||||
|
|
||||||
|
AVDs running in a guest VM are built with virtual controllers for each radio
|
||||||
|
chip. These controllers route chip requests to host emulators (qemu and
|
||||||
|
crosvm) using virtio and from there they are forwarded to this gRpc service.
|
||||||
|
|
||||||
|
This setup provides a transparent radio environment across AVDs and
|
||||||
|
accessories because the network simulator contains libraries to emulate
|
||||||
|
Bluetooth, 80211MAC, UWB, and Rtt chips.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def StreamPackets(request_iterator,
|
||||||
|
target,
|
||||||
|
options=(),
|
||||||
|
channel_credentials=None,
|
||||||
|
call_credentials=None,
|
||||||
|
insecure=False,
|
||||||
|
compression=None,
|
||||||
|
wait_for_ready=None,
|
||||||
|
timeout=None,
|
||||||
|
metadata=None):
|
||||||
|
return grpc.experimental.stream_stream(request_iterator, target, '/netsim.packet.PacketStreamer/StreamPackets',
|
||||||
|
packet__streamer__pb2.PacketRequest.SerializeToString,
|
||||||
|
packet__streamer__pb2.PacketResponse.FromString,
|
||||||
|
options, channel_credentials,
|
||||||
|
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||||
|
# source: startup.proto
|
||||||
|
"""Generated protocol buffer code."""
|
||||||
|
from google.protobuf.internal import builder as _builder
|
||||||
|
from google.protobuf import descriptor as _descriptor
|
||||||
|
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||||
|
from google.protobuf import symbol_database as _symbol_database
|
||||||
|
# @@protoc_insertion_point(imports)
|
||||||
|
|
||||||
|
_sym_db = _symbol_database.Default()
|
||||||
|
|
||||||
|
|
||||||
|
from . import common_pb2 as common__pb2
|
||||||
|
|
||||||
|
|
||||||
|
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\rstartup.proto\x12\x0enetsim.startup\x1a\x0c\x63ommon.proto\"\x7f\n\x0bStartupInfo\x12\x33\n\x07\x64\x65vices\x18\x01 \x03(\x0b\x32\".netsim.startup.StartupInfo.Device\x1a;\n\x06\x44\x65vice\x12\x0c\n\x04name\x18\x01 \x01(\t\x12#\n\x05\x63hips\x18\x02 \x03(\x0b\x32\x14.netsim.startup.Chip\"<\n\x08\x43hipInfo\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\"\n\x04\x63hip\x18\x02 \x01(\x0b\x32\x14.netsim.startup.Chip\"\x96\x01\n\x04\x43hip\x12%\n\x04kind\x18\x01 \x01(\x0e\x32\x17.netsim.common.ChipKind\x12\n\n\x02id\x18\x02 \x01(\t\x12\x14\n\x0cmanufacturer\x18\x03 \x01(\t\x12\x14\n\x0cproduct_name\x18\x04 \x01(\t\x12\r\n\x05\x66\x64_in\x18\x05 \x01(\x05\x12\x0e\n\x06\x66\x64_out\x18\x06 \x01(\x05\x12\x10\n\x08loopback\x18\x07 \x01(\x08\x62\x06proto3')
|
||||||
|
|
||||||
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
|
||||||
|
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'startup_pb2', globals())
|
||||||
|
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||||
|
|
||||||
|
DESCRIPTOR._options = None
|
||||||
|
_STARTUPINFO._serialized_start=47
|
||||||
|
_STARTUPINFO._serialized_end=174
|
||||||
|
_STARTUPINFO_DEVICE._serialized_start=115
|
||||||
|
_STARTUPINFO_DEVICE._serialized_end=174
|
||||||
|
_CHIPINFO._serialized_start=176
|
||||||
|
_CHIPINFO._serialized_end=236
|
||||||
|
_CHIP._serialized_start=239
|
||||||
|
_CHIP._serialized_end=389
|
||||||
|
# @@protoc_insertion_point(module_scope)
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
from . import common_pb2 as _common_pb2
|
||||||
|
from google.protobuf.internal import containers as _containers
|
||||||
|
from google.protobuf import descriptor as _descriptor
|
||||||
|
from google.protobuf import message as _message
|
||||||
|
from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union
|
||||||
|
|
||||||
|
DESCRIPTOR: _descriptor.FileDescriptor
|
||||||
|
|
||||||
|
class Chip(_message.Message):
|
||||||
|
__slots__ = ["fd_in", "fd_out", "id", "kind", "loopback", "manufacturer", "product_name"]
|
||||||
|
FD_IN_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
FD_OUT_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
ID_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
KIND_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
LOOPBACK_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
MANUFACTURER_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
PRODUCT_NAME_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
fd_in: int
|
||||||
|
fd_out: int
|
||||||
|
id: str
|
||||||
|
kind: _common_pb2.ChipKind
|
||||||
|
loopback: bool
|
||||||
|
manufacturer: str
|
||||||
|
product_name: str
|
||||||
|
def __init__(self, kind: _Optional[_Union[_common_pb2.ChipKind, str]] = ..., id: _Optional[str] = ..., manufacturer: _Optional[str] = ..., product_name: _Optional[str] = ..., fd_in: _Optional[int] = ..., fd_out: _Optional[int] = ..., loopback: bool = ...) -> None: ...
|
||||||
|
|
||||||
|
class ChipInfo(_message.Message):
|
||||||
|
__slots__ = ["chip", "name"]
|
||||||
|
CHIP_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
NAME_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
chip: Chip
|
||||||
|
name: str
|
||||||
|
def __init__(self, name: _Optional[str] = ..., chip: _Optional[_Union[Chip, _Mapping]] = ...) -> None: ...
|
||||||
|
|
||||||
|
class StartupInfo(_message.Message):
|
||||||
|
__slots__ = ["devices"]
|
||||||
|
class Device(_message.Message):
|
||||||
|
__slots__ = ["chips", "name"]
|
||||||
|
CHIPS_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
NAME_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
chips: _containers.RepeatedCompositeFieldContainer[Chip]
|
||||||
|
name: str
|
||||||
|
def __init__(self, name: _Optional[str] = ..., chips: _Optional[_Iterable[_Union[Chip, _Mapping]]] = ...) -> None: ...
|
||||||
|
DEVICES_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
devices: _containers.RepeatedCompositeFieldContainer[StartupInfo.Device]
|
||||||
|
def __init__(self, devices: _Optional[_Iterable[_Union[StartupInfo.Device, _Mapping]]] = ...) -> None: ...
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
||||||
|
"""Client and server classes corresponding to protobuf-defined services."""
|
||||||
|
import grpc
|
||||||
|
|
||||||
@@ -9,19 +9,20 @@ The two main use cases are:
|
|||||||
|
|
||||||
* Connecting the Bumble host stack to the Android emulator's virtual controller.
|
* Connecting the Bumble host stack to the Android emulator's virtual controller.
|
||||||
* Using Bumble as an HCI bridge to connect the Android emulator to a physical
|
* Using Bumble as an HCI bridge to connect the Android emulator to a physical
|
||||||
Bluetooth controller, such as a USB dongle
|
Bluetooth controller, such as a USB dongle, or other HCI transport.
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
Bluetooth support in the Android emulator is a recent feature that may still
|
Bluetooth support in the Android emulator is a recent feature that may still
|
||||||
be evolving. The information contained here be somewhat out of sync with the
|
be evolving. The information contained here be somewhat out of sync with the
|
||||||
version of the emulator you are using.
|
version of the emulator you are using.
|
||||||
You will need version 31.3.8.0 or later.
|
You will need version 33.1.4.0 or later.
|
||||||
|
|
||||||
The Android emulator supports Bluetooth in two ways: either by exposing virtual
|
The Android emulator supports Bluetooth in two ways: either by exposing virtual
|
||||||
Bluetooth controllers to which you can connect a virtual Bluetooth host stack, or
|
Bluetooth controllers to which you can connect a virtual Bluetooth host stack, or
|
||||||
by exposing an way to connect your own virtual controller to the Android Bluetooth
|
by exposing a way to connect your own virtual controller to the Android Bluetooth
|
||||||
stack via a virtual HCI interface.
|
stack via a virtual HCI interface.
|
||||||
Both ways are controlled via gRPC requests to the Android emulator.
|
Both ways are controlled via gRPC requests to the Android emulator controller and/or
|
||||||
|
from the Android emulator.
|
||||||
|
|
||||||
## Launching the Emulator
|
## Launching the Emulator
|
||||||
|
|
||||||
@@ -33,48 +34,82 @@ the command line.
|
|||||||
For details on how to launch the Android emulator from the command line,
|
For details on how to launch the Android emulator from the command line,
|
||||||
visit [this Android Studio user guide page](https://developer.android.com/studio/run/emulator-commandline)
|
visit [this Android Studio user guide page](https://developer.android.com/studio/run/emulator-commandline)
|
||||||
|
|
||||||
The `-grpc <port>` command line option may be used to select a gRPC port other than the default.
|
The `-packet-streamer-endpoint <endpoint>` command line option may be used to enable
|
||||||
|
Bluetooth emulation and tell the emulator which virtual controller to connect to.
|
||||||
|
|
||||||
## Connecting to Root Canal
|
## Connecting to Netsim
|
||||||
|
|
||||||
The Android emulator's virtual Bluetooth controller is called **Root Canal**.
|
If the emulator doesn't have Bluetooth emulation enabled by default, use the
|
||||||
Multiple instances of Root Canal virtual controllers can be instantiated, they
|
`-packet-streamer-endpoint default` option to tell it to connect to Netsim.
|
||||||
communicate link layer packets between them, thus creating a virtual radio network.
|
If Netsim is not running, the emulator will start it automatically.
|
||||||
Configuring a Bumble Device instance to use Root Canal as a virtual controller
|
|
||||||
|
The Android emulator's virtual Bluetooth controller is called **Netsim**.
|
||||||
|
Netsim runs as a background process and allows multiple clients to connect to it,
|
||||||
|
each connecting to its own virtual controller instance hosted by Netsim. All the
|
||||||
|
clients connected to the same Netsim process can then "talk" to each other over a
|
||||||
|
virtual radio link layer.
|
||||||
|
Netsim supports other wireless protocols than Bluetooth, but the relevant part here
|
||||||
|
is Bluetooth. The virtual Bluetooth controller used by Netsim is sometimes referred to
|
||||||
|
as **Root Canal**.
|
||||||
|
|
||||||
|
Configuring a Bumble Device instance to use netsim as a virtual controller
|
||||||
allows that virtual device to communicate with the Android Bluetooth stack, and
|
allows that virtual device to communicate with the Android Bluetooth stack, and
|
||||||
through it with Android applications as well as system-managed profiles.
|
through it with Android applications as well as system-managed profiles.
|
||||||
To connect a Bumble host stack to a Root Canal virtual controller instance, use
|
To connect a Bumble host stack to a netsim virtual controller instance, use
|
||||||
the bumble `android-emulator` transport in `host` mode (the default).
|
the Bumble `android-netsim` transport in `host` mode (the default).
|
||||||
|
|
||||||
!!! example "Run the example GATT server connected to the emulator"
|
!!! example "Run the example GATT server connected to the emulator via Netsim"
|
||||||
``` shell
|
``` shell
|
||||||
$ python run_gatt_server.py device1.json android-emulator
|
$ python run_gatt_server.py device1.json android-netsim
|
||||||
```
|
```
|
||||||
|
|
||||||
|
By default, the Bumble `android-netsim` transport will try to automatically discover
|
||||||
|
the port number on which the netsim process is exposing its gRPC server interface. If
|
||||||
|
that discovery process fails, or if you want to specify the interface manually, you
|
||||||
|
can pass a `hostname` and `port` as parameters to the transport, as: `android-netsim:<host>:<port>`.
|
||||||
|
|
||||||
|
!!! example "Run the example GATT server connected to the emulator via Netsim on a localhost, port 8877"
|
||||||
|
``` shell
|
||||||
|
$ python run_gatt_server.py device1.json android-netsim:localhost:8877
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiple Instances
|
||||||
|
|
||||||
|
If you want to connect multiple Bumble devices to netsim, it may be useful to give each one
|
||||||
|
a netsim controller with a specific name. This can be done using the `name=<name>` transport option.
|
||||||
|
For example: `android-netsim:localhost:8877,name=bumble1`
|
||||||
|
|
||||||
## Connecting a Custom Virtual Controller
|
## Connecting a Custom Virtual Controller
|
||||||
|
|
||||||
This is an advanced use case, which may not be officially supported, but should work in recent
|
This is an advanced use case, which may not be officially supported, but should work in recent
|
||||||
versions of the emulator.
|
versions of the emulator.
|
||||||
You will likely need to start the emulator from the command line, in order to specify the `-forward-vhci` option (unless the emulator offers a way to control that feature from a user/ui menu).
|
|
||||||
|
|
||||||
!!! example "Launch the emulator with VHCI forwarding"
|
The first step is to run the Bumble HCI bridge, specifying netsim as the "host" end of the
|
||||||
In this example, we launch an emulator AVD named "Tiramisu"
|
bridge, and another controller (typically a USB Bluetooth dongle, but any other supported
|
||||||
```shell
|
transport can work as well) as the "controller" end of the bridge.
|
||||||
$ emulator -forward-vhci -avd Tiramisu
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! tip
|
To connect a virtual controller to the Android Bluetooth stack, use the bumble `android-netsim` transport in `controller` mode. For example, with port number 8877, the transport name would be: `android-netsim:_:8877,mode=controller`.
|
||||||
Attaching a virtual controller use the VHCI forwarder while the Android Bluetooth stack
|
|
||||||
is running isn't supported. So you need to disable Bluetooth in your running Android guest
|
|
||||||
before attaching the virtual controller, then re-enable it once attached.
|
|
||||||
|
|
||||||
To connect a virtual controller to the Android Bluetooth stack, use the bumble `android-emulator` transport in `controller` mode. For example, using the default gRPC port, the transport name would be: `android-emulator:mode=controller`.
|
|
||||||
|
|
||||||
!!! example "Connect the Android emulator to the first USB Bluetooth dongle, using the `hci_bridge` application"
|
!!! example "Connect the Android emulator to the first USB Bluetooth dongle, using the `hci_bridge` application"
|
||||||
```shell
|
```shell
|
||||||
$ bumble-hci-bridge android-emulator:mode=controller usb:0
|
$ bumble-hci-bridge android-netsim:_:8877,mode=controller usb:0
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Then, you can start the emulator and tell it to connect to this bridge, instead of netsim.
|
||||||
|
You will likely need to start the emulator from the command line, in order to specify the `-packet-streamer-endpoint <hostname>:<port>` option (unless the emulator offers a way to control that feature from a user/ui menu).
|
||||||
|
|
||||||
|
!!! example "Launch the emulator with a netsim replacement"
|
||||||
|
In this example, we launch an emulator AVD named "Tiramisu", with a Bumble HCI bridge running
|
||||||
|
on port 8877.
|
||||||
|
```shell
|
||||||
|
$ emulator -packet-streamer-endpoint localhost:8877 -avd Tiramisu
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! tip
|
||||||
|
Attaching a virtual controller while the Android Bluetooth stack is running may not be well supported. So you may need to disable Bluetooth in your running Android guest
|
||||||
|
before attaching the virtual controller, then re-enable it once attached.
|
||||||
|
|
||||||
|
|
||||||
## Other Tools
|
## Other Tools
|
||||||
|
|
||||||
The `show` application that's included with Bumble can be used to parse and pretty-print the HCI packets
|
The `show` application that's included with Bumble can be used to parse and pretty-print the HCI packets
|
||||||
|
|||||||
@@ -1,22 +1,41 @@
|
|||||||
ANDROID EMULATOR TRANSPORT
|
ANDROID EMULATOR TRANSPORT
|
||||||
==========================
|
==========================
|
||||||
|
|
||||||
The Android emulator transport either connects, as a host, to a "Root Canal" virtual controller
|
!!! warning
|
||||||
("host" mode), or attaches a virtual controller to the Android Bluetooth host stack ("controller" mode).
|
Bluetooth support in the Android emulator has recently changed. The older mode, using
|
||||||
|
the `android-emulator` transport name with Bumble, while still implemented, is now
|
||||||
|
obsolete, and may not be supported by recent versions of the emulator.
|
||||||
|
Use the `android-netsim` transport name instead.
|
||||||
|
|
||||||
|
|
||||||
|
The Android "netsim" transport either connects, as a host, to a **Netsim** virtual controller
|
||||||
|
("host" mode), or acts as a virtual controller itself ("controller" mode) accepting host
|
||||||
|
connections.
|
||||||
|
|
||||||
## Moniker
|
## Moniker
|
||||||
The moniker syntax for an Android Emulator transport is: `android-emulator:[mode=<host|controller>][<hostname>:<port>]`, where
|
The moniker syntax for an Android Emulator "netsim" transport is: `android-netsim:[<host>:<port>][<options>]`,
|
||||||
the `mode` parameter can specify running as a host or a controller, and `<hostname>:<port>` can specify a host name (or IP address) and TCP port number on which to reach the gRPC server for the emulator.
|
where `<options>` is a ','-separated list of `<name>=<value>` pairs`.
|
||||||
Both the `mode=<host|controller>` and `<hostname>:<port>` parameters are optional (so the moniker `android-emulator` by itself is a valid moniker, which will create a transport in `host` mode, connected to `localhost` on the default gRPC port for the emulator).
|
The `mode` parameter name can specify running as a host or a controller, and `<hostname>:<port>` can specify a host name (or IP address) and TCP port number on which to reach the gRPC server for the emulator (in "host" mode), or to accept gRPC connections (in "controller" mode).
|
||||||
|
Both the `mode=<host|controller>` and `<hostname>:<port>` parameters are optional (so the moniker `android-netsim` by itself is a valid moniker, which will create a transport in `host` mode, connected to `localhost` on the default gRPC port for the Netsim background process).
|
||||||
|
|
||||||
!!! example Example
|
!!! example Example
|
||||||
`android-emulator`
|
`android-netsim`
|
||||||
connect as a host to the emulator on localhost:8554
|
connect as a host to Netsim on the gRPC port discovered automatically.
|
||||||
|
|
||||||
!!! example Example
|
!!! example Example
|
||||||
`android-emulator:mode=controller`
|
`android-netsim:_:8555,mode=controller`
|
||||||
connect as a controller to the emulator on localhost:8554
|
Run as a controller, accepting gRPC connection on port 8555.
|
||||||
|
|
||||||
!!! example Example
|
!!! example Example
|
||||||
`android-emulator:localhost:8555`
|
`android-netsim:localhost:8555`
|
||||||
connect as a host to the emulator on localhost:8555
|
connect as a host to Netsim on localhost:8555
|
||||||
|
|
||||||
|
!!! example Example
|
||||||
|
`android-netsim:localhost:8555`
|
||||||
|
connect as a host to Netsim on localhost:8555
|
||||||
|
|
||||||
|
!!! example Example
|
||||||
|
`android-netsim:name=bumble1234`
|
||||||
|
connect as a host to Netsim on the discovered gRPC port, using `bumble1234` as the
|
||||||
|
controller instance name.
|
||||||
|
|
||||||
|
|||||||
@@ -16,5 +16,6 @@ Several types of transports are supported:
|
|||||||
* [PTY](pty.md): a PTY (pseudo terminal) is used to send/receive HCI packets. This is convenient to expose a virtual controller as if it were an HCI UART
|
* [PTY](pty.md): a PTY (pseudo terminal) is used to send/receive HCI packets. This is convenient to expose a virtual controller as if it were an HCI UART
|
||||||
* [VHCI](vhci.md): used to attach a virtual controller to a Bluetooth stack on platforms that support it.
|
* [VHCI](vhci.md): used to attach a virtual controller to a Bluetooth stack on platforms that support it.
|
||||||
* [HCI Socket](hci_socket.md): an HCI socket, on platforms that support it, to send/receive HCI packets to/from an HCI controller managed by the OS.
|
* [HCI Socket](hci_socket.md): an HCI socket, on platforms that support it, to send/receive HCI packets to/from an HCI controller managed by the OS.
|
||||||
* [Android Emulator](android_emulator.md): a gRPC connection to an Android emulator is used to setup either an HCI interface to the emulator's "Root Canal" virtual controller, or attach a virtual controller to the Android Bluetooth host stack.
|
* [Android Emulator](android_emulator.md): a gRPC connection to the Android emulator's "netsim"
|
||||||
|
virtual controller, or from the Android emulator, is used to setup either an HCI interface to the emulator's "netsim" virtual controller, or serve as a virtual controller for the Android Bluetooth host stack.
|
||||||
* [File](file.md): HCI packets are read/written to a file-like node in the filesystem.
|
* [File](file.md): HCI packets are read/written to a file-like node in the filesystem.
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Bumble Speaker",
|
"name": "Bumble Speaker",
|
||||||
|
"address": "F0:F1:F2:F3:F4:F5",
|
||||||
"class_of_device": 2360324,
|
"class_of_device": 2360324,
|
||||||
"keystore": "JsonKeyStore"
|
"keystore": "JsonKeyStore"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]),
|
||||||
)
|
)
|
||||||
|
|||||||
+12
-21
@@ -13,6 +13,9 @@ testpaths = [
|
|||||||
|
|
||||||
[tool.pylint.master]
|
[tool.pylint.master]
|
||||||
init-hook = 'import sys; sys.path.append(".")'
|
init-hook = 'import sys; sys.path.append(".")'
|
||||||
|
ignore-paths = [
|
||||||
|
'.*_pb2(_grpc)?.py'
|
||||||
|
]
|
||||||
|
|
||||||
[tool.pylint.messages_control]
|
[tool.pylint.messages_control]
|
||||||
max-line-length = "88"
|
max-line-length = "88"
|
||||||
@@ -37,44 +40,32 @@ disable = [
|
|||||||
"too-many-statements",
|
"too-many-statements",
|
||||||
]
|
]
|
||||||
|
|
||||||
ignore = [
|
|
||||||
"emulated_bluetooth_pb2.py",
|
|
||||||
"emulated_bluetooth_pb2_grpc.py",
|
|
||||||
"emulated_bluetooth_vhci_pb2_grpc.py",
|
|
||||||
"emulated_bluetooth_packets_pb2.py",
|
|
||||||
"emulated_bluetooth_vhci_pb2.py"
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.pylint.typecheck]
|
[tool.pylint.typecheck]
|
||||||
signature-mutators="AsyncRunner.run_in_task"
|
signature-mutators="AsyncRunner.run_in_task"
|
||||||
|
|
||||||
[tool.black]
|
[tool.black]
|
||||||
skip-string-normalization = true
|
skip-string-normalization = true
|
||||||
|
extend-exclude = '''
|
||||||
|
(
|
||||||
|
.*_pb2(_grpc)?.py # exclude autogenerated Protocol Buffer files anywhere in the project
|
||||||
|
)
|
||||||
|
'''
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
exclude = ['bumble/transport/grpc_protobuf']
|
||||||
|
|
||||||
[[tool.mypy.overrides]]
|
[[tool.mypy.overrides]]
|
||||||
module = "bumble.transport.emulated_bluetooth_pb2_grpc"
|
module = "bumble.transport.grpc_protobuf.*"
|
||||||
ignore_missing_imports = true
|
|
||||||
|
|
||||||
[[tool.mypy.overrides]]
|
|
||||||
module = "bumble.transport.emulated_bluetooth_packets_pb2"
|
|
||||||
ignore_errors = true
|
ignore_errors = true
|
||||||
|
|
||||||
[[tool.mypy.overrides]]
|
[[tool.mypy.overrides]]
|
||||||
module = "aioconsole.*"
|
module = "aioconsole.*"
|
||||||
ignore_missing_imports = true
|
ignore_missing_imports = true
|
||||||
|
|
||||||
[[tool.mypy.overrides]]
|
|
||||||
module = "colors.*"
|
|
||||||
ignore_missing_imports = true
|
|
||||||
|
|
||||||
[[tool.mypy.overrides]]
|
[[tool.mypy.overrides]]
|
||||||
module = "construct.*"
|
module = "construct.*"
|
||||||
ignore_missing_imports = true
|
ignore_missing_imports = true
|
||||||
|
|
||||||
[[tool.mypy.overrides]]
|
|
||||||
module = "emulated_bluetooth_packets_pb2.*"
|
|
||||||
ignore_missing_imports = true
|
|
||||||
|
|
||||||
[[tool.mypy.overrides]]
|
[[tool.mypy.overrides]]
|
||||||
module = "grpc.*"
|
module = "grpc.*"
|
||||||
ignore_missing_imports = true
|
ignore_missing_imports = true
|
||||||
|
|||||||
@@ -1,27 +1,16 @@
|
|||||||
# Invoke this script with an argument pointing to where the Android emulator .proto files are.
|
# Invoke this script with an argument pointing to where the Android emulator .proto files are
|
||||||
# The .proto files should be slightly modified from their original version (as distributed with
|
# (for example, ~/Library/Android/sdk/emulator/lib on a mac, or
|
||||||
# the Android emulator):
|
# $AOSP/external/qemu/android/android-grpc/python/aemu-grpc/src/aemu/proto from the AOSP sources)
|
||||||
# --> Remove unused types/methods from emulated_bluetooth.proto
|
PROTOC_OUT=bumble/transport/grpc_protobuf
|
||||||
|
|
||||||
PROTOC_OUT=bumble/transport
|
proto_files=(emulated_bluetooth.proto emulated_bluetooth_vhci.proto emulated_bluetooth_packets.proto emulated_bluetooth_device.proto grpc_endpoint_description.proto)
|
||||||
LICENSE_FILE_INPUT=bumble/transport/android_emulator.py
|
|
||||||
|
|
||||||
proto_files=(emulated_bluetooth.proto emulated_bluetooth_vhci.proto emulated_bluetooth_packets.proto)
|
|
||||||
for proto_file in "${proto_files[@]}"
|
for proto_file in "${proto_files[@]}"
|
||||||
do
|
do
|
||||||
python -m grpc_tools.protoc -I$1 --proto_path=bumble/transport --python_out=$PROTOC_OUT --pyi_out=$PROTOC_OUT --grpc_python_out=$PROTOC_OUT $1/$proto_file
|
python -m grpc_tools.protoc -I$1 --proto_path=bumble/transport --python_out=$PROTOC_OUT --pyi_out=$PROTOC_OUT --grpc_python_out=$PROTOC_OUT $1/$proto_file
|
||||||
done
|
done
|
||||||
|
|
||||||
python_files=(emulated_bluetooth_pb2.py emulated_bluetooth_pb2_grpc.py emulated_bluetooth_packets_pb2.py emulated_bluetooth_packets_pb2_grpc.py emulated_bluetooth_vhci_pb2_grpc.py emulated_bluetooth_vhci_pb2.py)
|
python_files=(emulated_bluetooth_pb2_grpc.py emulated_bluetooth_pb2.py emulated_bluetooth_packets_pb2.py emulated_bluetooth_vhci_pb2_grpc.py emulated_bluetooth_vhci_pb2.py emulated_bluetooth_device_pb2.py grpc_endpoint_description_pb2.py)
|
||||||
for python_file in "${python_files[@]}"
|
for python_file in "${python_files[@]}"
|
||||||
do
|
do
|
||||||
sed -i '' 's/^import .*_pb2 as/from . &/' $PROTOC_OUT/$python_file
|
sed -i 's/^import .*_pb2 as/from . \0/' $PROTOC_OUT/$python_file
|
||||||
done
|
|
||||||
|
|
||||||
stub_files=(emulated_bluetooth_pb2.pyi emulated_bluetooth_packets_pb2.pyi emulated_bluetooth_vhci_pb2.pyi)
|
|
||||||
for source_file in "${python_files[@]}" "${stub_files[@]}"
|
|
||||||
do
|
|
||||||
head -14 $LICENSE_FILE_INPUT > $PROTOC_OUT/${source_file}.lic
|
|
||||||
cat $PROTOC_OUT/$source_file >> $PROTOC_OUT/${source_file}.lic
|
|
||||||
mv $PROTOC_OUT/${source_file}.lic $PROTOC_OUT/$source_file
|
|
||||||
done
|
done
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Invoke this script with an argument pointing to where the AOSP `tools/netsim/src/proto` is
|
||||||
|
PROTOC_OUT=bumble/transport/grpc_protobuf
|
||||||
|
|
||||||
|
proto_files=(common.proto packet_streamer.proto hci_packet.proto startup.proto)
|
||||||
|
for proto_file in "${proto_files[@]}"
|
||||||
|
do
|
||||||
|
python -m grpc_tools.protoc -I$1 --proto_path=bumble/transport --python_out=$PROTOC_OUT --pyi_out=$PROTOC_OUT --grpc_python_out=$PROTOC_OUT $1/$proto_file
|
||||||
|
done
|
||||||
|
|
||||||
|
python_files=(packet_streamer_pb2_grpc.py packet_streamer_pb2.py hci_packet_pb2.py startup_pb2.py)
|
||||||
|
for python_file in "${python_files[@]}"
|
||||||
|
do
|
||||||
|
sed -i 's/^import .*_pb2 as/from . \0/' $PROTOC_OUT/$python_file
|
||||||
|
done
|
||||||
@@ -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 =
|
||||||
@@ -72,8 +74,9 @@ test =
|
|||||||
coverage >= 6.4
|
coverage >= 6.4
|
||||||
development =
|
development =
|
||||||
black == 22.10
|
black == 22.10
|
||||||
|
grpcio-tools >= 1.51.1
|
||||||
invoke >= 1.7.3
|
invoke >= 1.7.3
|
||||||
mypy == 1.1.1
|
mypy == 1.2.0
|
||||||
nox >= 2022
|
nox >= 2022
|
||||||
pylint == 2.15.8
|
pylint == 2.15.8
|
||||||
types-appdirs >= 1.4.3
|
types-appdirs >= 1.4.3
|
||||||
|
|||||||
+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
|
||||||
|
|||||||
+19
-1
@@ -15,7 +15,7 @@
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Imports
|
# Imports
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
from bumble.core import AdvertisingData, get_dict_key_by_value
|
from bumble.core import AdvertisingData, UUID, get_dict_key_by_value
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
def test_ad_data():
|
def test_ad_data():
|
||||||
@@ -49,6 +49,24 @@ def test_get_dict_key_by_value():
|
|||||||
assert get_dict_key_by_value(dictionary, 3) is None
|
assert get_dict_key_by_value(dictionary, 3) is None
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def test_uuid_to_hex_str() -> None:
|
||||||
|
assert UUID("b5ea").to_hex_str() == "B5EA"
|
||||||
|
assert UUID("df5ce654").to_hex_str() == "DF5CE654"
|
||||||
|
assert (
|
||||||
|
UUID("df5ce654-e059-11ed-b5ea-0242ac120002").to_hex_str()
|
||||||
|
== "DF5CE654E05911EDB5EA0242AC120002"
|
||||||
|
)
|
||||||
|
assert UUID("b5ea").to_hex_str('-') == "B5EA"
|
||||||
|
assert UUID("df5ce654").to_hex_str('-') == "DF5CE654"
|
||||||
|
assert (
|
||||||
|
UUID("df5ce654-e059-11ed-b5ea-0242ac120002").to_hex_str('-')
|
||||||
|
== "DF5CE654-E059-11ED-B5EA-0242AC120002"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
test_ad_data()
|
test_ad_data()
|
||||||
|
test_get_dict_key_by_value()
|
||||||
|
test_uuid_to_hex_str()
|
||||||
|
|||||||
+165
-32
@@ -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
|
||||||
@@ -114,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,
|
||||||
)
|
)
|
||||||
@@ -143,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]),
|
||||||
)
|
)
|
||||||
@@ -239,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]),
|
||||||
)
|
)
|
||||||
@@ -284,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,
|
||||||
)
|
)
|
||||||
@@ -420,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,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -437,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
|
||||||
@@ -500,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,
|
||||||
)
|
)
|
||||||
@@ -544,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]),
|
||||||
)
|
)
|
||||||
@@ -560,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]),
|
||||||
)
|
)
|
||||||
@@ -576,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]),
|
||||||
)
|
)
|
||||||
@@ -796,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"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -832,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]),
|
||||||
)
|
)
|
||||||
@@ -843,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)"""
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -871,21 +894,131 @@ def test_attribute_string_to_permissions():
|
|||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
def test_charracteristic_permissions():
|
def test_characteristic_permissions():
|
||||||
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,
|
||||||
'READABLE,WRITEABLE',
|
'READABLE,WRITEABLE',
|
||||||
)
|
)
|
||||||
assert characteristic.permissions == 3
|
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():
|
def test_descriptor_permissions():
|
||||||
descriptor = Descriptor('2902', 'READABLE,WRITEABLE')
|
descriptor = Descriptor('2902', 'READABLE,WRITEABLE')
|
||||||
assert descriptor.permissions == 3
|
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())
|
||||||
|
|||||||
+8
-2
@@ -215,12 +215,18 @@ def test_HCI_Command():
|
|||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
def test_HCI_PIN_Code_Request_Reply_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(
|
command = HCI_PIN_Code_Request_Reply_Command(
|
||||||
bd_addr=Address(
|
bd_addr=Address(
|
||||||
'00:11:22:33:44:55', address_type=Address.PUBLIC_DEVICE_ADDRESS
|
'00:11:22:33:44:55', address_type=Address.PUBLIC_DEVICE_ADDRESS
|
||||||
),
|
),
|
||||||
pin_code_length=4,
|
pin_code_length=pin_code_length,
|
||||||
pin_code=b'1234',
|
pin_code=padded_pin_code,
|
||||||
)
|
)
|
||||||
basic_check(command)
|
basic_check(command)
|
||||||
|
|
||||||
|
|||||||
+95
-23
@@ -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,32 +162,37 @@ 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]),
|
||||||
)
|
)
|
||||||
|
|
||||||
s1 = Service('8140E247-04F0-42C1-BC34-534C344DAFCA', [c1, c2, c3])
|
s1 = Service('8140E247-04F0-42C1-BC34-534C344DAFCA', [c1, c2, c3])
|
||||||
s2 = Service('97210A0F-1875-4D05-9E5D-326EB171257A', [c4])
|
s2 = Service('97210A0F-1875-4D05-9E5D-326EB171257A', [c4])
|
||||||
two_devices.devices[1].add_services([s1, s2])
|
s3 = Service('1853', [])
|
||||||
|
s4 = Service('3A12C182-14E2-4FE0-8C5B-65D7C569F9DB', [], included_services=[s2, s3])
|
||||||
|
two_devices.devices[1].add_services([s1, s2, s4])
|
||||||
|
|
||||||
# Start
|
# Start
|
||||||
await two_devices.devices[0].power_on()
|
await two_devices.devices[0].power_on()
|
||||||
@@ -167,6 +227,13 @@ async def test_self_gatt():
|
|||||||
assert result is not None
|
assert result is not None
|
||||||
assert result == c1.value
|
assert result == c1.value
|
||||||
|
|
||||||
|
result = await peer.discover_service(s4.uuid)
|
||||||
|
assert len(result) == 1
|
||||||
|
result = await peer.discover_included_services(result[0])
|
||||||
|
assert len(result) == 2
|
||||||
|
# Service UUID is only present when the UUID is 16-bit Bluetooth UUID
|
||||||
|
assert result[1].uuid.to_bytes() == s3.uuid.to_bytes()
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -178,7 +245,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 +270,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 +325,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 +343,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 +389,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 +432,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 +453,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