forked from auracaster/bumble_mirror
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 864889ccab | |||
| bc29f327ef | |||
| 1894b96de4 | |||
| 33ae047765 | |||
| 1efa2e9d44 | |||
| e77723a5f9 | |||
| fe8cf51432 | |||
| 97a0e115ae | |||
| 46e7aac77c | |||
| 08a6f4fa49 | |||
| ca063eda0b | |||
| c97ba4319f | |||
| a5275ade29 | |||
| e7b39c4188 | |||
| 0594eaef09 | |||
| 05200284d2 | |||
| d21da78aa3 | |||
| fbc7cf02a3 | |||
| a8beb6b1ff | |||
| 2d44de611f | |||
| 9874bb3b37 | |||
| 6645ad47ee | |||
| ad27de7717 | |||
| e6fc63b2d8 | |||
| 1321c7da81 | |||
| 5a1b03fd91 | |||
| de47721753 | |||
| 83a76a75d3 | |||
| d5b5ef8313 | |||
| 856a8d53cd | |||
| 177c273a57 |
Vendored
+6
-1
@@ -71,5 +71,10 @@
|
|||||||
"editor.rulers": [88]
|
"editor.rulers": [88]
|
||||||
},
|
},
|
||||||
"python.formatting.provider": "black",
|
"python.formatting.provider": "black",
|
||||||
"pylint.importStrategy": "useBundled"
|
"pylint.importStrategy": "useBundled",
|
||||||
|
"python.testing.pytestArgs": [
|
||||||
|
"."
|
||||||
|
],
|
||||||
|
"python.testing.unittestEnabled": false,
|
||||||
|
"python.testing.pytestEnabled": true
|
||||||
}
|
}
|
||||||
|
|||||||
+1207
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -27,7 +27,6 @@ import re
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
import click
|
import click
|
||||||
import colors
|
|
||||||
|
|
||||||
from prompt_toolkit import Application
|
from prompt_toolkit import Application
|
||||||
from prompt_toolkit.history import FileHistory
|
from prompt_toolkit.history import FileHistory
|
||||||
@@ -53,6 +52,7 @@ from prompt_toolkit.layout import (
|
|||||||
|
|
||||||
from bumble import __version__
|
from bumble import __version__
|
||||||
import bumble.core
|
import bumble.core
|
||||||
|
from bumble import colors
|
||||||
from bumble.core import UUID, AdvertisingData, BT_LE_TRANSPORT
|
from bumble.core import UUID, AdvertisingData, BT_LE_TRANSPORT
|
||||||
from bumble.device import ConnectionParametersPreferences, Device, Connection, Peer
|
from bumble.device import ConnectionParametersPreferences, Device, Connection, Peer
|
||||||
from bumble.utils import AsyncRunner
|
from bumble.utils import AsyncRunner
|
||||||
|
|||||||
+17
-6
@@ -19,9 +19,9 @@ import asyncio
|
|||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
import click
|
import click
|
||||||
from colors import color
|
|
||||||
from bumble.company_ids import COMPANY_IDENTIFIERS
|
from bumble.company_ids import COMPANY_IDENTIFIERS
|
||||||
|
|
||||||
|
from bumble.colors import color
|
||||||
from bumble.core import name_or_number
|
from bumble.core import name_or_number
|
||||||
from bumble.hci import (
|
from bumble.hci import (
|
||||||
map_null_terminated_utf8_string,
|
map_null_terminated_utf8_string,
|
||||||
@@ -30,6 +30,8 @@ from bumble.hci import (
|
|||||||
HCI_VERSION_NAMES,
|
HCI_VERSION_NAMES,
|
||||||
LMP_VERSION_NAMES,
|
LMP_VERSION_NAMES,
|
||||||
HCI_Command,
|
HCI_Command,
|
||||||
|
HCI_Command_Complete_Event,
|
||||||
|
HCI_Command_Status_Event,
|
||||||
HCI_READ_BD_ADDR_COMMAND,
|
HCI_READ_BD_ADDR_COMMAND,
|
||||||
HCI_Read_BD_ADDR_Command,
|
HCI_Read_BD_ADDR_Command,
|
||||||
HCI_READ_LOCAL_NAME_COMMAND,
|
HCI_READ_LOCAL_NAME_COMMAND,
|
||||||
@@ -45,11 +47,20 @@ from bumble.host import Host
|
|||||||
from bumble.transport import open_transport_or_link
|
from bumble.transport import open_transport_or_link
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def command_succeeded(response):
|
||||||
|
if isinstance(response, HCI_Command_Status_Event):
|
||||||
|
return response.status == HCI_SUCCESS
|
||||||
|
if isinstance(response, HCI_Command_Complete_Event):
|
||||||
|
return response.return_parameters.status == HCI_SUCCESS
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
async def get_classic_info(host):
|
async def get_classic_info(host):
|
||||||
if host.supports_command(HCI_READ_BD_ADDR_COMMAND):
|
if host.supports_command(HCI_READ_BD_ADDR_COMMAND):
|
||||||
response = await host.send_command(HCI_Read_BD_ADDR_Command())
|
response = await host.send_command(HCI_Read_BD_ADDR_Command())
|
||||||
if response.return_parameters.status == HCI_SUCCESS:
|
if command_succeeded(response):
|
||||||
print()
|
print()
|
||||||
print(
|
print(
|
||||||
color('Classic Address:', 'yellow'), response.return_parameters.bd_addr
|
color('Classic Address:', 'yellow'), response.return_parameters.bd_addr
|
||||||
@@ -57,7 +68,7 @@ async def get_classic_info(host):
|
|||||||
|
|
||||||
if host.supports_command(HCI_READ_LOCAL_NAME_COMMAND):
|
if host.supports_command(HCI_READ_LOCAL_NAME_COMMAND):
|
||||||
response = await host.send_command(HCI_Read_Local_Name_Command())
|
response = await host.send_command(HCI_Read_Local_Name_Command())
|
||||||
if response.return_parameters.status == HCI_SUCCESS:
|
if command_succeeded(response):
|
||||||
print()
|
print()
|
||||||
print(
|
print(
|
||||||
color('Local Name:', 'yellow'),
|
color('Local Name:', 'yellow'),
|
||||||
@@ -73,7 +84,7 @@ async def get_le_info(host):
|
|||||||
response = await host.send_command(
|
response = await host.send_command(
|
||||||
HCI_LE_Read_Number_Of_Supported_Advertising_Sets_Command()
|
HCI_LE_Read_Number_Of_Supported_Advertising_Sets_Command()
|
||||||
)
|
)
|
||||||
if response.return_parameters.status == HCI_SUCCESS:
|
if command_succeeded(response):
|
||||||
print(
|
print(
|
||||||
color('LE Number Of Supported Advertising Sets:', 'yellow'),
|
color('LE Number Of Supported Advertising Sets:', 'yellow'),
|
||||||
response.return_parameters.num_supported_advertising_sets,
|
response.return_parameters.num_supported_advertising_sets,
|
||||||
@@ -84,7 +95,7 @@ async def get_le_info(host):
|
|||||||
response = await host.send_command(
|
response = await host.send_command(
|
||||||
HCI_LE_Read_Maximum_Advertising_Data_Length_Command()
|
HCI_LE_Read_Maximum_Advertising_Data_Length_Command()
|
||||||
)
|
)
|
||||||
if response.return_parameters.status == HCI_SUCCESS:
|
if command_succeeded(response):
|
||||||
print(
|
print(
|
||||||
color('LE Maximum Advertising Data Length:', 'yellow'),
|
color('LE Maximum Advertising Data Length:', 'yellow'),
|
||||||
response.return_parameters.max_advertising_data_length,
|
response.return_parameters.max_advertising_data_length,
|
||||||
@@ -93,7 +104,7 @@ async def get_le_info(host):
|
|||||||
|
|
||||||
if host.supports_command(HCI_LE_READ_MAXIMUM_DATA_LENGTH_COMMAND):
|
if host.supports_command(HCI_LE_READ_MAXIMUM_DATA_LENGTH_COMMAND):
|
||||||
response = await host.send_command(HCI_LE_Read_Maximum_Data_Length_Command())
|
response = await host.send_command(HCI_LE_Read_Maximum_Data_Length_Command())
|
||||||
if response.return_parameters.status == HCI_SUCCESS:
|
if command_succeeded(response):
|
||||||
print(
|
print(
|
||||||
color('Maximum Data Length:', 'yellow'),
|
color('Maximum Data Length:', 'yellow'),
|
||||||
(
|
(
|
||||||
|
|||||||
+1
-1
@@ -19,9 +19,9 @@ import asyncio
|
|||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
import click
|
import click
|
||||||
from colors import color
|
|
||||||
|
|
||||||
import bumble.core
|
import bumble.core
|
||||||
|
from bumble.colors import color
|
||||||
from bumble.device import Device, Peer
|
from bumble.device import Device, Peer
|
||||||
from bumble.gatt import show_services
|
from bumble.gatt import show_services
|
||||||
from bumble.transport import open_transport_or_link
|
from bumble.transport import open_transport_or_link
|
||||||
|
|||||||
+1
-1
@@ -20,8 +20,8 @@ import os
|
|||||||
import struct
|
import struct
|
||||||
import logging
|
import logging
|
||||||
import click
|
import click
|
||||||
from colors import color
|
|
||||||
|
|
||||||
|
from bumble.colors import color
|
||||||
from bumble.device import Device, Peer
|
from bumble.device import Device, Peer
|
||||||
from bumble.core import AdvertisingData
|
from bumble.core import AdvertisingData
|
||||||
from bumble.gatt import Service, Characteristic, CharacteristicValue
|
from bumble.gatt import Service, Characteristic, CharacteristicValue
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import click
|
import click
|
||||||
from colors import color
|
|
||||||
|
|
||||||
|
from bumble.colors import color
|
||||||
from bumble.transport import open_transport_or_link
|
from bumble.transport import open_transport_or_link
|
||||||
from bumble.device import Device
|
from bumble.device import Device
|
||||||
from bumble.utils import FlowControlAsyncPipe
|
from bumble.utils import FlowControlAsyncPipe
|
||||||
|
|||||||
@@ -23,9 +23,10 @@ import argparse
|
|||||||
import uuid
|
import uuid
|
||||||
import os
|
import os
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
from colors import color
|
|
||||||
import websockets
|
import websockets
|
||||||
|
|
||||||
|
from bumble.colors import color
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Logging
|
# Logging
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|||||||
+78
-43
@@ -19,9 +19,9 @@ import asyncio
|
|||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
import click
|
import click
|
||||||
import aioconsole
|
from prompt_toolkit.shortcuts import PromptSession
|
||||||
from 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.smp import PairingDelegate, PairingConfig
|
||||||
@@ -42,9 +42,23 @@ from bumble.att import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class Waiter:
|
||||||
|
instance = None
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.done = asyncio.get_running_loop().create_future()
|
||||||
|
|
||||||
|
def terminate(self):
|
||||||
|
self.done.set_result(None)
|
||||||
|
|
||||||
|
async def wait_until_terminated(self):
|
||||||
|
return await self.done
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class Delegate(PairingDelegate):
|
class Delegate(PairingDelegate):
|
||||||
def __init__(self, mode, connection, capability_string, prompt):
|
def __init__(self, mode, connection, capability_string, do_prompt):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
{
|
{
|
||||||
'keyboard': PairingDelegate.KEYBOARD_INPUT_ONLY,
|
'keyboard': PairingDelegate.KEYBOARD_INPUT_ONLY,
|
||||||
@@ -58,7 +72,18 @@ class Delegate(PairingDelegate):
|
|||||||
self.mode = mode
|
self.mode = mode
|
||||||
self.peer = Peer(connection)
|
self.peer = Peer(connection)
|
||||||
self.peer_name = None
|
self.peer_name = None
|
||||||
self.prompt = prompt
|
self.do_prompt = do_prompt
|
||||||
|
|
||||||
|
def print(self, message):
|
||||||
|
print(color(message, 'yellow'))
|
||||||
|
|
||||||
|
async def prompt(self, message):
|
||||||
|
# Wait a bit to allow some of the log lines to print before we prompt
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
session = PromptSession(message)
|
||||||
|
response = await session.prompt_async()
|
||||||
|
return response.lower().strip()
|
||||||
|
|
||||||
async def update_peer_name(self):
|
async def update_peer_name(self):
|
||||||
if self.peer_name is not None:
|
if self.peer_name is not None:
|
||||||
@@ -73,19 +98,15 @@ class Delegate(PairingDelegate):
|
|||||||
self.peer_name = '[?]'
|
self.peer_name = '[?]'
|
||||||
|
|
||||||
async def accept(self):
|
async def accept(self):
|
||||||
if self.prompt:
|
if self.do_prompt:
|
||||||
await self.update_peer_name()
|
await self.update_peer_name()
|
||||||
|
|
||||||
# Wait a bit to allow some of the log lines to print before we prompt
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
|
|
||||||
# Prompt for acceptance
|
# Prompt for acceptance
|
||||||
print(color('###-----------------------------------', 'yellow'))
|
self.print('###-----------------------------------')
|
||||||
print(color(f'### Pairing request from {self.peer_name}', 'yellow'))
|
self.print(f'### Pairing request from {self.peer_name}')
|
||||||
print(color('###-----------------------------------', 'yellow'))
|
self.print('###-----------------------------------')
|
||||||
while True:
|
while True:
|
||||||
response = await aioconsole.ainput(color('>>> Accept? ', 'yellow'))
|
response = await self.prompt('>>> Accept? ')
|
||||||
response = response.lower().strip()
|
|
||||||
|
|
||||||
if response == 'yes':
|
if response == 'yes':
|
||||||
return True
|
return True
|
||||||
@@ -96,23 +117,17 @@ class Delegate(PairingDelegate):
|
|||||||
# Accept silently
|
# Accept silently
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def compare_numbers(self, number, digits=6):
|
async def compare_numbers(self, number, digits):
|
||||||
await self.update_peer_name()
|
await self.update_peer_name()
|
||||||
|
|
||||||
# Wait a bit to allow some of the log lines to print before we prompt
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
|
|
||||||
# Prompt for a numeric comparison
|
# Prompt for a numeric comparison
|
||||||
print(color('###-----------------------------------', 'yellow'))
|
self.print('###-----------------------------------')
|
||||||
print(color(f'### Pairing with {self.peer_name}', 'yellow'))
|
self.print(f'### Pairing with {self.peer_name}')
|
||||||
print(color('###-----------------------------------', 'yellow'))
|
self.print('###-----------------------------------')
|
||||||
while True:
|
while True:
|
||||||
response = await aioconsole.ainput(
|
response = await self.prompt(
|
||||||
color(
|
f'>>> Does the other device display {number:0{digits}}? '
|
||||||
f'>>> Does the other device display {number:0{digits}}? ', 'yellow'
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
response = response.lower().strip()
|
|
||||||
|
|
||||||
if response == 'yes':
|
if response == 'yes':
|
||||||
return True
|
return True
|
||||||
@@ -123,30 +138,24 @@ class Delegate(PairingDelegate):
|
|||||||
async def get_number(self):
|
async def get_number(self):
|
||||||
await self.update_peer_name()
|
await self.update_peer_name()
|
||||||
|
|
||||||
# Wait a bit to allow some of the log lines to print before we prompt
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
|
|
||||||
# Prompt for a PIN
|
# Prompt for a PIN
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
print(color('###-----------------------------------', 'yellow'))
|
self.print('###-----------------------------------')
|
||||||
print(color(f'### Pairing with {self.peer_name}', 'yellow'))
|
self.print(f'### Pairing with {self.peer_name}')
|
||||||
print(color('###-----------------------------------', 'yellow'))
|
self.print('###-----------------------------------')
|
||||||
return int(await aioconsole.ainput(color('>>> Enter PIN: ', 'yellow')))
|
return int(await self.prompt('>>> Enter PIN: '))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def display_number(self, number, digits=6):
|
async def display_number(self, number, digits):
|
||||||
await self.update_peer_name()
|
await self.update_peer_name()
|
||||||
|
|
||||||
# Wait a bit to allow some of the log lines to print before we prompt
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
|
|
||||||
# Display a PIN code
|
# Display a PIN code
|
||||||
print(color('###-----------------------------------', 'yellow'))
|
self.print('###-----------------------------------')
|
||||||
print(color(f'### Pairing with {self.peer_name}', 'yellow'))
|
self.print(f'### Pairing with {self.peer_name}')
|
||||||
print(color(f'### PIN: {number:0{digits}}', 'yellow'))
|
self.print(f'### PIN: {number:0{digits}}')
|
||||||
print(color('###-----------------------------------', 'yellow'))
|
self.print('###-----------------------------------')
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -238,6 +247,7 @@ def on_pairing(keys):
|
|||||||
print(color('*** Paired!', 'cyan'))
|
print(color('*** Paired!', 'cyan'))
|
||||||
keys.print(prefix=color('*** ', 'cyan'))
|
keys.print(prefix=color('*** ', 'cyan'))
|
||||||
print(color('***-----------------------------------', 'cyan'))
|
print(color('***-----------------------------------', 'cyan'))
|
||||||
|
Waiter.instance.terminate()
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -245,6 +255,7 @@ def on_pairing_failure(reason):
|
|||||||
print(color('***-----------------------------------', 'red'))
|
print(color('***-----------------------------------', 'red'))
|
||||||
print(color(f'*** Pairing failed: {smp_error_name(reason)}', 'red'))
|
print(color(f'*** Pairing failed: {smp_error_name(reason)}', 'red'))
|
||||||
print(color('***-----------------------------------', 'red'))
|
print(color('***-----------------------------------', 'red'))
|
||||||
|
Waiter.instance.terminate()
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -262,6 +273,8 @@ async def pair(
|
|||||||
hci_transport,
|
hci_transport,
|
||||||
address_or_name,
|
address_or_name,
|
||||||
):
|
):
|
||||||
|
Waiter.instance = Waiter()
|
||||||
|
|
||||||
print('<<< connecting to HCI...')
|
print('<<< connecting to HCI...')
|
||||||
async with await open_transport_or_link(hci_transport) as (hci_source, hci_sink):
|
async with await open_transport_or_link(hci_transport) as (hci_source, hci_sink):
|
||||||
print('<<< connected')
|
print('<<< connected')
|
||||||
@@ -332,7 +345,19 @@ async def pair(
|
|||||||
# Advertise so that peers can find us and connect
|
# Advertise so that peers can find us and connect
|
||||||
await device.start_advertising(auto_restart=True)
|
await device.start_advertising(auto_restart=True)
|
||||||
|
|
||||||
await hci_source.wait_for_termination()
|
# Run until the user asks to exit
|
||||||
|
await Waiter.instance.wait_until_terminated()
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class LogHandler(logging.Handler):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.setFormatter(logging.Formatter('%(levelname)s:%(name)s:%(message)s'))
|
||||||
|
|
||||||
|
def emit(self, record):
|
||||||
|
message = self.format(record)
|
||||||
|
print(message)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -366,7 +391,11 @@ async def pair(
|
|||||||
'--request', is_flag=True, help='Request that the connecting peer initiate pairing'
|
'--request', is_flag=True, help='Request that the connecting peer initiate pairing'
|
||||||
)
|
)
|
||||||
@click.option('--print-keys', is_flag=True, help='Print the bond keys before pairing')
|
@click.option('--print-keys', is_flag=True, help='Print the bond keys before pairing')
|
||||||
@click.option('--keystore-file', help='File in which to store the pairing keys')
|
@click.option(
|
||||||
|
'--keystore-file',
|
||||||
|
metavar='<filename>',
|
||||||
|
help='File in which to store the pairing keys',
|
||||||
|
)
|
||||||
@click.argument('device-config')
|
@click.argument('device-config')
|
||||||
@click.argument('hci_transport')
|
@click.argument('hci_transport')
|
||||||
@click.argument('address-or-name', required=False)
|
@click.argument('address-or-name', required=False)
|
||||||
@@ -384,7 +413,13 @@ def main(
|
|||||||
hci_transport,
|
hci_transport,
|
||||||
address_or_name,
|
address_or_name,
|
||||||
):
|
):
|
||||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
# Setup logging
|
||||||
|
log_handler = LogHandler()
|
||||||
|
root_logger = logging.getLogger()
|
||||||
|
root_logger.addHandler(log_handler)
|
||||||
|
root_logger.setLevel(os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||||
|
|
||||||
|
# Pair
|
||||||
asyncio.run(
|
asyncio.run(
|
||||||
pair(
|
pair(
|
||||||
mode,
|
mode,
|
||||||
|
|||||||
+1
-1
@@ -19,8 +19,8 @@ import asyncio
|
|||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
import click
|
import click
|
||||||
from colors import color
|
|
||||||
|
|
||||||
|
from bumble.colors import color
|
||||||
from bumble.device import Device
|
from bumble.device import Device
|
||||||
from bumble.transport import open_transport_or_link
|
from bumble.transport import open_transport_or_link
|
||||||
from bumble.keys import JsonKeyStore
|
from bumble.keys import JsonKeyStore
|
||||||
|
|||||||
+1
-1
@@ -17,8 +17,8 @@
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
import struct
|
import struct
|
||||||
import click
|
import click
|
||||||
from colors import color
|
|
||||||
|
|
||||||
|
from bumble.colors import color
|
||||||
from bumble import hci
|
from bumble import hci
|
||||||
from bumble.transport.common import PacketReader
|
from bumble.transport.common import PacketReader
|
||||||
from bumble.helpers import PacketTracer
|
from bumble.helpers import PacketTracer
|
||||||
|
|||||||
+1
-1
@@ -30,8 +30,8 @@ import os
|
|||||||
import logging
|
import logging
|
||||||
import click
|
import click
|
||||||
import usb1
|
import usb1
|
||||||
from colors import color
|
|
||||||
|
|
||||||
|
from bumble.colors import color
|
||||||
from bumble.transport.usb import load_libusb
|
from bumble.transport.usb import load_libusb
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+45
-30
@@ -18,7 +18,6 @@
|
|||||||
import struct
|
import struct
|
||||||
import logging
|
import logging
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
import construct
|
|
||||||
|
|
||||||
from .company_ids import COMPANY_IDENTIFIERS
|
from .company_ids import COMPANY_IDENTIFIERS
|
||||||
from .sdp import (
|
from .sdp import (
|
||||||
@@ -258,17 +257,6 @@ class SbcMediaCodecInformation(
|
|||||||
A2DP spec - 4.3.2 Codec Specific Information Elements
|
A2DP spec - 4.3.2 Codec Specific Information Elements
|
||||||
'''
|
'''
|
||||||
|
|
||||||
BIT_FIELDS = construct.Bitwise(
|
|
||||||
construct.Sequence(
|
|
||||||
construct.BitsInteger(4),
|
|
||||||
construct.BitsInteger(4),
|
|
||||||
construct.BitsInteger(4),
|
|
||||||
construct.BitsInteger(2),
|
|
||||||
construct.BitsInteger(2),
|
|
||||||
construct.BitsInteger(8),
|
|
||||||
construct.BitsInteger(8),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
SAMPLING_FREQUENCY_BITS = {16000: 1 << 3, 32000: 1 << 2, 44100: 1 << 1, 48000: 1}
|
SAMPLING_FREQUENCY_BITS = {16000: 1 << 3, 32000: 1 << 2, 44100: 1 << 1, 48000: 1}
|
||||||
CHANNEL_MODE_BITS = {
|
CHANNEL_MODE_BITS = {
|
||||||
SBC_MONO_CHANNEL_MODE: 1 << 3,
|
SBC_MONO_CHANNEL_MODE: 1 << 3,
|
||||||
@@ -284,9 +272,22 @@ class SbcMediaCodecInformation(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_bytes(data):
|
def from_bytes(data: bytes) -> 'SbcMediaCodecInformation':
|
||||||
|
sampling_frequency = (data[0] >> 4) & 0x0F
|
||||||
|
channel_mode = (data[0] >> 0) & 0x0F
|
||||||
|
block_length = (data[1] >> 4) & 0x0F
|
||||||
|
subbands = (data[1] >> 2) & 0x03
|
||||||
|
allocation_method = (data[1] >> 0) & 0x03
|
||||||
|
minimum_bitpool_value = (data[2] >> 0) & 0xFF
|
||||||
|
maximum_bitpool_value = (data[3] >> 0) & 0xFF
|
||||||
return SbcMediaCodecInformation(
|
return SbcMediaCodecInformation(
|
||||||
*SbcMediaCodecInformation.BIT_FIELDS.parse(data)
|
sampling_frequency,
|
||||||
|
channel_mode,
|
||||||
|
block_length,
|
||||||
|
subbands,
|
||||||
|
allocation_method,
|
||||||
|
minimum_bitpool_value,
|
||||||
|
maximum_bitpool_value,
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -335,8 +336,17 @@ class SbcMediaCodecInformation(
|
|||||||
maximum_bitpool_value=maximum_bitpool_value,
|
maximum_bitpool_value=maximum_bitpool_value,
|
||||||
)
|
)
|
||||||
|
|
||||||
def __bytes__(self):
|
def __bytes__(self) -> bytes:
|
||||||
return self.BIT_FIELDS.build(self)
|
return bytes(
|
||||||
|
[
|
||||||
|
(self.sampling_frequency << 4) | self.channel_mode,
|
||||||
|
(self.block_length << 4)
|
||||||
|
| (self.subbands << 2)
|
||||||
|
| self.allocation_method,
|
||||||
|
self.minimum_bitpool_value,
|
||||||
|
self.maximum_bitpool_value,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
channel_modes = ['MONO', 'DUAL_CHANNEL', 'STEREO', 'JOINT_STEREO']
|
channel_modes = ['MONO', 'DUAL_CHANNEL', 'STEREO', 'JOINT_STEREO']
|
||||||
@@ -367,16 +377,6 @@ class AacMediaCodecInformation(
|
|||||||
A2DP spec - 4.5.2 Codec Specific Information Elements
|
A2DP spec - 4.5.2 Codec Specific Information Elements
|
||||||
'''
|
'''
|
||||||
|
|
||||||
BIT_FIELDS = construct.Bitwise(
|
|
||||||
construct.Sequence(
|
|
||||||
construct.BitsInteger(8),
|
|
||||||
construct.BitsInteger(12),
|
|
||||||
construct.BitsInteger(2),
|
|
||||||
construct.BitsInteger(2),
|
|
||||||
construct.BitsInteger(1),
|
|
||||||
construct.BitsInteger(23),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
OBJECT_TYPE_BITS = {
|
OBJECT_TYPE_BITS = {
|
||||||
MPEG_2_AAC_LC_OBJECT_TYPE: 1 << 7,
|
MPEG_2_AAC_LC_OBJECT_TYPE: 1 << 7,
|
||||||
MPEG_4_AAC_LC_OBJECT_TYPE: 1 << 6,
|
MPEG_4_AAC_LC_OBJECT_TYPE: 1 << 6,
|
||||||
@@ -400,9 +400,15 @@ class AacMediaCodecInformation(
|
|||||||
CHANNELS_BITS = {1: 1 << 1, 2: 1}
|
CHANNELS_BITS = {1: 1 << 1, 2: 1}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_bytes(data):
|
def from_bytes(data: bytes) -> 'AacMediaCodecInformation':
|
||||||
|
object_type = data[0]
|
||||||
|
sampling_frequency = (data[1] << 4) | ((data[2] >> 4) & 0x0F)
|
||||||
|
channels = (data[2] >> 2) & 0x03
|
||||||
|
rfa = 0
|
||||||
|
vbr = (data[3] >> 7) & 0x01
|
||||||
|
bitrate = ((data[3] & 0x7F) << 16) | (data[4] << 8) | data[5]
|
||||||
return AacMediaCodecInformation(
|
return AacMediaCodecInformation(
|
||||||
*AacMediaCodecInformation.BIT_FIELDS.parse(data)
|
object_type, sampling_frequency, channels, rfa, vbr, bitrate
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -430,8 +436,17 @@ class AacMediaCodecInformation(
|
|||||||
bitrate=bitrate,
|
bitrate=bitrate,
|
||||||
)
|
)
|
||||||
|
|
||||||
def __bytes__(self):
|
def __bytes__(self) -> bytes:
|
||||||
return self.BIT_FIELDS.build(self)
|
return bytes(
|
||||||
|
[
|
||||||
|
self.object_type & 0xFF,
|
||||||
|
(self.sampling_frequency >> 4) & 0xFF,
|
||||||
|
(((self.sampling_frequency & 0x0F) << 4) | (self.channels << 2)) & 0xFF,
|
||||||
|
((self.vbr << 7) | ((self.bitrate >> 16) & 0x7F)) & 0xFF,
|
||||||
|
((self.bitrate >> 8) & 0xFF) & 0xFF,
|
||||||
|
self.bitrate & 0xFF,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
object_types = [
|
object_types = [
|
||||||
|
|||||||
+42
-4
@@ -24,13 +24,15 @@
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import struct
|
import struct
|
||||||
from colors import color
|
|
||||||
from pyee import EventEmitter
|
from pyee import EventEmitter
|
||||||
from typing import Dict, Type
|
from typing import Dict, Type, TYPE_CHECKING
|
||||||
|
|
||||||
from bumble.core import UUID, name_or_number
|
from bumble.core import UUID, name_or_number
|
||||||
from bumble.hci import HCI_Object, key_with_value
|
from bumble.hci import HCI_Object, key_with_value
|
||||||
|
from bumble.colors import color
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from bumble.device import Connection
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Constants
|
# Constants
|
||||||
@@ -749,7 +751,25 @@ class Attribute(EventEmitter):
|
|||||||
def decode_value(self, value_bytes):
|
def decode_value(self, value_bytes):
|
||||||
return value_bytes
|
return value_bytes
|
||||||
|
|
||||||
def read_value(self, connection):
|
def read_value(self, connection: Connection):
|
||||||
|
if (
|
||||||
|
self.permissions & self.READ_REQUIRES_ENCRYPTION
|
||||||
|
) and not connection.encryption:
|
||||||
|
raise ATT_Error(
|
||||||
|
error_code=ATT_INSUFFICIENT_ENCRYPTION_ERROR, att_handle=self.handle
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
self.permissions & self.READ_REQUIRES_AUTHENTICATION
|
||||||
|
) and not connection.authenticated:
|
||||||
|
raise ATT_Error(
|
||||||
|
error_code=ATT_INSUFFICIENT_AUTHENTICATION_ERROR, att_handle=self.handle
|
||||||
|
)
|
||||||
|
if self.permissions & self.READ_REQUIRES_AUTHORIZATION:
|
||||||
|
# TODO: handle authorization better
|
||||||
|
raise ATT_Error(
|
||||||
|
error_code=ATT_INSUFFICIENT_AUTHORIZATION_ERROR, att_handle=self.handle
|
||||||
|
)
|
||||||
|
|
||||||
if read := getattr(self.value, 'read', None):
|
if read := getattr(self.value, 'read', None):
|
||||||
try:
|
try:
|
||||||
value = read(connection) # pylint: disable=not-callable
|
value = read(connection) # pylint: disable=not-callable
|
||||||
@@ -762,7 +782,25 @@ class Attribute(EventEmitter):
|
|||||||
|
|
||||||
return self.encode_value(value)
|
return self.encode_value(value)
|
||||||
|
|
||||||
def write_value(self, connection, value_bytes):
|
def write_value(self, connection: Connection, value_bytes):
|
||||||
|
if (
|
||||||
|
self.permissions & self.WRITE_REQUIRES_ENCRYPTION
|
||||||
|
) and not connection.encryption:
|
||||||
|
raise ATT_Error(
|
||||||
|
error_code=ATT_INSUFFICIENT_ENCRYPTION_ERROR, att_handle=self.handle
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
self.permissions & self.WRITE_REQUIRES_AUTHENTICATION
|
||||||
|
) and not connection.authenticated:
|
||||||
|
raise ATT_Error(
|
||||||
|
error_code=ATT_INSUFFICIENT_AUTHENTICATION_ERROR, att_handle=self.handle
|
||||||
|
)
|
||||||
|
if self.permissions & self.WRITE_REQUIRES_AUTHORIZATION:
|
||||||
|
# TODO: handle authorization better
|
||||||
|
raise ATT_Error(
|
||||||
|
error_code=ATT_INSUFFICIENT_AUTHORIZATION_ERROR, att_handle=self.handle
|
||||||
|
)
|
||||||
|
|
||||||
value = self.decode_value(value_bytes)
|
value = self.decode_value(value_bytes)
|
||||||
|
|
||||||
if write := getattr(self.value, 'write', None):
|
if write := getattr(self.value, 'write', None):
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,6 @@ import asyncio
|
|||||||
import struct
|
import struct
|
||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
from colors import color
|
|
||||||
from pyee import EventEmitter
|
from pyee import EventEmitter
|
||||||
from typing import Dict, Type
|
from typing import Dict, Type
|
||||||
|
|
||||||
@@ -40,6 +39,7 @@ from .a2dp import (
|
|||||||
VendorSpecificMediaCodecInformation,
|
VendorSpecificMediaCodecInformation,
|
||||||
)
|
)
|
||||||
from . import sdp
|
from . import sdp
|
||||||
|
from .colors import color
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Logging
|
# Logging
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
from functools import partial
|
||||||
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
|
|
||||||
|
# ANSI color names. There is also a "default"
|
||||||
|
COLORS = ('black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white')
|
||||||
|
|
||||||
|
# ANSI style names
|
||||||
|
STYLES = (
|
||||||
|
'none',
|
||||||
|
'bold',
|
||||||
|
'faint',
|
||||||
|
'italic',
|
||||||
|
'underline',
|
||||||
|
'blink',
|
||||||
|
'blink2',
|
||||||
|
'negative',
|
||||||
|
'concealed',
|
||||||
|
'crossed',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
ColorSpec = Union[str, int]
|
||||||
|
|
||||||
|
|
||||||
|
def _join(*values: ColorSpec) -> str:
|
||||||
|
return ';'.join(str(v) for v in values)
|
||||||
|
|
||||||
|
|
||||||
|
def _color_code(spec: ColorSpec, base: int) -> str:
|
||||||
|
if isinstance(spec, str):
|
||||||
|
spec = spec.strip().lower()
|
||||||
|
|
||||||
|
if spec == 'default':
|
||||||
|
return _join(base + 9)
|
||||||
|
elif spec in COLORS:
|
||||||
|
return _join(base + COLORS.index(spec))
|
||||||
|
elif isinstance(spec, int) and 0 <= spec <= 255:
|
||||||
|
return _join(base + 8, 5, spec)
|
||||||
|
else:
|
||||||
|
raise ValueError('Invalid color spec "%s"' % spec)
|
||||||
|
|
||||||
|
|
||||||
|
def color(
|
||||||
|
s: str,
|
||||||
|
fg: Optional[ColorSpec] = None,
|
||||||
|
bg: Optional[ColorSpec] = None,
|
||||||
|
style: Optional[str] = None,
|
||||||
|
) -> str:
|
||||||
|
codes: List[ColorSpec] = []
|
||||||
|
|
||||||
|
if fg:
|
||||||
|
codes.append(_color_code(fg, 30))
|
||||||
|
if bg:
|
||||||
|
codes.append(_color_code(bg, 40))
|
||||||
|
if style:
|
||||||
|
for style_part in style.split('+'):
|
||||||
|
if style_part in STYLES:
|
||||||
|
codes.append(STYLES.index(style_part))
|
||||||
|
else:
|
||||||
|
raise ValueError('Invalid style "%s"' % style_part)
|
||||||
|
|
||||||
|
if codes:
|
||||||
|
return '\x1b[{0}m{1}\x1b[0m'.format(_join(*codes), s)
|
||||||
|
else:
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
# Foreground color shortcuts
|
||||||
|
black = partial(color, fg='black')
|
||||||
|
red = partial(color, fg='red')
|
||||||
|
green = partial(color, fg='green')
|
||||||
|
yellow = partial(color, fg='yellow')
|
||||||
|
blue = partial(color, fg='blue')
|
||||||
|
magenta = partial(color, fg='magenta')
|
||||||
|
cyan = partial(color, fg='cyan')
|
||||||
|
white = partial(color, fg='white')
|
||||||
|
|
||||||
|
# Style shortcuts
|
||||||
|
bold = partial(color, style='bold')
|
||||||
|
none = partial(color, style='none')
|
||||||
|
faint = partial(color, style='faint')
|
||||||
|
italic = partial(color, style='italic')
|
||||||
|
underline = partial(color, style='underline')
|
||||||
|
blink = partial(color, style='blink')
|
||||||
|
blink2 = partial(color, style='blink2')
|
||||||
|
negative = partial(color, style='negative')
|
||||||
|
concealed = partial(color, style='concealed')
|
||||||
|
crossed = partial(color, style='crossed')
|
||||||
@@ -20,7 +20,7 @@ import asyncio
|
|||||||
import itertools
|
import itertools
|
||||||
import random
|
import random
|
||||||
import struct
|
import struct
|
||||||
from 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
|
||||||
|
|
||||||
from bumble.hci import (
|
from bumble.hci import (
|
||||||
|
|||||||
+16
-6
@@ -144,9 +144,12 @@ class ConnectionError(BaseError): # pylint: disable=redefined-builtin
|
|||||||
class UUID:
|
class UUID:
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 3, Part B - 2.5.1 UUID
|
See Bluetooth spec Vol 3, Part B - 2.5.1 UUID
|
||||||
|
|
||||||
|
Note that this class expects and works in little-endian byte-order throughout.
|
||||||
|
The exception is when interacting with strings, which are in big-endian byte-order.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
BASE_UUID = bytes.fromhex('00001000800000805F9B34FB')
|
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):
|
def __init__(self, uuid_str_or_int, name=None):
|
||||||
@@ -209,13 +212,20 @@ class 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=False):
|
||||||
if len(self.uuid_bytes) == 16 or not force_128:
|
'''
|
||||||
|
Serialize UUID in little-endian byte-order
|
||||||
|
'''
|
||||||
|
if not force_128:
|
||||||
return self.uuid_bytes
|
return self.uuid_bytes
|
||||||
|
|
||||||
if len(self.uuid_bytes) == 4:
|
if len(self.uuid_bytes) == 2:
|
||||||
return self.uuid_bytes + UUID.BASE_UUID
|
return self.BASE_UUID + self.uuid_bytes + bytes([0, 0])
|
||||||
|
elif len(self.uuid_bytes) == 4:
|
||||||
return self.uuid_bytes + bytes([0, 0]) + UUID.BASE_UUID
|
return self.BASE_UUID + self.uuid_bytes
|
||||||
|
elif len(self.uuid_bytes) == 16:
|
||||||
|
return self.uuid_bytes
|
||||||
|
else:
|
||||||
|
assert False, "unreachable"
|
||||||
|
|
||||||
def to_pdu_bytes(self):
|
def to_pdu_bytes(self):
|
||||||
'''
|
'''
|
||||||
|
|||||||
+32
-23
@@ -25,8 +25,7 @@ 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, ClassVar, Dict, List, Optional, Tuple, 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 (
|
||||||
@@ -51,6 +50,7 @@ from .hci import (
|
|||||||
HCI_LE_EXTENDED_CREATE_CONNECTION_COMMAND,
|
HCI_LE_EXTENDED_CREATE_CONNECTION_COMMAND,
|
||||||
HCI_LE_RAND_COMMAND,
|
HCI_LE_RAND_COMMAND,
|
||||||
HCI_LE_READ_PHY_COMMAND,
|
HCI_LE_READ_PHY_COMMAND,
|
||||||
|
HCI_LE_SET_PHY_COMMAND,
|
||||||
HCI_MITM_NOT_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS,
|
HCI_MITM_NOT_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS,
|
||||||
HCI_MITM_NOT_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS,
|
HCI_MITM_NOT_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS,
|
||||||
HCI_MITM_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS,
|
HCI_MITM_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS,
|
||||||
@@ -535,6 +535,9 @@ class Connection(CompositeEventEmitter):
|
|||||||
def on_connection_parameters_update_failure(self, error):
|
def on_connection_parameters_update_failure(self, error):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def on_connection_data_length_change(self):
|
||||||
|
pass
|
||||||
|
|
||||||
def on_connection_phy_update(self):
|
def on_connection_phy_update(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -1240,6 +1243,11 @@ class Device(CompositeEventEmitter):
|
|||||||
# Done
|
# Done
|
||||||
self.powered_on = True
|
self.powered_on = True
|
||||||
|
|
||||||
|
async def power_off(self) -> None:
|
||||||
|
if self.powered_on:
|
||||||
|
await self.host.flush()
|
||||||
|
self.powered_on = False
|
||||||
|
|
||||||
def supports_le_feature(self, feature):
|
def supports_le_feature(self, feature):
|
||||||
return self.host.supports_le_feature(feature)
|
return self.host.supports_le_feature(feature)
|
||||||
|
|
||||||
@@ -1664,7 +1672,7 @@ class Device(CompositeEventEmitter):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
if not phys:
|
if not phys:
|
||||||
raise ValueError('least one supported PHY needed')
|
raise ValueError('at least one supported PHY needed')
|
||||||
|
|
||||||
phy_count = len(phys)
|
phy_count = len(phys)
|
||||||
initiating_phys = phy_list_to_bits(phys)
|
initiating_phys = phy_list_to_bits(phys)
|
||||||
@@ -1805,7 +1813,7 @@ class Device(CompositeEventEmitter):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
return await self.abort_on('flush', pending_connection)
|
return await self.abort_on('flush', pending_connection)
|
||||||
except ConnectionError as error:
|
except core.ConnectionError as error:
|
||||||
raise core.TimeoutError() from error
|
raise core.TimeoutError() from error
|
||||||
finally:
|
finally:
|
||||||
self.remove_listener('connection', on_connection)
|
self.remove_listener('connection', on_connection)
|
||||||
@@ -1827,7 +1835,7 @@ class Device(CompositeEventEmitter):
|
|||||||
set.
|
set.
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
* A `connect` to the same peer will also complete this call.
|
* A `connect` to the same peer will not complete this call.
|
||||||
* The `timeout` parameter is only handled while waiting for the connection
|
* The `timeout` parameter is only handled while waiting for the connection
|
||||||
request, once received and accepted, the controller shall issue a connection
|
request, once received and accepted, the controller shall issue a connection
|
||||||
complete event.
|
complete event.
|
||||||
@@ -2009,7 +2017,7 @@ class Device(CompositeEventEmitter):
|
|||||||
NOTE: the name of the parameters may look odd, but it just follows the names
|
NOTE: the name of the parameters may look odd, but it just follows the names
|
||||||
used in the Bluetooth spec.
|
used in the Bluetooth spec.
|
||||||
'''
|
'''
|
||||||
await self.send_command(
|
result = await self.send_command(
|
||||||
HCI_LE_Connection_Update_Command(
|
HCI_LE_Connection_Update_Command(
|
||||||
connection_handle=connection.handle,
|
connection_handle=connection.handle,
|
||||||
connection_interval_min=connection_interval_min,
|
connection_interval_min=connection_interval_min,
|
||||||
@@ -2018,9 +2026,10 @@ class Device(CompositeEventEmitter):
|
|||||||
supervision_timeout=supervision_timeout,
|
supervision_timeout=supervision_timeout,
|
||||||
min_ce_length=min_ce_length,
|
min_ce_length=min_ce_length,
|
||||||
max_ce_length=max_ce_length,
|
max_ce_length=max_ce_length,
|
||||||
),
|
)
|
||||||
check_result=True,
|
|
||||||
)
|
)
|
||||||
|
if result.status != HCI_Command_Status_Event.PENDING:
|
||||||
|
raise HCI_StatusError(result)
|
||||||
|
|
||||||
async def get_connection_rssi(self, connection):
|
async def get_connection_rssi(self, connection):
|
||||||
result = await self.send_command(
|
result = await self.send_command(
|
||||||
@@ -2038,21 +2047,31 @@ class Device(CompositeEventEmitter):
|
|||||||
async def set_connection_phy(
|
async def set_connection_phy(
|
||||||
self, connection, tx_phys=None, rx_phys=None, phy_options=None
|
self, connection, tx_phys=None, rx_phys=None, phy_options=None
|
||||||
):
|
):
|
||||||
|
if not self.host.supports_command(HCI_LE_SET_PHY_COMMAND):
|
||||||
|
logger.warning('ignoring request, command not supported')
|
||||||
|
return
|
||||||
|
|
||||||
all_phys_bits = (1 if tx_phys is None else 0) | (
|
all_phys_bits = (1 if tx_phys is None else 0) | (
|
||||||
(1 if rx_phys is None else 0) << 1
|
(1 if rx_phys is None else 0) << 1
|
||||||
)
|
)
|
||||||
|
|
||||||
return await self.send_command(
|
result = await self.send_command(
|
||||||
HCI_LE_Set_PHY_Command(
|
HCI_LE_Set_PHY_Command(
|
||||||
connection_handle=connection.handle,
|
connection_handle=connection.handle,
|
||||||
all_phys=all_phys_bits,
|
all_phys=all_phys_bits,
|
||||||
tx_phys=phy_list_to_bits(tx_phys),
|
tx_phys=phy_list_to_bits(tx_phys),
|
||||||
rx_phys=phy_list_to_bits(rx_phys),
|
rx_phys=phy_list_to_bits(rx_phys),
|
||||||
phy_options=0 if phy_options is None else int(phy_options),
|
phy_options=0 if phy_options is None else int(phy_options),
|
||||||
),
|
)
|
||||||
check_result=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if result.status != HCI_COMMAND_STATUS_PENDING:
|
||||||
|
logger.warning(
|
||||||
|
'HCI_LE_Set_PHY_Command failed: '
|
||||||
|
f'{HCI_Constant.error_name(result.status)}'
|
||||||
|
)
|
||||||
|
raise HCI_StatusError(result)
|
||||||
|
|
||||||
async def set_default_phy(self, tx_phys=None, rx_phys=None):
|
async def set_default_phy(self, tx_phys=None, rx_phys=None):
|
||||||
all_phys_bits = (1 if tx_phys is None else 0) | (
|
all_phys_bits = (1 if tx_phys is None else 0) | (
|
||||||
(1 if rx_phys is None else 0) << 1
|
(1 if rx_phys is None else 0) << 1
|
||||||
@@ -2409,16 +2428,6 @@ class Device(CompositeEventEmitter):
|
|||||||
)
|
)
|
||||||
self.connections[connection_handle] = connection
|
self.connections[connection_handle] = connection
|
||||||
|
|
||||||
# We may have an accept ongoing waiting for a connection request for
|
|
||||||
# `peer_address`.
|
|
||||||
# Typically happen when using `connect` to the same `peer_address` we are
|
|
||||||
# waiting for with an `accept`.
|
|
||||||
# In this case, set the completed `connection` to the `accept` future
|
|
||||||
# result.
|
|
||||||
if peer_address in self.classic_pending_accepts:
|
|
||||||
future, *_ = self.classic_pending_accepts.pop(peer_address)
|
|
||||||
future.set_result(connection)
|
|
||||||
|
|
||||||
# Emit an event to notify listeners of the new connection
|
# Emit an event to notify listeners of the new connection
|
||||||
self.emit('connection', connection)
|
self.emit('connection', connection)
|
||||||
else:
|
else:
|
||||||
@@ -2501,7 +2510,7 @@ class Device(CompositeEventEmitter):
|
|||||||
self.advertising = False
|
self.advertising = False
|
||||||
|
|
||||||
# Notify listeners
|
# Notify listeners
|
||||||
error = ConnectionError(
|
error = core.ConnectionError(
|
||||||
error_code,
|
error_code,
|
||||||
transport,
|
transport,
|
||||||
peer_address,
|
peer_address,
|
||||||
@@ -2574,7 +2583,7 @@ class Device(CompositeEventEmitter):
|
|||||||
@with_connection_from_handle
|
@with_connection_from_handle
|
||||||
def on_disconnection_failure(self, connection, error_code):
|
def on_disconnection_failure(self, connection, error_code):
|
||||||
logger.debug(f'*** Disconnection failed: {error_code}')
|
logger.debug(f'*** Disconnection failed: {error_code}')
|
||||||
error = ConnectionError(
|
error = core.ConnectionError(
|
||||||
error_code,
|
error_code,
|
||||||
connection.transport,
|
connection.transport,
|
||||||
connection.peer_address,
|
connection.peer_address,
|
||||||
|
|||||||
+1
-1
@@ -29,8 +29,8 @@ import functools
|
|||||||
import logging
|
import logging
|
||||||
import struct
|
import struct
|
||||||
from typing import Optional, Sequence
|
from typing import Optional, Sequence
|
||||||
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
|
||||||
from .att import Attribute
|
from .att import Attribute
|
||||||
|
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
import struct
|
import struct
|
||||||
|
|
||||||
from colors import color
|
|
||||||
from pyee import EventEmitter
|
from pyee import EventEmitter
|
||||||
|
|
||||||
|
from .colors import color
|
||||||
from .hci import HCI_Constant
|
from .hci import HCI_Constant
|
||||||
from .att import (
|
from .att import (
|
||||||
ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
||||||
|
|||||||
+61
-35
@@ -29,8 +29,8 @@ from collections import defaultdict
|
|||||||
import struct
|
import struct
|
||||||
from typing import List, Tuple, Optional
|
from typing import List, Tuple, Optional
|
||||||
from pyee import EventEmitter
|
from pyee import EventEmitter
|
||||||
from colors import color
|
|
||||||
|
|
||||||
|
from .colors import color
|
||||||
from .core import UUID
|
from .core import UUID
|
||||||
from .att import (
|
from .att import (
|
||||||
ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
||||||
@@ -61,7 +61,6 @@ from .att import (
|
|||||||
from .gatt import (
|
from .gatt import (
|
||||||
GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
|
GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
|
||||||
GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
|
GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
|
||||||
GATT_INCLUDE_ATTRIBUTE_TYPE,
|
|
||||||
GATT_MAX_ATTRIBUTE_VALUE_SIZE,
|
GATT_MAX_ATTRIBUTE_VALUE_SIZE,
|
||||||
GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
|
GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
|
||||||
GATT_REQUEST_TIMEOUT,
|
GATT_REQUEST_TIMEOUT,
|
||||||
@@ -543,8 +542,6 @@ class Server(EventEmitter):
|
|||||||
if attribute.handle >= request.starting_handle
|
if attribute.handle >= request.starting_handle
|
||||||
and attribute.handle <= request.ending_handle
|
and attribute.handle <= request.ending_handle
|
||||||
):
|
):
|
||||||
# TODO: check permissions
|
|
||||||
|
|
||||||
this_uuid_size = len(attribute.type.to_pdu_bytes())
|
this_uuid_size = len(attribute.type.to_pdu_bytes())
|
||||||
|
|
||||||
if attributes:
|
if attributes:
|
||||||
@@ -638,6 +635,13 @@ class Server(EventEmitter):
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
pdu_space_available = connection.att_mtu - 2
|
pdu_space_available = connection.att_mtu - 2
|
||||||
|
|
||||||
|
response = ATT_Error_Response(
|
||||||
|
request_opcode_in_error=request.op_code,
|
||||||
|
attribute_handle_in_error=request.starting_handle,
|
||||||
|
error_code=ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
||||||
|
)
|
||||||
|
|
||||||
attributes = []
|
attributes = []
|
||||||
for attribute in (
|
for attribute in (
|
||||||
attribute
|
attribute
|
||||||
@@ -647,10 +651,21 @@ class Server(EventEmitter):
|
|||||||
and attribute.handle <= request.ending_handle
|
and attribute.handle <= request.ending_handle
|
||||||
and pdu_space_available
|
and pdu_space_available
|
||||||
):
|
):
|
||||||
# TODO: check permissions
|
|
||||||
|
try:
|
||||||
|
attribute_value = attribute.read_value(connection)
|
||||||
|
except ATT_Error as error:
|
||||||
|
# If the first attribute is unreadable, return an error
|
||||||
|
# Otherwise return attributes up to this point
|
||||||
|
if not attributes:
|
||||||
|
response = ATT_Error_Response(
|
||||||
|
request_opcode_in_error=request.op_code,
|
||||||
|
attribute_handle_in_error=attribute.handle,
|
||||||
|
error_code=error.error_code,
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
# Check the attribute value size
|
# Check the attribute value size
|
||||||
attribute_value = attribute.read_value(connection)
|
|
||||||
max_attribute_size = min(connection.att_mtu - 4, 253)
|
max_attribute_size = min(connection.att_mtu - 4, 253)
|
||||||
if len(attribute_value) > max_attribute_size:
|
if len(attribute_value) > max_attribute_size:
|
||||||
# We need to truncate
|
# We need to truncate
|
||||||
@@ -676,11 +691,7 @@ class Server(EventEmitter):
|
|||||||
length=entry_size, attribute_data_list=b''.join(attribute_data_list)
|
length=entry_size, attribute_data_list=b''.join(attribute_data_list)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
response = ATT_Error_Response(
|
logging.debug(f"not found {request}")
|
||||||
request_opcode_in_error=request.op_code,
|
|
||||||
attribute_handle_in_error=request.starting_handle,
|
|
||||||
error_code=ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.send_response(connection, response)
|
self.send_response(connection, response)
|
||||||
|
|
||||||
@@ -690,10 +701,17 @@ class Server(EventEmitter):
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
if attribute := self.get_attribute(request.attribute_handle):
|
if attribute := self.get_attribute(request.attribute_handle):
|
||||||
# TODO: check permissions
|
try:
|
||||||
value = attribute.read_value(connection)
|
value = attribute.read_value(connection)
|
||||||
value_size = min(connection.att_mtu - 1, len(value))
|
except ATT_Error as error:
|
||||||
response = ATT_Read_Response(attribute_value=value[:value_size])
|
response = ATT_Error_Response(
|
||||||
|
request_opcode_in_error=request.op_code,
|
||||||
|
attribute_handle_in_error=request.attribute_handle,
|
||||||
|
error_code=error.error_code,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
value_size = min(connection.att_mtu - 1, len(value))
|
||||||
|
response = ATT_Read_Response(attribute_value=value[:value_size])
|
||||||
else:
|
else:
|
||||||
response = ATT_Error_Response(
|
response = ATT_Error_Response(
|
||||||
request_opcode_in_error=request.op_code,
|
request_opcode_in_error=request.op_code,
|
||||||
@@ -708,29 +726,36 @@ class Server(EventEmitter):
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
if attribute := self.get_attribute(request.attribute_handle):
|
if attribute := self.get_attribute(request.attribute_handle):
|
||||||
# TODO: check permissions
|
try:
|
||||||
value = attribute.read_value(connection)
|
value = attribute.read_value(connection)
|
||||||
if request.value_offset > len(value):
|
except ATT_Error as error:
|
||||||
response = ATT_Error_Response(
|
response = ATT_Error_Response(
|
||||||
request_opcode_in_error=request.op_code,
|
request_opcode_in_error=request.op_code,
|
||||||
attribute_handle_in_error=request.attribute_handle,
|
attribute_handle_in_error=request.attribute_handle,
|
||||||
error_code=ATT_INVALID_OFFSET_ERROR,
|
error_code=error.error_code,
|
||||||
)
|
|
||||||
elif len(value) <= connection.att_mtu - 1:
|
|
||||||
response = ATT_Error_Response(
|
|
||||||
request_opcode_in_error=request.op_code,
|
|
||||||
attribute_handle_in_error=request.attribute_handle,
|
|
||||||
error_code=ATT_ATTRIBUTE_NOT_LONG_ERROR,
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
part_size = min(
|
if request.value_offset > len(value):
|
||||||
connection.att_mtu - 1, len(value) - request.value_offset
|
response = ATT_Error_Response(
|
||||||
)
|
request_opcode_in_error=request.op_code,
|
||||||
response = ATT_Read_Blob_Response(
|
attribute_handle_in_error=request.attribute_handle,
|
||||||
part_attribute_value=value[
|
error_code=ATT_INVALID_OFFSET_ERROR,
|
||||||
request.value_offset : request.value_offset + part_size
|
)
|
||||||
]
|
elif len(value) <= connection.att_mtu - 1:
|
||||||
)
|
response = ATT_Error_Response(
|
||||||
|
request_opcode_in_error=request.op_code,
|
||||||
|
attribute_handle_in_error=request.attribute_handle,
|
||||||
|
error_code=ATT_ATTRIBUTE_NOT_LONG_ERROR,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
part_size = min(
|
||||||
|
connection.att_mtu - 1, len(value) - request.value_offset
|
||||||
|
)
|
||||||
|
response = ATT_Read_Blob_Response(
|
||||||
|
part_attribute_value=value[
|
||||||
|
request.value_offset : request.value_offset + part_size
|
||||||
|
]
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
response = ATT_Error_Response(
|
response = ATT_Error_Response(
|
||||||
request_opcode_in_error=request.op_code,
|
request_opcode_in_error=request.op_code,
|
||||||
@@ -746,7 +771,6 @@ class Server(EventEmitter):
|
|||||||
if request.attribute_group_type not in (
|
if request.attribute_group_type not in (
|
||||||
GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
|
GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
|
||||||
GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE,
|
GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE,
|
||||||
GATT_INCLUDE_ATTRIBUTE_TYPE,
|
|
||||||
):
|
):
|
||||||
response = ATT_Error_Response(
|
response = ATT_Error_Response(
|
||||||
request_opcode_in_error=request.op_code,
|
request_opcode_in_error=request.op_code,
|
||||||
@@ -766,8 +790,10 @@ class Server(EventEmitter):
|
|||||||
and attribute.handle <= request.ending_handle
|
and attribute.handle <= request.ending_handle
|
||||||
and pdu_space_available
|
and pdu_space_available
|
||||||
):
|
):
|
||||||
# Check the attribute value size
|
# No need to catch permission errors here, since these attributes
|
||||||
|
# must all be world-readable
|
||||||
attribute_value = attribute.read_value(connection)
|
attribute_value = attribute.read_value(connection)
|
||||||
|
# Check the attribute value size
|
||||||
max_attribute_size = min(connection.att_mtu - 6, 251)
|
max_attribute_size = min(connection.att_mtu - 6, 251)
|
||||||
if len(attribute_value) > max_attribute_size:
|
if len(attribute_value) > max_attribute_size:
|
||||||
# We need to truncate
|
# We need to truncate
|
||||||
|
|||||||
+14
-2
@@ -20,9 +20,9 @@ import struct
|
|||||||
import collections
|
import collections
|
||||||
import logging
|
import logging
|
||||||
import functools
|
import functools
|
||||||
from colors import color
|
|
||||||
from typing import Dict, Type, Union
|
from typing import Dict, Type, Union
|
||||||
|
|
||||||
|
from .colors import color
|
||||||
from .core import (
|
from .core import (
|
||||||
BT_BR_EDR_TRANSPORT,
|
BT_BR_EDR_TRANSPORT,
|
||||||
AdvertisingData,
|
AdvertisingData,
|
||||||
@@ -1421,7 +1421,11 @@ class HCI_Constant:
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class HCI_Error(ProtocolError):
|
class HCI_Error(ProtocolError):
|
||||||
def __init__(self, error_code):
|
def __init__(self, error_code):
|
||||||
super().__init__(error_code, 'hci', HCI_Constant.error_name(error_code))
|
super().__init__(
|
||||||
|
error_code,
|
||||||
|
error_namespace='hci',
|
||||||
|
error_name=HCI_Constant.error_name(error_code),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -1846,6 +1850,8 @@ class HCI_Packet:
|
|||||||
Abstract Base class for HCI packets
|
Abstract Base class for HCI packets
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
hci_packet_type: int
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_bytes(packet):
|
def from_bytes(packet):
|
||||||
packet_type = packet[0]
|
packet_type = packet[0]
|
||||||
@@ -1864,6 +1870,9 @@ class HCI_Packet:
|
|||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
self.name = name
|
self.name = name
|
||||||
|
|
||||||
|
def __bytes__(self) -> bytes:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
@@ -1875,6 +1884,9 @@ class HCI_CustomPacket(HCI_Packet):
|
|||||||
self.hci_packet_type = payload[0]
|
self.hci_packet_type = payload[0]
|
||||||
self.payload = payload
|
self.payload = payload
|
||||||
|
|
||||||
|
def __bytes__(self) -> bytes:
|
||||||
|
return self.payload
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class HCI_Command(HCI_Packet):
|
class HCI_Command(HCI_Packet):
|
||||||
|
|||||||
+1
-1
@@ -16,8 +16,8 @@
|
|||||||
# Imports
|
# Imports
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
import logging
|
import logging
|
||||||
from colors import color
|
|
||||||
|
|
||||||
|
from .colors import color
|
||||||
from .att import ATT_CID, ATT_PDU
|
from .att import ATT_CID, ATT_PDU
|
||||||
from .smp import SMP_CID, SMP_Command
|
from .smp import SMP_CID, SMP_Command
|
||||||
from .core import name_or_number
|
from .core import name_or_number
|
||||||
|
|||||||
+2
-1
@@ -18,7 +18,8 @@
|
|||||||
import logging
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
import collections
|
import collections
|
||||||
from colors import color
|
|
||||||
|
from .colors import color
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|||||||
+9
-2
@@ -20,9 +20,9 @@ import collections
|
|||||||
import logging
|
import logging
|
||||||
import struct
|
import struct
|
||||||
|
|
||||||
from colors import color
|
from bumble.colors import color
|
||||||
|
|
||||||
from bumble.l2cap import L2CAP_PDU
|
from bumble.l2cap import L2CAP_PDU
|
||||||
|
from bumble.snoop import Snooper
|
||||||
|
|
||||||
from .hci import (
|
from .hci import (
|
||||||
HCI_ACL_DATA_PACKET,
|
HCI_ACL_DATA_PACKET,
|
||||||
@@ -134,6 +134,7 @@ class Host(AbortableEventEmitter):
|
|||||||
self.long_term_key_provider = None
|
self.long_term_key_provider = None
|
||||||
self.link_key_provider = None
|
self.link_key_provider = None
|
||||||
self.pairing_io_capability_provider = None # Classic only
|
self.pairing_io_capability_provider = None # Classic only
|
||||||
|
self.snooper = None
|
||||||
|
|
||||||
# Connect to the source and sink if specified
|
# Connect to the source and sink if specified
|
||||||
if controller_source:
|
if controller_source:
|
||||||
@@ -274,6 +275,9 @@ class Host(AbortableEventEmitter):
|
|||||||
self.hci_sink = sink
|
self.hci_sink = sink
|
||||||
|
|
||||||
def send_hci_packet(self, packet):
|
def send_hci_packet(self, packet):
|
||||||
|
if self.snooper:
|
||||||
|
self.snooper.snoop(packet, Snooper.Direction.HOST_TO_CONTROLLER)
|
||||||
|
|
||||||
self.hci_sink.on_packet(packet.to_bytes())
|
self.hci_sink.on_packet(packet.to_bytes())
|
||||||
|
|
||||||
async def send_command(self, command, check_result=False):
|
async def send_command(self, command, check_result=False):
|
||||||
@@ -420,6 +424,9 @@ class Host(AbortableEventEmitter):
|
|||||||
def on_hci_packet(self, packet):
|
def on_hci_packet(self, packet):
|
||||||
logger.debug(f'{color("### CONTROLLER -> HOST", "green")}: {packet}')
|
logger.debug(f'{color("### CONTROLLER -> HOST", "green")}: {packet}')
|
||||||
|
|
||||||
|
if self.snooper:
|
||||||
|
self.snooper.snoop(packet, Snooper.Direction.CONTROLLER_TO_HOST)
|
||||||
|
|
||||||
# If the packet is a command, invoke the handler for this packet
|
# If the packet is a command, invoke the handler for this packet
|
||||||
if packet.hci_packet_type == HCI_COMMAND_PACKET:
|
if packet.hci_packet_type == HCI_COMMAND_PACKET:
|
||||||
self.on_hci_command_packet(packet)
|
self.on_hci_command_packet(packet)
|
||||||
|
|||||||
+3
-3
@@ -25,8 +25,8 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from colors import color
|
|
||||||
|
|
||||||
|
from .colors import color
|
||||||
from .hci import Address
|
from .hci import Address
|
||||||
|
|
||||||
|
|
||||||
@@ -130,7 +130,7 @@ class PairingKeys:
|
|||||||
for (key_property, key_value) in value.items():
|
for (key_property, key_value) in value.items():
|
||||||
print(f'{prefix} {color(key_property, "green")}: {key_value}')
|
print(f'{prefix} {color(key_property, "green")}: {key_value}')
|
||||||
else:
|
else:
|
||||||
print(f'{prefix}{color(property, "cyan")}: {value}')
|
print(f'{prefix}{color(container_property, "cyan")}: {value}')
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -217,7 +217,7 @@ class JsonKeyStore(KeyStore):
|
|||||||
params = device_config.keystore.split(':', 1)[1:]
|
params = device_config.keystore.split(':', 1)[1:]
|
||||||
namespace = str(device_config.address)
|
namespace = str(device_config.address)
|
||||||
if params:
|
if params:
|
||||||
filename = params[1]
|
filename = params[0]
|
||||||
else:
|
else:
|
||||||
filename = None
|
filename = None
|
||||||
|
|
||||||
|
|||||||
+14
-1
@@ -21,10 +21,10 @@ import logging
|
|||||||
import struct
|
import struct
|
||||||
|
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from colors import color
|
|
||||||
from pyee import EventEmitter
|
from pyee import EventEmitter
|
||||||
from typing import Dict, Type
|
from typing import Dict, Type
|
||||||
|
|
||||||
|
from .colors import color
|
||||||
from .core import BT_CENTRAL_ROLE, InvalidStateError, ProtocolError
|
from .core import BT_CENTRAL_ROLE, InvalidStateError, ProtocolError
|
||||||
from .hci import (
|
from .hci import (
|
||||||
HCI_LE_Connection_Update_Command,
|
HCI_LE_Connection_Update_Command,
|
||||||
@@ -796,6 +796,11 @@ class Channel(EventEmitter):
|
|||||||
self.disconnection_result = asyncio.get_running_loop().create_future()
|
self.disconnection_result = asyncio.get_running_loop().create_future()
|
||||||
return await self.disconnection_result
|
return await self.disconnection_result
|
||||||
|
|
||||||
|
def abort(self):
|
||||||
|
if self.state == self.OPEN:
|
||||||
|
self.change_state(self.CLOSED)
|
||||||
|
self.emit('close')
|
||||||
|
|
||||||
def send_configure_request(self):
|
def send_configure_request(self):
|
||||||
options = L2CAP_Control_Frame.encode_configuration_options(
|
options = L2CAP_Control_Frame.encode_configuration_options(
|
||||||
[
|
[
|
||||||
@@ -1105,6 +1110,10 @@ class LeConnectionOrientedChannel(EventEmitter):
|
|||||||
self.disconnection_result = asyncio.get_running_loop().create_future()
|
self.disconnection_result = asyncio.get_running_loop().create_future()
|
||||||
return await self.disconnection_result
|
return await self.disconnection_result
|
||||||
|
|
||||||
|
def abort(self):
|
||||||
|
if self.state == self.CONNECTED:
|
||||||
|
self.change_state(self.DISCONNECTED)
|
||||||
|
|
||||||
def on_pdu(self, pdu):
|
def on_pdu(self, pdu):
|
||||||
if self.sink is None:
|
if self.sink is None:
|
||||||
logger.warning('received pdu without a sink')
|
logger.warning('received pdu without a sink')
|
||||||
@@ -1492,8 +1501,12 @@ class ChannelManager:
|
|||||||
def on_disconnection(self, connection_handle, _reason):
|
def on_disconnection(self, connection_handle, _reason):
|
||||||
logger.debug(f'disconnection from {connection_handle}, cleaning up channels')
|
logger.debug(f'disconnection from {connection_handle}, cleaning up channels')
|
||||||
if connection_handle in self.channels:
|
if connection_handle in self.channels:
|
||||||
|
for _, channel in self.channels[connection_handle].items():
|
||||||
|
channel.abort()
|
||||||
del self.channels[connection_handle]
|
del self.channels[connection_handle]
|
||||||
if connection_handle in self.le_coc_channels:
|
if connection_handle in self.le_coc_channels:
|
||||||
|
for _, channel in self.le_coc_channels[connection_handle].items():
|
||||||
|
channel.abort()
|
||||||
del self.le_coc_channels[connection_handle]
|
del self.le_coc_channels[connection_handle]
|
||||||
if connection_handle in self.identifiers:
|
if connection_handle in self.identifiers:
|
||||||
del self.identifiers[connection_handle]
|
del self.identifiers[connection_handle]
|
||||||
|
|||||||
+3
-3
@@ -19,9 +19,7 @@ import logging
|
|||||||
import asyncio
|
import asyncio
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from colors import color
|
from bumble.colors import color
|
||||||
import websockets
|
|
||||||
|
|
||||||
from bumble.hci import (
|
from bumble.hci import (
|
||||||
Address,
|
Address,
|
||||||
HCI_SUCCESS,
|
HCI_SUCCESS,
|
||||||
@@ -220,6 +218,8 @@ class RemoteLink:
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def run_connection(self):
|
async def run_connection(self):
|
||||||
|
import websockets # lazy import
|
||||||
|
|
||||||
# Connect to the relay
|
# Connect to the relay
|
||||||
logger.debug(f'connecting to {self.uri}')
|
logger.debug(f'connecting to {self.uri}')
|
||||||
# pylint: disable-next=no-member
|
# pylint: disable-next=no-member
|
||||||
|
|||||||
+21
-11
@@ -18,10 +18,10 @@
|
|||||||
import logging
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from colors import color
|
|
||||||
from pyee import EventEmitter
|
from pyee import EventEmitter
|
||||||
|
|
||||||
from . import core
|
from . import core
|
||||||
|
from .colors import color
|
||||||
from .core import BT_BR_EDR_TRANSPORT, InvalidStateError, ProtocolError
|
from .core import BT_BR_EDR_TRANSPORT, InvalidStateError, ProtocolError
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -852,17 +852,27 @@ class Server(EventEmitter):
|
|||||||
# Register ourselves with the L2CAP channel manager
|
# Register ourselves with the L2CAP channel manager
|
||||||
device.register_l2cap_server(RFCOMM_PSM, self.on_connection)
|
device.register_l2cap_server(RFCOMM_PSM, self.on_connection)
|
||||||
|
|
||||||
def listen(self, acceptor):
|
def listen(self, acceptor, channel=0):
|
||||||
# Find a free channel number
|
if channel:
|
||||||
for channel in range(
|
if channel in self.acceptors:
|
||||||
RFCOMM_DYNAMIC_CHANNEL_NUMBER_START, RFCOMM_DYNAMIC_CHANNEL_NUMBER_END + 1
|
# Busy
|
||||||
):
|
return 0
|
||||||
if channel not in self.acceptors:
|
else:
|
||||||
self.acceptors[channel] = acceptor
|
# Find a free channel number
|
||||||
return channel
|
for candidate in range(
|
||||||
|
RFCOMM_DYNAMIC_CHANNEL_NUMBER_START,
|
||||||
|
RFCOMM_DYNAMIC_CHANNEL_NUMBER_END + 1,
|
||||||
|
):
|
||||||
|
if candidate not in self.acceptors:
|
||||||
|
channel = candidate
|
||||||
|
break
|
||||||
|
|
||||||
# All channels used...
|
if channel == 0:
|
||||||
return 0
|
# All channels used...
|
||||||
|
return 0
|
||||||
|
|
||||||
|
self.acceptors[channel] = acceptor
|
||||||
|
return channel
|
||||||
|
|
||||||
def on_connection(self, l2cap_channel):
|
def on_connection(self, l2cap_channel):
|
||||||
logger.debug(f'+++ new L2CAP connection: {l2cap_channel}')
|
logger.debug(f'+++ new L2CAP connection: {l2cap_channel}')
|
||||||
|
|||||||
+2
-3
@@ -18,11 +18,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import logging
|
import logging
|
||||||
import struct
|
import struct
|
||||||
from colors import color
|
|
||||||
import colors
|
|
||||||
from typing import Dict, List, Type
|
from typing import Dict, List, Type
|
||||||
|
|
||||||
from . import core
|
from . import core
|
||||||
|
from .colors import color
|
||||||
from .core import InvalidStateError
|
from .core import InvalidStateError
|
||||||
from .hci import HCI_Object, name_or_number, key_with_value
|
from .hci import HCI_Object, name_or_number, key_with_value
|
||||||
|
|
||||||
@@ -506,7 +505,7 @@ class ServiceAttribute:
|
|||||||
def to_string(self, with_colors=False):
|
def to_string(self, with_colors=False):
|
||||||
if with_colors:
|
if with_colors:
|
||||||
return (
|
return (
|
||||||
f'Attribute(id={colors.color(self.id_name(self.id),"magenta")},'
|
f'Attribute(id={color(self.id_name(self.id),"magenta")},'
|
||||||
f'value={self.value})'
|
f'value={self.value})'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+48
-30
@@ -29,8 +29,8 @@ import secrets
|
|||||||
from typing import Dict, Optional, Type
|
from typing import Dict, Optional, Type
|
||||||
|
|
||||||
from pyee import EventEmitter
|
from pyee import EventEmitter
|
||||||
from colors import color
|
|
||||||
|
|
||||||
|
from .colors import color
|
||||||
from .hci import Address, HCI_LE_Enable_Encryption_Command, HCI_Object, key_with_value
|
from .hci import Address, HCI_LE_Enable_Encryption_Command, HCI_Object, key_with_value
|
||||||
from .core import (
|
from .core import (
|
||||||
BT_BR_EDR_TRANSPORT,
|
BT_BR_EDR_TRANSPORT,
|
||||||
@@ -498,7 +498,7 @@ class PairingDelegate:
|
|||||||
DISPLAY_OUTPUT_ONLY = SMP_DISPLAY_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_YES_NO_INPUT = SMP_DISPLAY_YES_NO_IO_CAPABILITY
|
||||||
DISPLAY_OUTPUT_AND_KEYBOARD_INPUT = SMP_KEYBOARD_DISPLAY_IO_CAPABILITY
|
DISPLAY_OUTPUT_AND_KEYBOARD_INPUT = SMP_KEYBOARD_DISPLAY_IO_CAPABILITY
|
||||||
DEFAULT_KEY_DISTRIBUTION = (
|
DEFAULT_KEY_DISTRIBUTION: int = (
|
||||||
SMP_ENC_KEY_DISTRIBUTION_FLAG | SMP_ID_KEY_DISTRIBUTION_FLAG
|
SMP_ENC_KEY_DISTRIBUTION_FLAG | SMP_ID_KEY_DISTRIBUTION_FLAG
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -518,13 +518,15 @@ class PairingDelegate:
|
|||||||
async def confirm(self) -> bool:
|
async def confirm(self) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def compare_numbers(self, _number: int, _digits: int = 6) -> bool:
|
# pylint: disable-next=unused-argument
|
||||||
|
async def compare_numbers(self, number: int, digits: int) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def get_number(self) -> int:
|
async def get_number(self) -> int:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
async def display_number(self, _number: int, _digits: int = 6) -> None:
|
# pylint: disable-next=unused-argument
|
||||||
|
async def display_number(self, number: int, digits: int) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def key_distribution_response(
|
async def key_distribution_response(
|
||||||
@@ -661,7 +663,8 @@ class Session:
|
|||||||
self.peer_expected_distributions = []
|
self.peer_expected_distributions = []
|
||||||
self.dh_key = None
|
self.dh_key = None
|
||||||
self.confirm_value = None
|
self.confirm_value = None
|
||||||
self.passkey = 0
|
self.passkey = None
|
||||||
|
self.passkey_ready = asyncio.Event()
|
||||||
self.passkey_step = 0
|
self.passkey_step = 0
|
||||||
self.passkey_display = False
|
self.passkey_display = False
|
||||||
self.pairing_method = 0
|
self.pairing_method = 0
|
||||||
@@ -839,6 +842,7 @@ class Session:
|
|||||||
# Generate random Passkey/PIN code
|
# Generate random Passkey/PIN code
|
||||||
self.passkey = secrets.randbelow(1000000)
|
self.passkey = secrets.randbelow(1000000)
|
||||||
logger.debug(f'Pairing PIN CODE: {self.passkey:06}')
|
logger.debug(f'Pairing PIN CODE: {self.passkey:06}')
|
||||||
|
self.passkey_ready.set()
|
||||||
|
|
||||||
# The value of TK is computed from the PIN code
|
# The value of TK is computed from the PIN code
|
||||||
if not self.sc:
|
if not self.sc:
|
||||||
@@ -859,6 +863,8 @@ class Session:
|
|||||||
self.tk = passkey.to_bytes(16, byteorder='little')
|
self.tk = passkey.to_bytes(16, byteorder='little')
|
||||||
logger.debug(f'TK from passkey = {self.tk.hex()}')
|
logger.debug(f'TK from passkey = {self.tk.hex()}')
|
||||||
|
|
||||||
|
self.passkey_ready.set()
|
||||||
|
|
||||||
if next_steps is not None:
|
if next_steps is not None:
|
||||||
next_steps()
|
next_steps()
|
||||||
|
|
||||||
@@ -910,17 +916,29 @@ class Session:
|
|||||||
logger.debug(f'generated random: {self.r.hex()}')
|
logger.debug(f'generated random: {self.r.hex()}')
|
||||||
|
|
||||||
if self.sc:
|
if self.sc:
|
||||||
if self.pairing_method in (self.JUST_WORKS, self.NUMERIC_COMPARISON):
|
|
||||||
z = 0
|
|
||||||
elif self.pairing_method == self.PASSKEY:
|
|
||||||
z = 0x80 + ((self.passkey >> self.passkey_step) & 1)
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.is_initiator:
|
async def next_steps():
|
||||||
confirm_value = crypto.f4(self.pka, self.pkb, self.r, bytes([z]))
|
if self.pairing_method in (self.JUST_WORKS, self.NUMERIC_COMPARISON):
|
||||||
else:
|
z = 0
|
||||||
confirm_value = crypto.f4(self.pkb, self.pka, self.r, bytes([z]))
|
elif self.pairing_method == self.PASSKEY:
|
||||||
|
# We need a passkey
|
||||||
|
await self.passkey_ready.wait()
|
||||||
|
|
||||||
|
z = 0x80 + ((self.passkey >> self.passkey_step) & 1)
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.is_initiator:
|
||||||
|
confirm_value = crypto.f4(self.pka, self.pkb, self.r, bytes([z]))
|
||||||
|
else:
|
||||||
|
confirm_value = crypto.f4(self.pkb, self.pka, self.r, bytes([z]))
|
||||||
|
|
||||||
|
self.send_command(
|
||||||
|
SMP_Pairing_Confirm_Command(confirm_value=confirm_value)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Perform the next steps asynchronously in case we need to wait for input
|
||||||
|
self.connection.abort_on('disconnection', next_steps())
|
||||||
else:
|
else:
|
||||||
confirm_value = crypto.c1(
|
confirm_value = crypto.c1(
|
||||||
self.tk,
|
self.tk,
|
||||||
@@ -933,7 +951,7 @@ class Session:
|
|||||||
self.ra,
|
self.ra,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.send_command(SMP_Pairing_Confirm_Command(confirm_value=confirm_value))
|
self.send_command(SMP_Pairing_Confirm_Command(confirm_value=confirm_value))
|
||||||
|
|
||||||
def send_pairing_random_command(self):
|
def send_pairing_random_command(self):
|
||||||
self.send_command(SMP_Pairing_Random_Command(random_value=self.r))
|
self.send_command(SMP_Pairing_Random_Command(random_value=self.r))
|
||||||
@@ -1364,8 +1382,8 @@ class Session:
|
|||||||
|
|
||||||
# Start phase 2
|
# Start phase 2
|
||||||
if self.sc:
|
if self.sc:
|
||||||
if self.pairing_method == self.PASSKEY and self.passkey_display:
|
if self.pairing_method == self.PASSKEY:
|
||||||
self.display_passkey()
|
self.display_or_input_passkey()
|
||||||
|
|
||||||
self.send_public_key_command()
|
self.send_public_key_command()
|
||||||
else:
|
else:
|
||||||
@@ -1426,18 +1444,22 @@ class Session:
|
|||||||
else:
|
else:
|
||||||
srand = self.r
|
srand = self.r
|
||||||
mrand = command.random_value
|
mrand = command.random_value
|
||||||
stk = crypto.s1(self.tk, srand, mrand)
|
self.stk = crypto.s1(self.tk, srand, mrand)
|
||||||
logger.debug(f'STK = {stk.hex()}')
|
logger.debug(f'STK = {self.stk.hex()}')
|
||||||
|
|
||||||
# Generate LTK
|
# Generate LTK
|
||||||
self.ltk = crypto.r()
|
self.ltk = crypto.r()
|
||||||
|
|
||||||
if self.is_initiator:
|
if self.is_initiator:
|
||||||
self.start_encryption(stk)
|
self.start_encryption(self.stk)
|
||||||
else:
|
else:
|
||||||
self.send_pairing_random_command()
|
self.send_pairing_random_command()
|
||||||
|
|
||||||
def on_smp_pairing_random_command_secure_connections(self, command):
|
def on_smp_pairing_random_command_secure_connections(self, command):
|
||||||
|
if self.pairing_method == self.PASSKEY and self.passkey is None:
|
||||||
|
logger.warning('no passkey entered, ignoring command')
|
||||||
|
return
|
||||||
|
|
||||||
# pylint: disable=too-many-return-statements
|
# pylint: disable=too-many-return-statements
|
||||||
if self.is_initiator:
|
if self.is_initiator:
|
||||||
if self.pairing_method in (self.JUST_WORKS, self.NUMERIC_COMPARISON):
|
if self.pairing_method in (self.JUST_WORKS, self.NUMERIC_COMPARISON):
|
||||||
@@ -1565,17 +1587,13 @@ class Session:
|
|||||||
logger.debug(f'DH key: {self.dh_key.hex()}')
|
logger.debug(f'DH key: {self.dh_key.hex()}')
|
||||||
|
|
||||||
if self.is_initiator:
|
if self.is_initiator:
|
||||||
if self.pairing_method == self.PASSKEY:
|
self.send_pairing_confirm_command()
|
||||||
if self.passkey_display:
|
|
||||||
self.send_pairing_confirm_command()
|
|
||||||
else:
|
|
||||||
self.input_passkey(self.send_pairing_confirm_command)
|
|
||||||
else:
|
else:
|
||||||
# Send our public key back to the initiator
|
|
||||||
if self.pairing_method == self.PASSKEY:
|
if self.pairing_method == self.PASSKEY:
|
||||||
self.display_or_input_passkey(self.send_public_key_command)
|
self.display_or_input_passkey()
|
||||||
else:
|
|
||||||
self.send_public_key_command()
|
# Send our public key back to the initiator
|
||||||
|
self.send_public_key_command()
|
||||||
|
|
||||||
if self.pairing_method in (self.JUST_WORKS, self.NUMERIC_COMPARISON):
|
if self.pairing_method in (self.JUST_WORKS, self.NUMERIC_COMPARISON):
|
||||||
# We can now send the confirmation value
|
# We can now send the confirmation value
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
# 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
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
from enum import IntEnum
|
||||||
|
import struct
|
||||||
|
import datetime
|
||||||
|
from typing import BinaryIO
|
||||||
|
|
||||||
|
from bumble.hci import HCI_Packet, HCI_COMMAND_PACKET, HCI_EVENT_PACKET
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Classes
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class Snooper:
|
||||||
|
"""
|
||||||
|
Base class for snooper implementations.
|
||||||
|
|
||||||
|
A snooper is an object that will be provided with HCI packets as they are
|
||||||
|
exchanged between a host and a controller.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Direction(IntEnum):
|
||||||
|
HOST_TO_CONTROLLER = 0
|
||||||
|
CONTROLLER_TO_HOST = 1
|
||||||
|
|
||||||
|
class DataLinkType(IntEnum):
|
||||||
|
H1 = 1001
|
||||||
|
H4 = 1002
|
||||||
|
HCI_BSCP = 1003
|
||||||
|
H5 = 1004
|
||||||
|
|
||||||
|
def snoop(self, hci_packet: HCI_Packet, direction: Direction) -> None:
|
||||||
|
"""Snoop on an HCI packet."""
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class BtSnooper(Snooper):
|
||||||
|
"""
|
||||||
|
Snooper that saves HCI packets using the BTSnoop format, based on RFC 1761.
|
||||||
|
"""
|
||||||
|
|
||||||
|
IDENTIFICATION_PATTERN = b'btsnoop\0'
|
||||||
|
TIMESTAMP_ANCHOR = datetime.datetime(2000, 1, 1)
|
||||||
|
TIMESTAMP_DELTA = 0x00E03AB44A676000
|
||||||
|
ONE_MS = datetime.timedelta(microseconds=1)
|
||||||
|
|
||||||
|
def __init__(self, output: BinaryIO):
|
||||||
|
self.output = output
|
||||||
|
|
||||||
|
# Write the header
|
||||||
|
self.output.write(
|
||||||
|
self.IDENTIFICATION_PATTERN + struct.pack('>LL', 1, self.DataLinkType.H4)
|
||||||
|
)
|
||||||
|
|
||||||
|
def snoop(self, hci_packet: HCI_Packet, direction: Snooper.Direction) -> None:
|
||||||
|
flags = int(direction)
|
||||||
|
if hci_packet.hci_packet_type in (HCI_EVENT_PACKET, HCI_COMMAND_PACKET):
|
||||||
|
flags |= 0x10
|
||||||
|
|
||||||
|
# Compute the current timestamp
|
||||||
|
timestamp = (
|
||||||
|
int((datetime.datetime.utcnow() - self.TIMESTAMP_ANCHOR) / self.ONE_MS)
|
||||||
|
+ self.TIMESTAMP_DELTA
|
||||||
|
)
|
||||||
|
|
||||||
|
# Emit the record
|
||||||
|
packet_data = bytes(hci_packet)
|
||||||
|
self.output.write(
|
||||||
|
struct.pack(
|
||||||
|
'>IIIIQ',
|
||||||
|
len(packet_data), # Original Length
|
||||||
|
len(packet_data), # Included Length
|
||||||
|
flags, # Packet Flags
|
||||||
|
0, # Cumulative Drops
|
||||||
|
timestamp, # Timestamp
|
||||||
|
)
|
||||||
|
+ packet_data
|
||||||
|
)
|
||||||
@@ -18,7 +18,6 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from .common import Transport, AsyncPipeSink
|
from .common import Transport, AsyncPipeSink
|
||||||
from ..link import RemoteLink
|
|
||||||
from ..controller import Controller
|
from ..controller import Controller
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -110,6 +109,8 @@ async def open_transport(name: str) -> Transport:
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
async def open_transport_or_link(name):
|
async def open_transport_or_link(name):
|
||||||
if name.startswith('link-relay:'):
|
if name.startswith('link-relay:'):
|
||||||
|
from ..link import RemoteLink # lazy import
|
||||||
|
|
||||||
link = RemoteLink(name[11:])
|
link = RemoteLink(name[11:])
|
||||||
await link.wait_until_connected()
|
await link.wait_until_connected()
|
||||||
controller = Controller('remote', link=link)
|
controller = Controller('remote', link=link)
|
||||||
|
|||||||
@@ -18,9 +18,9 @@
|
|||||||
import struct
|
import struct
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from colors import color
|
|
||||||
|
|
||||||
from .. import hci
|
from .. import hci
|
||||||
|
from ..colors import color
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -22,10 +22,10 @@ import time
|
|||||||
|
|
||||||
import usb.core
|
import usb.core
|
||||||
import usb.util
|
import usb.util
|
||||||
from colors import color
|
|
||||||
|
|
||||||
from .common import Transport, ParserSource
|
from .common import Transport, ParserSource
|
||||||
from .. import hci
|
from .. import hci
|
||||||
|
from ..colors import color
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -23,10 +23,10 @@ import ctypes
|
|||||||
import platform
|
import platform
|
||||||
|
|
||||||
import usb1
|
import usb1
|
||||||
from colors import color
|
|
||||||
|
|
||||||
from .common import Transport, ParserSource
|
from .common import Transport, ParserSource
|
||||||
from .. import hci
|
from .. import hci
|
||||||
|
from ..colors import color
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|||||||
+20
-2
@@ -20,11 +20,11 @@ import logging
|
|||||||
import traceback
|
import traceback
|
||||||
import collections
|
import collections
|
||||||
import sys
|
import sys
|
||||||
from typing import Awaitable, TypeVar
|
from typing import Awaitable, Set, TypeVar
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from colors import color
|
|
||||||
from pyee import EventEmitter
|
from pyee import EventEmitter
|
||||||
|
|
||||||
|
from .colors import color
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Logging
|
# Logging
|
||||||
@@ -78,6 +78,8 @@ class AbortableEventEmitter(EventEmitter):
|
|||||||
return future
|
return future
|
||||||
|
|
||||||
def on_event(*_):
|
def on_event(*_):
|
||||||
|
if future.done():
|
||||||
|
return
|
||||||
msg = f'abort: {event} event occurred.'
|
msg = f'abort: {event} event occurred.'
|
||||||
if isinstance(future, asyncio.Task):
|
if isinstance(future, asyncio.Task):
|
||||||
# python < 3.9 does not support passing a message on `Task.cancel`
|
# python < 3.9 does not support passing a message on `Task.cancel`
|
||||||
@@ -155,6 +157,9 @@ class AsyncRunner:
|
|||||||
# Shared default queue
|
# Shared default queue
|
||||||
default_queue = WorkQueue()
|
default_queue = WorkQueue()
|
||||||
|
|
||||||
|
# Shared set of running tasks
|
||||||
|
running_tasks: Set[Awaitable] = set()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def run_in_task(queue=None):
|
def run_in_task(queue=None):
|
||||||
"""
|
"""
|
||||||
@@ -185,6 +190,19 @@ class AsyncRunner:
|
|||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def spawn(coroutine):
|
||||||
|
"""
|
||||||
|
Spawn a task to run a coroutine in a "fire and forget" mode.
|
||||||
|
|
||||||
|
Using this method instead of just calling `asyncio.create_task(coroutine)`
|
||||||
|
is necessary when you don't keep a reference to the task, because `asyncio`
|
||||||
|
only keeps weak references to alive tasks.
|
||||||
|
"""
|
||||||
|
task = asyncio.create_task(coroutine)
|
||||||
|
AsyncRunner.running_tasks.add(task)
|
||||||
|
task.add_done_callback(AsyncRunner.running_tasks.remove)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class FlowControlAsyncPipe:
|
class FlowControlAsyncPipe:
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ nav:
|
|||||||
- Apps & Tools:
|
- Apps & Tools:
|
||||||
- Overview: apps_and_tools/index.md
|
- Overview: apps_and_tools/index.md
|
||||||
- Console: apps_and_tools/console.md
|
- Console: apps_and_tools/console.md
|
||||||
- Link Relay: apps_and_tools/link_relay.md
|
- Bench: apps_and_tools/bench.md
|
||||||
- HCI Bridge: apps_and_tools/hci_bridge.md
|
- HCI Bridge: apps_and_tools/hci_bridge.md
|
||||||
- Golden Gate Bridge: apps_and_tools/gg_bridge.md
|
- Golden Gate Bridge: apps_and_tools/gg_bridge.md
|
||||||
- Show: apps_and_tools/show.md
|
- Show: apps_and_tools/show.md
|
||||||
@@ -51,6 +51,7 @@ nav:
|
|||||||
- Pair: apps_and_tools/pair.md
|
- Pair: apps_and_tools/pair.md
|
||||||
- Unbond: apps_and_tools/unbond.md
|
- Unbond: apps_and_tools/unbond.md
|
||||||
- USB Probe: apps_and_tools/usb_probe.md
|
- USB Probe: apps_and_tools/usb_probe.md
|
||||||
|
- Link Relay: apps_and_tools/link_relay.md
|
||||||
- Hardware:
|
- Hardware:
|
||||||
- Overview: hardware/index.md
|
- Overview: hardware/index.md
|
||||||
- Platforms:
|
- Platforms:
|
||||||
@@ -62,7 +63,7 @@ nav:
|
|||||||
- Examples:
|
- Examples:
|
||||||
- Overview: examples/index.md
|
- Overview: examples/index.md
|
||||||
|
|
||||||
copyright: Copyright 2021-2022 Google LLC
|
copyright: Copyright 2021-2023 Google LLC
|
||||||
|
|
||||||
theme:
|
theme:
|
||||||
name: 'material'
|
name: 'material'
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
BENCH TOOL
|
||||||
|
==========
|
||||||
|
|
||||||
|
The "bench" tool implements a number of different ways of measuring the
|
||||||
|
throughput and/or latency between two devices.
|
||||||
|
|
||||||
|
# General Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
Usage: bench.py [OPTIONS] COMMAND [ARGS]...
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--device-config FILENAME Device configuration file
|
||||||
|
--role [sender|receiver|ping|pong]
|
||||||
|
--mode [gatt-client|gatt-server|l2cap-client|l2cap-server|rfcomm-client|rfcomm-server]
|
||||||
|
--att-mtu MTU GATT MTU (gatt-client mode) [23<=x<=517]
|
||||||
|
-s, --packet-size SIZE Packet size (server role) [8<=x<=4096]
|
||||||
|
-c, --packet-count COUNT Packet count (server role)
|
||||||
|
-sd, --start-delay SECONDS Start delay (server role)
|
||||||
|
--help Show this message and exit.
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
central Run as a central (initiates the connection)
|
||||||
|
peripheral Run as a peripheral (waits for a connection)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options for the ``central`` Command
|
||||||
|
```
|
||||||
|
Usage: bumble-bench central [OPTIONS] TRANSPORT
|
||||||
|
|
||||||
|
Run as a central (initiates the connection)
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--peripheral ADDRESS_OR_NAME Address or name to connect to
|
||||||
|
--connection-interval, --ci CONNECTION_INTERVAL
|
||||||
|
Connection interval (in ms)
|
||||||
|
--phy [1m|2m|coded] PHY to use
|
||||||
|
--help Show this message and exit.
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
To test once device against another, one of the two devices must be running
|
||||||
|
the ``peripheral`` command and the other the ``central`` command. The device
|
||||||
|
running the ``peripheral`` command will accept connections from the device
|
||||||
|
running the ``central`` command.
|
||||||
|
When using Bluetooth LE (all modes except for ``rfcomm-server`` and ``rfcomm-client``utils),
|
||||||
|
the default addresses configured in the tool should be sufficient. But when using
|
||||||
|
Bluetooth Classic, the address of the Peripheral must be specified on the Central
|
||||||
|
using the ``--peripheral`` option. The address will be printed by the Peripheral when
|
||||||
|
it starts.
|
||||||
|
|
||||||
|
Independently of whether the device is the Central or Peripheral, each device selects a
|
||||||
|
``mode`` and and ``role`` to run as. The ``mode`` and ``role`` of the Central and Peripheral
|
||||||
|
must be compatible.
|
||||||
|
|
||||||
|
Device 1 mode | Device 2 mode
|
||||||
|
------------------|------------------
|
||||||
|
``gatt-client`` | ``gatt-server``
|
||||||
|
``l2cap-client`` | ``l2cap-server``
|
||||||
|
``rfcomm-client`` | ``rfcomm-server``
|
||||||
|
|
||||||
|
Device 1 role | Device 2 role
|
||||||
|
--------------|--------------
|
||||||
|
``sender`` | ``receiver``
|
||||||
|
``ping`` | ``pong``
|
||||||
|
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
|
||||||
|
In the following examples, we have two USB Bluetooth controllers, one on `usb:0` and
|
||||||
|
the other on `usb:1`, and two consoles/terminals. We will run a command in each.
|
||||||
|
|
||||||
|
!!! example "GATT Throughput"
|
||||||
|
Using the default mode and role for the Central and Peripheral.
|
||||||
|
|
||||||
|
In the first console/terminal:
|
||||||
|
```
|
||||||
|
$ bumble-bench peripheral usb:0
|
||||||
|
```
|
||||||
|
|
||||||
|
In the second console/terminal:
|
||||||
|
```
|
||||||
|
$ bumble-bench central usb:1
|
||||||
|
```
|
||||||
|
|
||||||
|
In this default configuration, the Central runs a Sender, as a GATT client,
|
||||||
|
connecting to the Peripheral running a Receiver, as a GATT server.
|
||||||
|
|
||||||
|
!!! example "L2CAP Throughput"
|
||||||
|
In the first console/terminal:
|
||||||
|
```
|
||||||
|
$ bumble-bench --mode l2cap-server peripheral usb:0
|
||||||
|
```
|
||||||
|
|
||||||
|
In the second console/terminal:
|
||||||
|
```
|
||||||
|
$ bumble-bench --mode l2cap-client central usb:1
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! example "RFComm Throughput"
|
||||||
|
In the first console/terminal:
|
||||||
|
```
|
||||||
|
$ bumble-bench --mode rfcomm-server peripheral usb:0
|
||||||
|
```
|
||||||
|
|
||||||
|
NOTE: the BT address of the Peripheral will be printed out, use it with the
|
||||||
|
``--peripheral`` option for the Central.
|
||||||
|
|
||||||
|
In this example, we use a larger packet size and packet count than the default.
|
||||||
|
|
||||||
|
In the second console/terminal:
|
||||||
|
```
|
||||||
|
$ bumble-bench --mode rfcomm-client --packet-size 2000 --packet-count 100 central --peripheral 00:16:A4:5A:40:F2 usb:1
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! example "Ping/Pong Latency"
|
||||||
|
In the first console/terminal:
|
||||||
|
```
|
||||||
|
$ bumble-bench --role pong peripheral usb:0
|
||||||
|
```
|
||||||
|
|
||||||
|
In the second console/terminal:
|
||||||
|
```
|
||||||
|
$ bumble-bench --role ping central usb:1
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! example "Reversed modes with GATT and custom connection interval"
|
||||||
|
In the first console/terminal:
|
||||||
|
```
|
||||||
|
$ bumble-bench --mode gatt-client peripheral usb:0
|
||||||
|
```
|
||||||
|
|
||||||
|
In the second console/terminal:
|
||||||
|
```
|
||||||
|
$ bumble-bench --mode gatt-server central --ci 10 usb:1
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! example "Reversed modes with L2CAP and custom PHY"
|
||||||
|
In the first console/terminal:
|
||||||
|
```
|
||||||
|
$ bumble-bench --mode l2cap-client peripheral usb:0
|
||||||
|
```
|
||||||
|
|
||||||
|
In the second console/terminal:
|
||||||
|
```
|
||||||
|
$ bumble-bench --mode l2cap-server central --phy 2m usb:1
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! example "Reversed roles with L2CAP"
|
||||||
|
In the first console/terminal:
|
||||||
|
```
|
||||||
|
$ bumble-bench --mode l2cap-client --role sender peripheral usb:0
|
||||||
|
```
|
||||||
|
|
||||||
|
In the second console/terminal:
|
||||||
|
```
|
||||||
|
$ bumble-bench --mode l2cap-server --role receiver central usb:1
|
||||||
|
```
|
||||||
@@ -5,6 +5,7 @@ Included in the project are a few apps and tools, built on top of the core libra
|
|||||||
These include:
|
These include:
|
||||||
|
|
||||||
* [Console](console.md) - an interactive text-based console
|
* [Console](console.md) - an interactive text-based console
|
||||||
|
* [Bench](bench.md) - Speed and Latency benchmarking between two devices (LE and Classic)
|
||||||
* [Pair](pair.md) - Pair/bond two devices (LE and Classic)
|
* [Pair](pair.md) - Pair/bond two devices (LE and Classic)
|
||||||
* [Unbond](unbond.md) - Remove a previously established bond
|
* [Unbond](unbond.md) - Remove a previously established bond
|
||||||
* [HCI Bridge](hci_bridge.md) - a HCI transport bridge to connect two HCI transports and filter/snoop the HCI packets
|
* [HCI Bridge](hci_bridge.md) - a HCI transport bridge to connect two HCI transports and filter/snoop the HCI packets
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ The project initially only supported BLE (Bluetooth Low Energy), but support for
|
|||||||
eventually added. Support for BLE is therefore currently somewhat more advanced than for Classic.
|
eventually added. Support for BLE is therefore currently somewhat more advanced than for Classic.
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
This project is still very much experimental and in an alpha state where a lot of things are still missing or broken, and what's there changes frequently.
|
This project is still in an early state of development where some things are still missing or broken, and what's implemented may change and evolve frequently.
|
||||||
Also, there are still a few hardcoded values/parameters in some of the examples and apps which need to be changed (those will eventually be command line arguments, as appropriate)
|
|
||||||
|
|
||||||
Overview
|
Overview
|
||||||
--------
|
--------
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import asyncio
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
from colors import color
|
from bumble.colors import color
|
||||||
from bumble.device import Device
|
from bumble.device import Device
|
||||||
from bumble.transport import open_transport
|
from bumble.transport import open_transport
|
||||||
from bumble.profiles.battery_service import BatteryServiceProxy
|
from bumble.profiles.battery_service import BatteryServiceProxy
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import asyncio
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
from colors import color
|
from bumble.colors import color
|
||||||
from bumble.device import Device, Peer
|
from bumble.device import Device, Peer
|
||||||
from bumble.profiles.device_information_service import DeviceInformationServiceProxy
|
from bumble.profiles.device_information_service import DeviceInformationServiceProxy
|
||||||
from bumble.transport import open_transport
|
from bumble.transport import open_transport
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import asyncio
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
from colors import color
|
from bumble.colors import color
|
||||||
from bumble.device import Device
|
from bumble.device import Device
|
||||||
from bumble.transport import open_transport
|
from bumble.transport import open_transport
|
||||||
from bumble.profiles.heart_rate_service import HeartRateServiceProxy
|
from bumble.profiles.heart_rate_service import HeartRateServiceProxy
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import logging
|
|||||||
import struct
|
import struct
|
||||||
import json
|
import json
|
||||||
import websockets
|
import websockets
|
||||||
from colors import color
|
from bumble.colors import color
|
||||||
|
|
||||||
from bumble.core import AdvertisingData
|
from bumble.core import AdvertisingData
|
||||||
from bumble.device import Device, Connection, Peer
|
from bumble.device import Device, Connection, Peer
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import sys
|
|||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from colors import color
|
from bumble.colors import color
|
||||||
from bumble.device import Device
|
from bumble.device import Device
|
||||||
from bumble.transport import open_transport_or_link
|
from bumble.transport import open_transport_or_link
|
||||||
from bumble.core import (
|
from bumble.core import (
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import sys
|
|||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from colors import color
|
from bumble.colors import color
|
||||||
from bumble.device import Device
|
from bumble.device import Device
|
||||||
from bumble.transport import open_transport_or_link
|
from bumble.transport import open_transport_or_link
|
||||||
from bumble.core import BT_BR_EDR_TRANSPORT
|
from bumble.core import BT_BR_EDR_TRANSPORT
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import asyncio
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
from colors import color
|
from bumble.colors import color
|
||||||
|
|
||||||
from bumble.device import Device
|
from bumble.device import Device
|
||||||
from bumble.transport import open_transport_or_link
|
from bumble.transport import open_transport_or_link
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import asyncio
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
from colors import color
|
from bumble.colors import color
|
||||||
|
|
||||||
from bumble.device import Device
|
from bumble.device import Device
|
||||||
from bumble.transport import open_transport_or_link
|
from bumble.transport import open_transport_or_link
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import logging
|
|||||||
import asyncio
|
import asyncio
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
from colors import color
|
from bumble.colors import color
|
||||||
|
|
||||||
from bumble.device import Device
|
from bumble.device import Device
|
||||||
from bumble.controller import Controller
|
from bumble.controller import Controller
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# 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 sys
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from bumble.colors import color
|
||||||
|
|
||||||
|
from bumble.device import Device
|
||||||
|
from bumble.transport import open_transport_or_link
|
||||||
|
from bumble.snoop import BtSnooper
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
async def main():
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
print('Usage: run_device_with_snooper.py <transport-spec> <snoop-file>')
|
||||||
|
print('example: run_device_with_snooper.py usb:0 btsnoop.log')
|
||||||
|
return
|
||||||
|
|
||||||
|
print('<<< connecting to HCI...')
|
||||||
|
async with await open_transport_or_link(sys.argv[1]) as (hci_source, hci_sink):
|
||||||
|
print('<<< connected')
|
||||||
|
|
||||||
|
device = Device.with_hci('Bumble', 'F0:F1:F2:F3:F4:F5', hci_source, hci_sink)
|
||||||
|
|
||||||
|
with open(sys.argv[2], "wb") as snoop_file:
|
||||||
|
device.host.snooper = BtSnooper(snoop_file)
|
||||||
|
await device.power_on()
|
||||||
|
await device.start_scanning()
|
||||||
|
|
||||||
|
await hci_source.wait_for_termination()
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
|
||||||
|
asyncio.run(main())
|
||||||
@@ -19,7 +19,7 @@ import asyncio
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
from colors import color
|
from bumble.colors import color
|
||||||
|
|
||||||
from bumble.core import ProtocolError
|
from bumble.core import ProtocolError
|
||||||
from bumble.device import Device, Peer
|
from bumble.device import Device, Peer
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
from colors import color
|
from bumble.colors import color
|
||||||
|
|
||||||
from bumble.core import ProtocolError
|
from bumble.core import ProtocolError
|
||||||
from bumble.controller import Controller
|
from bumble.controller import Controller
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import sys
|
|||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from colors import color
|
from bumble.colors import color
|
||||||
|
|
||||||
import bumble.core
|
import bumble.core
|
||||||
from bumble.device import Device
|
from bumble.device import Device
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import sys
|
|||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from colors import color
|
from bumble.colors import color
|
||||||
|
|
||||||
import bumble.core
|
import bumble.core
|
||||||
from bumble.device import Device
|
from bumble.device import Device
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import asyncio
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
from colors import color
|
from bumble.colors import color
|
||||||
|
|
||||||
from bumble.device import Device
|
from bumble.device import Device
|
||||||
from bumble.transport import open_transport_or_link
|
from bumble.transport import open_transport_or_link
|
||||||
|
|||||||
@@ -30,11 +30,8 @@ package_dir =
|
|||||||
bumble.apps = apps
|
bumble.apps = apps
|
||||||
include-package-data = True
|
include-package-data = True
|
||||||
install_requires =
|
install_requires =
|
||||||
aioconsole >= 0.4.1
|
|
||||||
ansicolors >= 1.1
|
|
||||||
appdirs >= 1.4
|
appdirs >= 1.4
|
||||||
click >= 7.1.2; platform_system!='Emscripten'
|
click >= 7.1.2; platform_system!='Emscripten'
|
||||||
construct >= 2.10
|
|
||||||
cryptography == 35; platform_system!='Emscripten'
|
cryptography == 35; platform_system!='Emscripten'
|
||||||
grpcio >= 1.46; platform_system!='Emscripten'
|
grpcio >= 1.46; platform_system!='Emscripten'
|
||||||
libusb1 >= 2.0.1; platform_system!='Emscripten'
|
libusb1 >= 2.0.1; platform_system!='Emscripten'
|
||||||
@@ -60,6 +57,7 @@ console_scripts =
|
|||||||
bumble-unbond = bumble.apps.unbond:main
|
bumble-unbond = bumble.apps.unbond:main
|
||||||
bumble-usb-probe = bumble.apps.usb_probe:main
|
bumble-usb-probe = bumble.apps.usb_probe:main
|
||||||
bumble-link-relay = bumble.apps.link_relay.link_relay:main
|
bumble-link-relay = bumble.apps.link_relay.link_relay:main
|
||||||
|
bumble-bench = bumble.apps.bench:main
|
||||||
|
|
||||||
[options.package_data]
|
[options.package_data]
|
||||||
* = py.typed, *.pyi
|
* = py.typed, *.pyi
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ async def test_device_connect_parallel():
|
|||||||
d1.host.set_packet_sink(Sink(d1_flow()))
|
d1.host.set_packet_sink(Sink(d1_flow()))
|
||||||
d2.host.set_packet_sink(Sink(d2_flow()))
|
d2.host.set_packet_sink(Sink(d2_flow()))
|
||||||
|
|
||||||
[c01, c02, a10, a20, a01] = await asyncio.gather(
|
[c01, c02, a10, a20] = await asyncio.gather(
|
||||||
*[
|
*[
|
||||||
asyncio.create_task(
|
asyncio.create_task(
|
||||||
d0.connect(d1.public_address, transport=BT_BR_EDR_TRANSPORT)
|
d0.connect(d1.public_address, transport=BT_BR_EDR_TRANSPORT)
|
||||||
@@ -207,7 +207,6 @@ async def test_device_connect_parallel():
|
|||||||
),
|
),
|
||||||
asyncio.create_task(d1.accept(peer_address=d0.public_address)),
|
asyncio.create_task(d1.accept(peer_address=d0.public_address)),
|
||||||
asyncio.create_task(d2.accept()),
|
asyncio.create_task(d2.accept()),
|
||||||
asyncio.create_task(d0.accept(peer_address=d1.public_address)),
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -215,11 +214,9 @@ async def test_device_connect_parallel():
|
|||||||
assert type(c02) == Connection
|
assert type(c02) == Connection
|
||||||
assert type(a10) == Connection
|
assert type(a10) == Connection
|
||||||
assert type(a20) == Connection
|
assert type(a20) == Connection
|
||||||
assert type(a01) == Connection
|
|
||||||
|
|
||||||
assert c01.handle == a10.handle and c01.handle == 0x100
|
assert c01.handle == a10.handle and c01.handle == 0x100
|
||||||
assert c02.handle == a20.handle and c02.handle == 0x101
|
assert c02.handle == a20.handle and c02.handle == 0x101
|
||||||
assert a01 == c01
|
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|||||||
+12
-5
@@ -32,7 +32,6 @@ from bumble.smp import (
|
|||||||
PairingDelegate,
|
PairingDelegate,
|
||||||
SMP_PAIRING_NOT_SUPPORTED_ERROR,
|
SMP_PAIRING_NOT_SUPPORTED_ERROR,
|
||||||
SMP_CONFIRM_VALUE_FAILED_ERROR,
|
SMP_CONFIRM_VALUE_FAILED_ERROR,
|
||||||
SMP_ID_KEY_DISTRIBUTION_FLAG,
|
|
||||||
)
|
)
|
||||||
from bumble.core import ProtocolError
|
from bumble.core import ProtocolError
|
||||||
|
|
||||||
@@ -273,9 +272,15 @@ KEY_DIST = range(16)
|
|||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
'io_cap, sc, mitm, key_dist', itertools.product(IO_CAP, SC, MITM, KEY_DIST)
|
'io_caps, sc, mitm, key_dist',
|
||||||
|
itertools.chain(
|
||||||
|
itertools.product([IO_CAP], SC, MITM, [15]),
|
||||||
|
itertools.product(
|
||||||
|
[[PairingDelegate.DISPLAY_OUTPUT_AND_KEYBOARD_INPUT]], SC, MITM, KEY_DIST
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
async def test_self_smp(io_cap, sc, mitm, key_dist):
|
async def test_self_smp(io_caps, sc, mitm, key_dist):
|
||||||
class Delegate(PairingDelegate):
|
class Delegate(PairingDelegate):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -296,6 +301,7 @@ async def test_self_smp(io_cap, sc, mitm, key_dist):
|
|||||||
self.peer_delegate = None
|
self.peer_delegate = None
|
||||||
self.number = asyncio.get_running_loop().create_future()
|
self.number = asyncio.get_running_loop().create_future()
|
||||||
|
|
||||||
|
# pylint: disable-next=unused-argument
|
||||||
async def compare_numbers(self, number, digits):
|
async def compare_numbers(self, number, digits):
|
||||||
if self.peer_delegate is None:
|
if self.peer_delegate is None:
|
||||||
logger.warning(f'[{self.name}] no peer delegate')
|
logger.warning(f'[{self.name}] no peer delegate')
|
||||||
@@ -331,8 +337,9 @@ async def test_self_smp(io_cap, sc, mitm, key_dist):
|
|||||||
|
|
||||||
pairing_config_sets = [('Initiator', [None]), ('Responder', [None])]
|
pairing_config_sets = [('Initiator', [None]), ('Responder', [None])]
|
||||||
for pairing_config_set in pairing_config_sets:
|
for pairing_config_set in pairing_config_sets:
|
||||||
delegate = Delegate(pairing_config_set[0], io_cap, key_dist, key_dist)
|
for io_cap in io_caps:
|
||||||
pairing_config_set[1].append(PairingConfig(sc, mitm, True, delegate))
|
delegate = Delegate(pairing_config_set[0], io_cap, key_dist, key_dist)
|
||||||
|
pairing_config_set[1].append(PairingConfig(sc, mitm, True, delegate))
|
||||||
|
|
||||||
for pairing_config1 in pairing_config_sets[0][1]:
|
for pairing_config1 in pairing_config_sets[0][1]:
|
||||||
for pairing_config2 in pairing_config_sets[1][1]:
|
for pairing_config2 in pairing_config_sets[1][1]:
|
||||||
|
|||||||
Reference in New Issue
Block a user