forked from auracaster/bumble_mirror
Compare commits
15 Commits
gbg/usb-se
...
v0.0.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d14df909c | ||
|
|
153788afe3 | ||
|
|
99ca31c063 | ||
|
|
9629e677f2 | ||
|
|
250c1e3395 | ||
|
|
70dca1d7c9 | ||
|
|
a5015c1305 | ||
|
|
6e22df4838 | ||
|
|
b4e2f21d2a | ||
|
|
1af61e8af3 | ||
|
|
e11119c565 | ||
|
|
b1a31564ef | ||
|
|
a9bd77e6ee | ||
|
|
d6b426eeec | ||
|
|
884315ae00 |
10
.github/workflows/python-build-test.yml
vendored
10
.github/workflows/python-build-test.yml
vendored
@@ -24,11 +24,11 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install build
|
||||
pip install ".[test]"
|
||||
- name: Build package
|
||||
run: |
|
||||
python -m build
|
||||
python -m pip install ".[test,development,documentation]"
|
||||
- name: Test with pytest
|
||||
run: |
|
||||
pytest
|
||||
- name: Build
|
||||
run: |
|
||||
inv build
|
||||
inv mkdocs
|
||||
|
||||
@@ -11,6 +11,8 @@ Bluetooth Stack for Apps, Emulation, Test and Experimentation
|
||||
|
||||
<img src="docs/mkdocs/src/images/logo_framed.png" alt="drawing" width="200" height="200"/>
|
||||
|
||||
Bumble is a full-featured Bluetooth stack written entirely in Python. It supports most of the common Bluetooth Low Energy (BLE) and Bluetooth Classic (BR/EDR) protocols and profiles, including GAP, L2CAP, ATT, GATT, SMP, SDP, RFCOMM, HFP, HID and A2DP. The stack can be used with physical radios via HCI over USB, UART, or the Linux VHCI, as well as virtual radios, including the virtual Bluetooth support of the Android emulator.
|
||||
|
||||
## Documentation
|
||||
|
||||
Browse the pre-built [Online Documentation](https://google.github.io/bumble/),
|
||||
|
||||
97
apps/pair.py
97
apps/pair.py
@@ -44,7 +44,7 @@ from bumble.att import (
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Delegate(PairingDelegate):
|
||||
def __init__(self, connection, capability_string, prompt):
|
||||
def __init__(self, mode, connection, capability_string, prompt):
|
||||
super().__init__({
|
||||
'keyboard': PairingDelegate.KEYBOARD_INPUT_ONLY,
|
||||
'display': PairingDelegate.DISPLAY_OUTPUT_ONLY,
|
||||
@@ -53,6 +53,7 @@ class Delegate(PairingDelegate):
|
||||
'none': PairingDelegate.NO_OUTPUT_NO_INPUT
|
||||
}[capability_string.lower()])
|
||||
|
||||
self.mode = mode
|
||||
self.peer = Peer(connection)
|
||||
self.peer_name = None
|
||||
self.prompt = prompt
|
||||
@@ -64,7 +65,7 @@ class Delegate(PairingDelegate):
|
||||
|
||||
# Try to get the peer's name
|
||||
if self.peer:
|
||||
peer_name = await get_peer_name(self.peer)
|
||||
peer_name = await get_peer_name(self.peer, self.mode)
|
||||
self.peer_name = f'{peer_name or ""} [{self.peer.connection.peer_address}]'
|
||||
else:
|
||||
self.peer_name = '[?]'
|
||||
@@ -91,7 +92,7 @@ class Delegate(PairingDelegate):
|
||||
# Accept silently
|
||||
return True
|
||||
|
||||
async def compare_numbers(self, number):
|
||||
async def compare_numbers(self, number, digits):
|
||||
await self.update_peer_name()
|
||||
|
||||
# Wait a bit to allow some of the log lines to print before we prompt
|
||||
@@ -102,7 +103,7 @@ class Delegate(PairingDelegate):
|
||||
print(color(f'### Pairing with {self.peer_name}', 'yellow'))
|
||||
print(color('###-----------------------------------', 'yellow'))
|
||||
while True:
|
||||
response = await aioconsole.ainput(color(f'>>> Does the other device display {number:06}? ', 'yellow'))
|
||||
response = await aioconsole.ainput(color(f'>>> Does the other device display {number:0{digits}}? ', 'yellow'))
|
||||
response = response.lower().strip()
|
||||
if response == 'yes':
|
||||
return True
|
||||
@@ -125,7 +126,7 @@ class Delegate(PairingDelegate):
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
async def display_number(self, number):
|
||||
async def display_number(self, number, digits):
|
||||
await self.update_peer_name()
|
||||
|
||||
# Wait a bit to allow some of the log lines to print before we prompt
|
||||
@@ -134,19 +135,23 @@ class Delegate(PairingDelegate):
|
||||
# Display a PIN code
|
||||
print(color('###-----------------------------------', 'yellow'))
|
||||
print(color(f'### Pairing with {self.peer_name}', 'yellow'))
|
||||
print(color(f'### PIN: {number:06}', 'yellow'))
|
||||
print(color(f'### PIN: {number:0{digits}}', 'yellow'))
|
||||
print(color('###-----------------------------------', 'yellow'))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def get_peer_name(peer):
|
||||
services = await peer.discover_service(GATT_GENERIC_ACCESS_SERVICE)
|
||||
if not services:
|
||||
return None
|
||||
async def get_peer_name(peer, mode):
|
||||
if mode == 'classic':
|
||||
return await peer.request_name()
|
||||
else:
|
||||
# Try to get the peer name from GATT
|
||||
services = await peer.discover_service(GATT_GENERIC_ACCESS_SERVICE)
|
||||
if not services:
|
||||
return None
|
||||
|
||||
values = await peer.read_characteristics_by_uuid(GATT_DEVICE_NAME_CHARACTERISTIC, services[0])
|
||||
if values:
|
||||
return values[0].decode('utf-8')
|
||||
values = await peer.read_characteristics_by_uuid(GATT_DEVICE_NAME_CHARACTERISTIC, services[0])
|
||||
if values:
|
||||
return values[0].decode('utf-8')
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -224,9 +229,22 @@ def on_pairing_failure(reason):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def pair(sc, mitm, bond, io, prompt, request, print_keys, keystore_file, device_config, transport, address_or_name):
|
||||
async def pair(
|
||||
mode,
|
||||
sc,
|
||||
mitm,
|
||||
bond,
|
||||
io,
|
||||
prompt,
|
||||
request,
|
||||
print_keys,
|
||||
keystore_file,
|
||||
device_config,
|
||||
hci_transport,
|
||||
address_or_name
|
||||
):
|
||||
print('<<< connecting to HCI...')
|
||||
async with await open_transport_or_link(transport) as (hci_source, hci_sink):
|
||||
async with await open_transport_or_link(hci_transport) as (hci_source, hci_sink):
|
||||
print('<<< connected')
|
||||
|
||||
# Create a device to manage the host
|
||||
@@ -245,19 +263,25 @@ async def pair(sc, mitm, bond, io, prompt, request, print_keys, keystore_file, d
|
||||
|
||||
# Expose a GATT characteristic that can be used to trigger pairing by
|
||||
# responding with an authentication error when read
|
||||
device.add_service(
|
||||
Service(
|
||||
'50DB505C-8AC4-4738-8448-3B1D9CC09CC5',
|
||||
[
|
||||
Characteristic(
|
||||
'552957FB-CF1F-4A31-9535-E78847E1A714',
|
||||
Characteristic.READ | Characteristic.WRITE,
|
||||
Characteristic.READABLE | Characteristic.WRITEABLE,
|
||||
CharacteristicValue(read=read_with_error, write=write_with_error)
|
||||
)
|
||||
]
|
||||
if mode == 'le':
|
||||
device.add_service(
|
||||
Service(
|
||||
'50DB505C-8AC4-4738-8448-3B1D9CC09CC5',
|
||||
[
|
||||
Characteristic(
|
||||
'552957FB-CF1F-4A31-9535-E78847E1A714',
|
||||
Characteristic.READ | Characteristic.WRITE,
|
||||
Characteristic.READABLE | Characteristic.WRITEABLE,
|
||||
CharacteristicValue(read=read_with_error, write=write_with_error)
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# Select LE or Classic
|
||||
if mode == 'classic':
|
||||
device.classic_enabled = True
|
||||
device.le_enabled = False
|
||||
|
||||
# Get things going
|
||||
await device.power_on()
|
||||
@@ -267,7 +291,7 @@ async def pair(sc, mitm, bond, io, prompt, request, print_keys, keystore_file, d
|
||||
sc,
|
||||
mitm,
|
||||
bond,
|
||||
Delegate(connection, io, prompt)
|
||||
Delegate(mode, connection, io, prompt)
|
||||
)
|
||||
|
||||
# Connect to a peer or wait for a connection
|
||||
@@ -278,10 +302,14 @@ async def pair(sc, mitm, bond, io, prompt, request, print_keys, keystore_file, d
|
||||
|
||||
if not request:
|
||||
try:
|
||||
await connection.pair()
|
||||
if mode == 'le':
|
||||
await connection.pair()
|
||||
else:
|
||||
await connection.authenticate()
|
||||
return
|
||||
except ProtocolError as error:
|
||||
print(color(f'Pairing failed: {error}', 'red'))
|
||||
return
|
||||
except ProtocolError:
|
||||
pass
|
||||
else:
|
||||
# Advertise so that peers can find us and connect
|
||||
await device.start_advertising(auto_restart=True)
|
||||
@@ -291,6 +319,7 @@ async def pair(sc, mitm, bond, io, prompt, request, print_keys, keystore_file, d
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@click.command()
|
||||
@click.option('--mode', type=click.Choice(['le', 'classic']), default='le', show_default=True)
|
||||
@click.option('--sc', type=bool, default=True, help='Use the Secure Connections protocol', show_default=True)
|
||||
@click.option('--mitm', type=bool, default=True, help='Request MITM protection', show_default=True)
|
||||
@click.option('--bond', type=bool, default=True, help='Enable bonding', show_default=True)
|
||||
@@ -300,11 +329,11 @@ async def pair(sc, mitm, bond, io, prompt, request, print_keys, keystore_file, d
|
||||
@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.argument('device-config')
|
||||
@click.argument('transport')
|
||||
@click.argument('hci_transport')
|
||||
@click.argument('address-or-name', required=False)
|
||||
def main(sc, mitm, bond, io, prompt, request, print_keys, keystore_file, device_config, transport, address_or_name):
|
||||
def main(mode, sc, mitm, bond, io, prompt, request, print_keys, keystore_file, device_config, hci_transport, address_or_name):
|
||||
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||
asyncio.run(pair(sc, mitm, bond, io, prompt, request, print_keys, keystore_file, device_config, transport, address_or_name))
|
||||
asyncio.run(pair(mode, sc, mitm, bond, io, prompt, request, print_keys, keystore_file, device_config, hci_transport, address_or_name))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
219
bumble/device.py
219
bumble/device.py
@@ -137,6 +137,10 @@ class Peer:
|
||||
def get_characteristics_by_uuid(self, uuid, service = None):
|
||||
return self.gatt_client.get_characteristics_by_uuid(uuid, service)
|
||||
|
||||
# [Classic only]
|
||||
async def request_name(self):
|
||||
return await self.connection.request_remote_name()
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.connection.peer_address} as {self.connection.role_name}'
|
||||
|
||||
@@ -176,6 +180,7 @@ class Connection(CompositeEventEmitter):
|
||||
self.transport = transport
|
||||
self.peer_address = peer_address
|
||||
self.peer_resolvable_address = peer_resolvable_address
|
||||
self.peer_name = None # Classic only
|
||||
self.role = role
|
||||
self.parameters = parameters
|
||||
self.encryption = 0
|
||||
@@ -231,6 +236,10 @@ class Connection(CompositeEventEmitter):
|
||||
supervision_timeout
|
||||
)
|
||||
|
||||
# [Classic only]
|
||||
async def request_remote_name(self):
|
||||
return await self.device.request_remote_name(self)
|
||||
|
||||
def __str__(self):
|
||||
return f'Connection(handle=0x{self.handle:04X}, role={self.role_name}, address={self.peer_address})'
|
||||
|
||||
@@ -290,9 +299,19 @@ def with_connection_from_handle(function):
|
||||
@functools.wraps(function)
|
||||
def wrapper(self, connection_handle, *args, **kwargs):
|
||||
if (connection := self.lookup_connection(connection_handle)) is None:
|
||||
logger.warn(f'no connection found for handle 0x{connection_handle:04X}')
|
||||
return
|
||||
function(self, connection, *args, **kwargs)
|
||||
raise ValueError('no connection for handle')
|
||||
return function(self, connection, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
# Decorator that converts the first argument from a bluetooth address to a connection
|
||||
def with_connection_from_address(function):
|
||||
@functools.wraps(function)
|
||||
def wrapper(self, address, *args, **kwargs):
|
||||
for connection in self.connections.values():
|
||||
if connection.peer_address == address:
|
||||
return function(self, connection, *args, **kwargs)
|
||||
raise ValueError('no connection for address')
|
||||
return wrapper
|
||||
|
||||
|
||||
@@ -453,6 +472,11 @@ class Device(CompositeEventEmitter):
|
||||
if connection := self.connections.get(connection_handle):
|
||||
return connection
|
||||
|
||||
def find_connection_by_bd_addr(self, bd_addr):
|
||||
for connection in self.connections.values():
|
||||
if connection.peer_address == bd_addr:
|
||||
return connection
|
||||
|
||||
def register_l2cap_server(self, psm, server):
|
||||
self.l2cap_channel_manager.register_server(psm, server)
|
||||
|
||||
@@ -792,8 +816,8 @@ class Device(CompositeEventEmitter):
|
||||
async def disconnect(self, connection, reason):
|
||||
# Create a future so that we can wait for the disconnection's result
|
||||
pending_disconnection = asyncio.get_running_loop().create_future()
|
||||
self.on('disconnection', pending_disconnection.set_result)
|
||||
self.on('disconnection_failure', pending_disconnection.set_exception)
|
||||
connection.on('disconnection', pending_disconnection.set_result)
|
||||
connection.on('disconnection_failure', pending_disconnection.set_exception)
|
||||
|
||||
# Request a disconnection
|
||||
result = await self.send_command(HCI_Disconnect_Command(connection_handle = connection.handle, reason = reason))
|
||||
@@ -806,8 +830,8 @@ class Device(CompositeEventEmitter):
|
||||
self.disconnecting = True
|
||||
return await pending_disconnection
|
||||
finally:
|
||||
self.remove_listener('disconnection', pending_disconnection.set_result)
|
||||
self.remove_listener('disconnection_failure', pending_disconnection.set_exception)
|
||||
connection.remove_listener('disconnection', pending_disconnection.set_result)
|
||||
connection.remove_listener('disconnection_failure', pending_disconnection.set_exception)
|
||||
self.disconnecting = False
|
||||
|
||||
async def update_connection_parameters(
|
||||
@@ -936,13 +960,13 @@ class Device(CompositeEventEmitter):
|
||||
# Set up event handlers
|
||||
pending_authentication = asyncio.get_running_loop().create_future()
|
||||
|
||||
def on_authentication_complete():
|
||||
def on_authentication():
|
||||
pending_authentication.set_result(None)
|
||||
|
||||
def on_authentication_failure(error):
|
||||
pending_authentication.set_exception(error)
|
||||
def on_authentication_failure(error_code):
|
||||
pending_authentication.set_exception(HCI_Error(error_code))
|
||||
|
||||
connection.on('connection_authentication_complete', on_authentication_complete)
|
||||
connection.on('connection_authentication', on_authentication)
|
||||
connection.on('connection_authentication_failure', on_authentication_failure)
|
||||
|
||||
# Request the authentication
|
||||
@@ -957,7 +981,7 @@ class Device(CompositeEventEmitter):
|
||||
# Wait for the authentication to complete
|
||||
await pending_authentication
|
||||
finally:
|
||||
connection.remove_listener('connection_authentication_complete', on_authentication_complete)
|
||||
connection.remove_listener('connection_authentication', on_authentication)
|
||||
connection.remove_listener('connection_authentication_failure', on_authentication_failure)
|
||||
|
||||
async def encrypt(self, connection):
|
||||
@@ -1028,6 +1052,40 @@ class Device(CompositeEventEmitter):
|
||||
connection.remove_listener('connection_encryption_change', on_encryption_change)
|
||||
connection.remove_listener('connection_encryption_failure', on_encryption_failure)
|
||||
|
||||
# [Classic only]
|
||||
async def request_remote_name(self, connection):
|
||||
# Set up event handlers
|
||||
pending_name = asyncio.get_running_loop().create_future()
|
||||
|
||||
def on_remote_name():
|
||||
pending_name.set_result(connection.peer_name)
|
||||
|
||||
def on_remote_name_failure(error_code):
|
||||
pending_name.set_exception(HCI_Error(error_code))
|
||||
|
||||
connection.on('remote_name', on_remote_name)
|
||||
connection.on('remote_name_failure', on_remote_name_failure)
|
||||
|
||||
try:
|
||||
result = await self.send_command(
|
||||
HCI_Remote_Name_Request_Command(
|
||||
bd_addr = connection.peer_address,
|
||||
page_scan_repetition_mode = HCI_Remote_Name_Request_Command.R0, # TODO investigate other options
|
||||
reserved = 0,
|
||||
clock_offset = 0 # TODO investigate non-0 values
|
||||
)
|
||||
)
|
||||
|
||||
if result.status != HCI_COMMAND_STATUS_PENDING:
|
||||
logger.warn(f'HCI_Set_Connection_Encryption_Command failed: {HCI_Constant.error_name(result.status)}')
|
||||
raise HCI_Error(result.status)
|
||||
|
||||
# Wait for the result
|
||||
return await pending_name
|
||||
finally:
|
||||
connection.remove_listener('remote_name', on_remote_name)
|
||||
connection.remove_listener('remote_name_failure', on_remote_name_failure)
|
||||
|
||||
# [Classic only]
|
||||
@host_event_handler
|
||||
def on_link_key(self, bd_addr, link_key, key_type):
|
||||
@@ -1123,14 +1181,15 @@ class Device(CompositeEventEmitter):
|
||||
asyncio.create_task(self.start_advertising(auto_restart=self.auto_restart_advertising))
|
||||
|
||||
@host_event_handler
|
||||
def on_disconnection_failure(self, error_code):
|
||||
@with_connection_from_handle
|
||||
def on_disconnection_failure(self, connection, error_code):
|
||||
logger.debug(f'*** Disconnection failed: {error_code}')
|
||||
error = ConnectionError(
|
||||
error_code,
|
||||
'hci',
|
||||
HCI_Constant.error_name(error_code)
|
||||
)
|
||||
self.emit('disconnection_failure', error)
|
||||
connection.emit('disconnection_failure', error)
|
||||
|
||||
@host_event_handler
|
||||
@AsyncRunner.run_in_task()
|
||||
@@ -1141,10 +1200,10 @@ class Device(CompositeEventEmitter):
|
||||
|
||||
@host_event_handler
|
||||
@with_connection_from_handle
|
||||
def on_connection_authentication_complete(self, connection):
|
||||
logger.debug(f'*** Connection Authentication Complete: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}')
|
||||
def on_connection_authentication(self, connection):
|
||||
logger.debug(f'*** Connection Authentication: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}')
|
||||
connection.authenticated = True
|
||||
connection.emit('connection_authentication_complete')
|
||||
connection.emit('connection_authentication')
|
||||
|
||||
@host_event_handler
|
||||
@with_connection_from_handle
|
||||
@@ -1152,6 +1211,132 @@ class Device(CompositeEventEmitter):
|
||||
logger.debug(f'*** Connection Authentication Failure: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}, error={error}')
|
||||
connection.emit('connection_authentication_failure', error)
|
||||
|
||||
# [Classic only]
|
||||
@host_event_handler
|
||||
@with_connection_from_address
|
||||
def on_authentication_io_capability_request(self, connection):
|
||||
# Ask what the pairing config should be for this connection
|
||||
pairing_config = self.pairing_config_factory(connection)
|
||||
|
||||
# Map the SMP IO capability to a Classic IO capability
|
||||
io_capability = {
|
||||
smp.SMP_DISPLAY_ONLY_IO_CAPABILITY: HCI_DISPLAY_ONLY_IO_CAPABILITY,
|
||||
smp.SMP_DISPLAY_YES_NO_IO_CAPABILITY: HCI_DISPLAY_YES_NO_IO_CAPABILITY,
|
||||
smp.SMP_KEYBOARD_ONLY_IO_CAPABILITY: HCI_KEYBOARD_ONLY_IO_CAPABILITY,
|
||||
smp.SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
|
||||
smp.SMP_KEYBOARD_DISPLAY_IO_CAPABILITY: HCI_DISPLAY_YES_NO_IO_CAPABILITY
|
||||
}.get(pairing_config.delegate.io_capability)
|
||||
|
||||
if io_capability is None:
|
||||
logger.warning(f'cannot map IO capability ({pairing_config.delegate.io_capability}')
|
||||
io_capability = HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY
|
||||
|
||||
# Compute the authentication requirements
|
||||
authentication_requirements = (
|
||||
# No Bonding
|
||||
(
|
||||
HCI_MITM_NOT_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS,
|
||||
HCI_MITM_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS
|
||||
),
|
||||
# General Bonding
|
||||
(
|
||||
HCI_MITM_NOT_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS,
|
||||
HCI_MITM_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS
|
||||
)
|
||||
)[1 if pairing_config.bonding else 0][1 if pairing_config.mitm else 0]
|
||||
|
||||
# Respond
|
||||
self.host.send_command_sync(
|
||||
HCI_IO_Capability_Request_Reply_Command(
|
||||
bd_addr = connection.peer_address,
|
||||
io_capability = io_capability,
|
||||
oob_data_present = 0x00, # Not present
|
||||
authentication_requirements = authentication_requirements
|
||||
)
|
||||
)
|
||||
|
||||
# [Classic only]
|
||||
@host_event_handler
|
||||
@with_connection_from_address
|
||||
def on_authentication_user_confirmation_request(self, connection, code):
|
||||
# Ask what the pairing config should be for this connection
|
||||
pairing_config = self.pairing_config_factory(connection)
|
||||
|
||||
can_confirm = pairing_config.delegate.io_capability not in {
|
||||
smp.SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
|
||||
smp.SMP_DISPLAY_ONLY_IO_CAPABILITY
|
||||
}
|
||||
|
||||
# Respond
|
||||
if can_confirm and pairing_config.delegate:
|
||||
async def compare_numbers():
|
||||
numbers_match = await pairing_config.delegate.compare_numbers(code, digits=6)
|
||||
if numbers_match:
|
||||
self.host.send_command_sync(
|
||||
HCI_User_Confirmation_Request_Reply_Command(bd_addr=connection.peer_address)
|
||||
)
|
||||
else:
|
||||
self.host.send_command_sync(
|
||||
HCI_User_Confirmation_Request_Negative_Reply_Command(bd_addr=connection.peer_address)
|
||||
)
|
||||
|
||||
asyncio.create_task(compare_numbers())
|
||||
else:
|
||||
self.host.send_command_sync(
|
||||
HCI_User_Confirmation_Request_Reply_Command(bd_addr=connection.peer_address)
|
||||
)
|
||||
|
||||
# [Classic only]
|
||||
@host_event_handler
|
||||
@with_connection_from_address
|
||||
def on_authentication_user_passkey_request(self, connection):
|
||||
# Ask what the pairing config should be for this connection
|
||||
pairing_config = self.pairing_config_factory(connection)
|
||||
|
||||
can_input = pairing_config.delegate.io_capability in {
|
||||
smp.SMP_KEYBOARD_ONLY_IO_CAPABILITY,
|
||||
smp.SMP_KEYBOARD_DISPLAY_IO_CAPABILITY
|
||||
}
|
||||
|
||||
# Respond
|
||||
if can_input and pairing_config.delegate:
|
||||
async def get_number():
|
||||
number = await pairing_config.delegate.get_number()
|
||||
if number is not None:
|
||||
self.host.send_command_sync(
|
||||
HCI_User_Passkey_Request_Reply_Command(
|
||||
bd_addr = connection.peer_address,
|
||||
numeric_value = number)
|
||||
)
|
||||
else:
|
||||
self.host.send_command_sync(
|
||||
HCI_User_Passkey_Request_Negative_Reply_Command(bd_addr=connection.peer_address)
|
||||
)
|
||||
|
||||
asyncio.create_task(get_number())
|
||||
else:
|
||||
self.host.send_command_sync(
|
||||
HCI_User_Passkey_Request_Negative_Reply_Command(bd_addr=connection.peer_address)
|
||||
)
|
||||
|
||||
# [Classic only]
|
||||
@host_event_handler
|
||||
@with_connection_from_address
|
||||
def on_remote_name(self, connection, remote_name):
|
||||
# Try to decode the name
|
||||
try:
|
||||
connection.peer_name = remote_name.decode('utf-8')
|
||||
connection.emit('remote_name')
|
||||
except UnicodeDecodeError as error:
|
||||
logger.warning('peer name is not valid UTF-8')
|
||||
connection.emit('remote_name_failure', error)
|
||||
|
||||
# [Classic only]
|
||||
@host_event_handler
|
||||
@with_connection_from_address
|
||||
def on_remote_name_failure(self, connection, error):
|
||||
connection.emit('remote_name_failure', error)
|
||||
|
||||
@host_event_handler
|
||||
@with_connection_from_handle
|
||||
def on_connection_encryption_change(self, connection, encryption):
|
||||
|
||||
@@ -79,6 +79,7 @@ HCI_VERSION_BLUETOOTH_CORE_4_2 = 8
|
||||
HCI_VERSION_BLUETOOTH_CORE_5_0 = 9
|
||||
HCI_VERSION_BLUETOOTH_CORE_5_1 = 10
|
||||
HCI_VERSION_BLUETOOTH_CORE_5_2 = 11
|
||||
HCI_VERSION_BLUETOOTH_CORE_5_3 = 12
|
||||
|
||||
# HCI Packet types
|
||||
HCI_COMMAND_PACKET = 0x01
|
||||
@@ -258,6 +259,9 @@ HCI_READ_REMOTE_VERSION_INFORMATION_COMMAND = hci_command_
|
||||
HCI_READ_CLOCK_OFFSET_COMMAND = hci_command_op_code(0x01, 0x001F)
|
||||
HCI_IO_CAPABILITY_REQUEST_REPLY_COMMAND = hci_command_op_code(0x01, 0x002B)
|
||||
HCI_USER_CONFIRMATION_REQUEST_REPLY_COMMAND = hci_command_op_code(0x01, 0x002C)
|
||||
HCI_USER_CONFIRMATION_REQUEST_NEGATIVE_REPLY_COMMAND = hci_command_op_code(0x01, 0x002D)
|
||||
HCI_USER_PASSKEY_REQUEST_REPLY_COMMAND = hci_command_op_code(0x01, 0x002E)
|
||||
HCI_USER_PASSKEY_REQUEST_NEGATIVE_REPLY_COMMAND = hci_command_op_code(0x01, 0x002F)
|
||||
HCI_ENHANCED_SETUP_SYNCHRONOUS_CONNECTION_COMMAND = hci_command_op_code(0x01, 0x003D)
|
||||
HCI_SNIFF_MODE_COMMAND = hci_command_op_code(0x02, 0x0003)
|
||||
HCI_EXIT_SNIFF_MODE_COMMAND = hci_command_op_code(0x02, 0x0004)
|
||||
@@ -407,6 +411,9 @@ HCI_COMMAND_NAMES = {
|
||||
HCI_READ_CLOCK_OFFSET_COMMAND: 'HCI_READ_CLOCK_OFFSET_COMMAND',
|
||||
HCI_IO_CAPABILITY_REQUEST_REPLY_COMMAND: 'HCI_IO_CAPABILITY_REQUEST_REPLY_COMMAND',
|
||||
HCI_USER_CONFIRMATION_REQUEST_REPLY_COMMAND: 'HCI_USER_CONFIRMATION_REQUEST_REPLY_COMMAND',
|
||||
HCI_USER_CONFIRMATION_REQUEST_NEGATIVE_REPLY_COMMAND: 'HCI_USER_CONFIRMATION_REQUEST_NEGATIVE_REPLY_COMMAND',
|
||||
HCI_USER_PASSKEY_REQUEST_REPLY_COMMAND: 'HCI_USER_PASSKEY_REQUEST_REPLY_COMMAND',
|
||||
HCI_USER_PASSKEY_REQUEST_NEGATIVE_REPLY_COMMAND: 'HCI_USER_PASSKEY_REQUEST_NEGATIVE_REPLY_COMMAND',
|
||||
HCI_ENHANCED_SETUP_SYNCHRONOUS_CONNECTION_COMMAND: 'HCI_ENHANCED_SETUP_SYNCHRONOUS_CONNECTION_COMMAND',
|
||||
HCI_SNIFF_MODE_COMMAND: 'HCI_SNIFF_MODE_COMMAND',
|
||||
HCI_EXIT_SNIFF_MODE_COMMAND: 'HCI_EXIT_SNIFF_MODE_COMMAND',
|
||||
@@ -683,20 +690,20 @@ HCI_IO_CAPABILITY_NAMES = {
|
||||
}
|
||||
|
||||
# Authentication Requirements
|
||||
HCI_MITM_NOT_REQUIRED_NO_BONDING_NUMERIC_COMPARISON_AUTHENTICATION_REQUIREMENTS = 0x00
|
||||
HCI_MITM_REQUIRED_NO_BONDING_USE_IO_CAPABILITIES_AUTHENTICATION_REQUIREMENTS = 0x01
|
||||
HCI_MITM_NOT_REQUIRED_DEDICATED_BONDING_NUMERIC_COMPARISON_AUTHENTICATION_REQUIREMENTS = 0x02
|
||||
HCI_MITM_REQUIRED_DEDICATED_BONDING_USE_IO_CAPABILITIES_AUTHENTICATION_REQUIREMENTS = 0x03
|
||||
HCI_MITM_NOT_REQUIRED_GENERAL_BONDING_NUMERIC_COMPARISON_AUTHENTICATION_REQUIREMENTS = 0x04
|
||||
HCI_MITM_REQUIRED_GENERAL_BONDING_USE_IO_CAPABILITIES_AUTHENTICATION_REQUIREMENTS = 0x05
|
||||
HCI_MITM_NOT_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS = 0x00
|
||||
HCI_MITM_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS = 0x01
|
||||
HCI_MITM_NOT_REQUIRED_DEDICATED_BONDING_AUTHENTICATION_REQUIREMENTS = 0x02
|
||||
HCI_MITM_REQUIRED_DEDICATED_BONDING_AUTHENTICATION_REQUIREMENTS = 0x03
|
||||
HCI_MITM_NOT_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS = 0x04
|
||||
HCI_MITM_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS = 0x05
|
||||
|
||||
HCI_AUTHENTICATION_REQUIREMENTS_NAMES = {
|
||||
HCI_MITM_NOT_REQUIRED_NO_BONDING_NUMERIC_COMPARISON_AUTHENTICATION_REQUIREMENTS: 'HCI_MITM_NOT_REQUIRED_NO_BONDING_NUMERIC_COMPARISON_AUTHENTICATION_REQUIREMENTS',
|
||||
HCI_MITM_REQUIRED_NO_BONDING_USE_IO_CAPABILITIES_AUTHENTICATION_REQUIREMENTS: 'HCI_MITM_REQUIRED_NO_BONDING_USE_IO_CAPABILITIES_AUTHENTICATION_REQUIREMENTS',
|
||||
HCI_MITM_NOT_REQUIRED_DEDICATED_BONDING_NUMERIC_COMPARISON_AUTHENTICATION_REQUIREMENTS: 'HCI_MITM_NOT_REQUIRED_DEDICATED_BONDING_NUMERIC_COMPARISON_AUTHENTICATION_REQUIREMENTS',
|
||||
HCI_MITM_REQUIRED_DEDICATED_BONDING_USE_IO_CAPABILITIES_AUTHENTICATION_REQUIREMENTS: 'HCI_MITM_REQUIRED_DEDICATED_BONDING_USE_IO_CAPABILITIES_AUTHENTICATION_REQUIREMENTS',
|
||||
HCI_MITM_NOT_REQUIRED_GENERAL_BONDING_NUMERIC_COMPARISON_AUTHENTICATION_REQUIREMENTS: 'HCI_MITM_NOT_REQUIRED_GENERAL_BONDING_NUMERIC_COMPARISON_AUTHENTICATION_REQUIREMENTS',
|
||||
HCI_MITM_REQUIRED_GENERAL_BONDING_USE_IO_CAPABILITIES_AUTHENTICATION_REQUIREMENTS: 'HCI_MITM_REQUIRED_GENERAL_BONDING_USE_IO_CAPABILITIES_AUTHENTICATION_REQUIREMENTS'
|
||||
HCI_MITM_NOT_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS: 'HCI_MITM_NOT_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS',
|
||||
HCI_MITM_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS: 'HCI_MITM_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS',
|
||||
HCI_MITM_NOT_REQUIRED_DEDICATED_BONDING_AUTHENTICATION_REQUIREMENTS: 'HCI_MITM_NOT_REQUIRED_DEDICATED_BONDING_AUTHENTICATION_REQUIREMENTS',
|
||||
HCI_MITM_REQUIRED_DEDICATED_BONDING_AUTHENTICATION_REQUIREMENTS: 'HCI_MITM_REQUIRED_DEDICATED_BONDING_AUTHENTICATION_REQUIREMENTS',
|
||||
HCI_MITM_NOT_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS: 'HCI_MITM_NOT_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS',
|
||||
HCI_MITM_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS: 'HCI_MITM_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS'
|
||||
}
|
||||
|
||||
# Link Key Types
|
||||
@@ -723,7 +730,7 @@ HCI_LINK_TYPE_NAMES = {
|
||||
}
|
||||
|
||||
# Address types
|
||||
HCI_PUBLIC_DEVICE_ADDRESS_TYPE = 0x0
|
||||
HCI_PUBLIC_DEVICE_ADDRESS_TYPE = 0x00
|
||||
HCI_RANDOM_DEVICE_ADDRESS_TYPE = 0x01
|
||||
HCI_PUBLIC_IDENTITY_ADDRESS_TYPE = 0x02
|
||||
HCI_RANDOM_IDENTITY_ADDRESS_TYPE = 0x03
|
||||
@@ -1371,6 +1378,9 @@ class HCI_Remote_Name_Request_Command(HCI_Command):
|
||||
'''
|
||||
See Bluetooth spec @ 7.1.19 Remote Name Request Command
|
||||
'''
|
||||
R0 = 0x00
|
||||
R1 = 0x01
|
||||
R2 = 0x02
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -1449,6 +1459,55 @@ class HCI_User_Confirmation_Request_Reply_Command(HCI_Command):
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command(
|
||||
fields=[
|
||||
('bd_addr', Address.parse_address)
|
||||
],
|
||||
return_parameters_fields=[
|
||||
('status', STATUS_SPEC),
|
||||
('bd_addr', Address.parse_address)
|
||||
]
|
||||
)
|
||||
class HCI_User_Confirmation_Request_Negative_Reply_Command(HCI_Command):
|
||||
'''
|
||||
See Bluetooth spec @ 7.1.31 User Confirmation Request Negative Reply Command
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command(
|
||||
fields=[
|
||||
('bd_addr', Address.parse_address),
|
||||
('numeric_value', 4)
|
||||
],
|
||||
return_parameters_fields=[
|
||||
('status', STATUS_SPEC),
|
||||
('bd_addr', Address.parse_address)
|
||||
]
|
||||
)
|
||||
class HCI_User_Passkey_Request_Reply_Command(HCI_Command):
|
||||
'''
|
||||
See Bluetooth spec @ 7.1.32 User Passkey Request Reply Command
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command(
|
||||
fields=[
|
||||
('bd_addr', Address.parse_address)
|
||||
],
|
||||
return_parameters_fields=[
|
||||
('status', STATUS_SPEC),
|
||||
('bd_addr', Address.parse_address)
|
||||
]
|
||||
)
|
||||
class HCI_User_Passkey_Request_Negative_Reply_Command(HCI_Command):
|
||||
'''
|
||||
See Bluetooth spec @ 7.1.33 User Passkey Request Negative Reply Command
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command([
|
||||
('connection_handle', 2),
|
||||
@@ -3367,6 +3426,16 @@ class HCI_User_Confirmation_Request_Event(HCI_Event):
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Event.event([
|
||||
('bd_addr', Address.parse_address)
|
||||
])
|
||||
class HCI_User_Passkey_Request_Event(HCI_Event):
|
||||
'''
|
||||
See Bluetooth spec @ 7.7.43 User Passkey Request Event
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Event.event([
|
||||
('status', STATUS_SPEC),
|
||||
|
||||
@@ -85,6 +85,7 @@ class Host(EventEmitter):
|
||||
self.command_semaphore = asyncio.Semaphore(1)
|
||||
self.long_term_key_provider = None
|
||||
self.link_key_provider = None
|
||||
self.pairing_io_capability_provider = None # Classic only
|
||||
|
||||
# Connect to the source and sink if specified
|
||||
if controller_source:
|
||||
@@ -363,7 +364,7 @@ class Host(EventEmitter):
|
||||
logger.debug(f'### BR/EDR CONNECTION FAILED: {event.status}')
|
||||
|
||||
# Notify the client
|
||||
self.emit('connection_failure', event.status)
|
||||
self.emit('connection_failure', event.connection_handle, event.status)
|
||||
|
||||
def on_hci_disconnection_complete_event(self, event):
|
||||
# Find the connection
|
||||
@@ -495,7 +496,7 @@ class Host(EventEmitter):
|
||||
def on_hci_authentication_complete_event(self, event):
|
||||
# Notify the client
|
||||
if event.status == HCI_SUCCESS:
|
||||
self.emit('connection_authentication_complete', event.connection_handle)
|
||||
self.emit('connection_authentication', event.connection_handle)
|
||||
else:
|
||||
self.emit('connection_authentication_failure', event.connection_handle, event.status)
|
||||
|
||||
@@ -560,26 +561,16 @@ class Host(EventEmitter):
|
||||
asyncio.create_task(send_link_key())
|
||||
|
||||
def on_hci_io_capability_request_event(self, event):
|
||||
# For now, just return NoInputNoOutput and no MITM
|
||||
# TODO: delegate the decision
|
||||
self.send_command_sync(
|
||||
HCI_IO_Capability_Request_Reply_Command(
|
||||
bd_addr = event.bd_addr,
|
||||
io_capability = HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
|
||||
oob_data_present = 0x00,
|
||||
authentication_requirements = 0x00 # 0x02 # FIXME: testing only
|
||||
)
|
||||
)
|
||||
self.emit('authentication_io_capability_request', event.bd_addr)
|
||||
|
||||
def on_hci_io_capability_response_event(self, event):
|
||||
pass
|
||||
|
||||
def on_hci_user_confirmation_request_event(self, event):
|
||||
# For now, just confirm everything
|
||||
# TODO: delegate the decision
|
||||
self.send_command_sync(
|
||||
HCI_User_Confirmation_Request_Reply_Command(bd_addr = event.bd_addr)
|
||||
)
|
||||
self.emit('authentication_user_confirmation_request', event.bd_addr, event.numeric_value)
|
||||
|
||||
def on_hci_user_passkey_request_event(self, event):
|
||||
self.emit('authentication_user_passkey_request', event.bd_addr)
|
||||
|
||||
def on_hci_inquiry_complete_event(self, event):
|
||||
self.emit('inquiry_complete')
|
||||
@@ -602,3 +593,9 @@ class Host(EventEmitter):
|
||||
event.extended_inquiry_response,
|
||||
event.rssi
|
||||
)
|
||||
|
||||
def on_hci_remote_name_request_complete_event(self, event):
|
||||
if event.status != HCI_SUCCESS:
|
||||
self.emit('remote_name_failure', event.bd_addr, event.status)
|
||||
else:
|
||||
self.emit('remote_name', event.bd_addr, event.remote_name)
|
||||
|
||||
@@ -464,13 +464,13 @@ class PairingDelegate:
|
||||
async def accept(self):
|
||||
return True
|
||||
|
||||
async def compare_numbers(self, number):
|
||||
async def compare_numbers(self, number, digits=6):
|
||||
return True
|
||||
|
||||
async def get_number(self):
|
||||
return 0
|
||||
|
||||
async def display_number(self, number):
|
||||
async def display_number(self, number, digits=6):
|
||||
pass
|
||||
|
||||
|
||||
@@ -699,7 +699,7 @@ class Session:
|
||||
async def prompt():
|
||||
logger.debug(f'verification code: {code}')
|
||||
try:
|
||||
response = await self.pairing_config.delegate.compare_numbers(code)
|
||||
response = await self.pairing_config.delegate.compare_numbers(code, digits=6)
|
||||
if response:
|
||||
next_steps()
|
||||
return
|
||||
@@ -733,7 +733,7 @@ class Session:
|
||||
self.tk = self.passkey.to_bytes(16, byteorder='little')
|
||||
logger.debug(f'TK from passkey = {self.tk.hex()}')
|
||||
|
||||
asyncio.create_task(self.pairing_config.delegate.display_number(self.passkey))
|
||||
asyncio.create_task(self.pairing_config.delegate.display_number(self.passkey, digits=6))
|
||||
|
||||
def input_passkey(self, next_steps=None):
|
||||
# Prompt the user for the passkey displayed on the peer
|
||||
|
||||
2
docs/mkdocs/src/apps_and_tools/gatt_dump.md
Normal file
2
docs/mkdocs/src/apps_and_tools/gatt_dump.md
Normal file
@@ -0,0 +1,2 @@
|
||||
GATT DUMP TOOL
|
||||
==============
|
||||
@@ -5,6 +5,8 @@ Included in the project are a few apps and tools, built on top of the core libra
|
||||
These include:
|
||||
|
||||
* [Console](console.md) - an interactive text-based console
|
||||
* [Pair](pair.md) - Pair/bond two devices (LE and Classic)
|
||||
* [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
|
||||
* [Golden Gate Bridge](gg_bridge.md) - a bridge between GATT and UDP to use with the Golden Gate "stack tool"
|
||||
* [Show](show.md) - Parse a file with HCI packets and print the details of each packet in a human readable form
|
||||
|
||||
2
docs/mkdocs/src/apps_and_tools/pair.md
Normal file
2
docs/mkdocs/src/apps_and_tools/pair.md
Normal file
@@ -0,0 +1,2 @@
|
||||
PAIR TOOL
|
||||
=========
|
||||
@@ -0,0 +1,2 @@
|
||||
SHOW TOOL
|
||||
=========
|
||||
|
||||
2
docs/mkdocs/src/apps_and_tools/unbond.md
Normal file
2
docs/mkdocs/src/apps_and_tools/unbond.md
Normal file
@@ -0,0 +1,2 @@
|
||||
UNBOND TOOL
|
||||
===========
|
||||
@@ -98,4 +98,9 @@ $ PYTHONPATH=. python examples/run_advertiser.py examples/device1.json serial:/d
|
||||
# Where To Go Next
|
||||
Once you've installed or downloaded Bumble, you can either start using some of the
|
||||
[Bundled apps and tools](apps_and_tools/index.md), or look at the [examples](examples/index.md)
|
||||
to get a feel for how to use the APIs, and start writing your own applications.
|
||||
to get a feel for how to use the APIs, and start writing your own applications.
|
||||
|
||||
Depending on the use case you're interested in exploring, you may need to use a physical Bluetooth
|
||||
controller, like a USB dongle or a board with a Bluetooth radio. Visit the [Hardware page](hardware/index.md)
|
||||
for more information on using a physical radio, and/or the [Transports page](transports/index.md) for more
|
||||
details on interfacing with either hardware modules or virtual controllers over various transports.
|
||||
|
||||
@@ -36,6 +36,7 @@ install_requires =
|
||||
grpcio >= 1.46; platform_system!='Emscripten'
|
||||
libusb1 >= 2.0.1; platform_system!='Emscripten'
|
||||
prompt_toolkit >= 3.0.16; platform_system!='Emscripten'
|
||||
protobuf >= 3.12.4
|
||||
pyee >= 8.2.2
|
||||
pyserial-asyncio >= 0.5; platform_system!='Emscripten'
|
||||
pyserial >= 3.5; platform_system!='Emscripten'
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
[pytest]
|
||||
junit_logging = all
|
||||
asyncio_mode = auto
|
||||
@@ -208,11 +208,11 @@ async def test_self_smp():
|
||||
self.peer_delegate = None
|
||||
self.number = asyncio.get_running_loop().create_future()
|
||||
|
||||
async def compare_numbers(self, number):
|
||||
async def compare_numbers(self, number, digits):
|
||||
if self.peer_delegate is None:
|
||||
logger.warn(f'[{self.name}] no peer delegate')
|
||||
return False
|
||||
await self.display_number(number)
|
||||
await self.display_number(number, digits=6)
|
||||
logger.debug(f'[{self.name}] waiting for peer number')
|
||||
peer_number = await self.peer_delegate.number
|
||||
logger.debug(f'[{self.name}] comparing numbers: {number} and {peer_number}')
|
||||
@@ -231,7 +231,7 @@ async def test_self_smp():
|
||||
logger.debug(f'[{self.name}] returning number: {peer_number}')
|
||||
return peer_number
|
||||
|
||||
async def display_number(self, number):
|
||||
async def display_number(self, number, digits):
|
||||
logger.debug(f'[{self.name}] displaying number: {number}')
|
||||
self.number.set_result(number)
|
||||
|
||||
@@ -293,7 +293,7 @@ async def test_self_smp_wrong_pin():
|
||||
def __init__(self):
|
||||
super().__init__(PairingDelegate.DISPLAY_OUTPUT_AND_KEYBOARD_INPUT)
|
||||
|
||||
async def compare_numbers(self, number):
|
||||
async def compare_numbers(self, number, digits):
|
||||
return False
|
||||
|
||||
wrong_pin_pairing_config = PairingConfig(delegate = WrongPinDelegate())
|
||||
|
||||
Reference in New Issue
Block a user