forked from auracaster/bumble_mirror
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 368e7eff05 | |||
| 55b813bbf5 | |||
| 14dfc1a501 | |||
| 938282e961 | |||
| 900c15b151 | |||
| 9ea93be723 | |||
| 894ab023c7 | |||
| 7bbb37b2da | |||
| 3fa5d320de | |||
| 16d684c199 | |||
| c28aa2ebb6 | |||
| 28586382f4 | |||
| 76f08977c4 | |||
| 15cbf52da4 | |||
| f4f84dffef | |||
| 6dfb07d7b9 | |||
| d7ce62beaa | |||
| 0e2a184edb | |||
| e6ee5ae996 | |||
| f1836e659f | |||
| 99218d3abf | |||
| b5ba0bef63 | |||
| 9cd1890faa | |||
| 472702a9d9 | |||
| b38740e5b7 | |||
| 009ecfce96 | |||
| d6075df356 | |||
| ebd0a0c8ca |
@@ -16,7 +16,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- name: Check out from Git
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Get history and tags for SCM versioning to work
|
||||||
|
run: |
|
||||||
|
git fetch --prune --unshallow
|
||||||
|
git fetch --depth=1 origin +refs/tags/*:refs/tags/*
|
||||||
- name: Set up Python 3.10
|
- name: Set up Python 3.10
|
||||||
uses: actions/setup-python@v3
|
uses: actions/setup-python@v3
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
name: Upload Python Package
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out from Git
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Get history and tags for SCM versioning to work
|
||||||
|
run: |
|
||||||
|
git fetch --prune --unshallow
|
||||||
|
git fetch --depth=1 origin +refs/tags/*:refs/tags/*
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v3
|
||||||
|
with:
|
||||||
|
python-version: '3.10'
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
python -m pip install build
|
||||||
|
- name: Build package
|
||||||
|
run: python -m build
|
||||||
|
- name: Publish package to PyPI
|
||||||
|
if: github.event_name == 'release' && startsWith(github.ref, 'refs/tags')
|
||||||
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
|
with:
|
||||||
|
user: __token__
|
||||||
|
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||||
@@ -21,6 +21,29 @@ or see the documentation source under `docs/mkdocs/src`, or build the static HTM
|
|||||||
mkdocs build -f docs/mkdocs/mkdocs.yml
|
mkdocs build -f docs/mkdocs/mkdocs.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Getting Started
|
||||||
|
|
||||||
|
For a quick start to using Bumble, see the [Getting Started](docs/mkdocs/src/getting_started.md) guide.
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
To install package dependencies needed to run the bumble examples execute the following commands:
|
||||||
|
|
||||||
|
```
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
python -m pip install ".[test,development,documentation]"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
Refer to the [Example Documentation](examples/README.md) for details on the included example scripts and how to run them.
|
||||||
|
|
||||||
|
The complete [list of Examples](/docs/mkdocs/src/examples/index.md), and what they are designed to do is here.
|
||||||
|
|
||||||
|
There are also a set of [Apps and Tools](docs/mkdocs/src/apps_and_tools/index.md) that show the utility of Bumble.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Licensed under the [Apache 2.0](LICENSE) License.
|
Licensed under the [Apache 2.0](LICENSE) License.
|
||||||
|
|||||||
+86
-18
@@ -32,6 +32,7 @@ from bumble.core import UUID, AdvertisingData
|
|||||||
from bumble.device import Device, Connection, Peer
|
from bumble.device import Device, Connection, Peer
|
||||||
from bumble.utils import AsyncRunner
|
from bumble.utils import AsyncRunner
|
||||||
from bumble.transport import open_transport_or_link
|
from bumble.transport import open_transport_or_link
|
||||||
|
from bumble.gatt import Characteristic
|
||||||
|
|
||||||
from prompt_toolkit import Application
|
from prompt_toolkit import Application
|
||||||
from prompt_toolkit.history import FileHistory
|
from prompt_toolkit.history import FileHistory
|
||||||
@@ -121,6 +122,8 @@ class ConsoleApp:
|
|||||||
},
|
},
|
||||||
'read': LiveCompleter(self.known_attributes),
|
'read': LiveCompleter(self.known_attributes),
|
||||||
'write': LiveCompleter(self.known_attributes),
|
'write': LiveCompleter(self.known_attributes),
|
||||||
|
'subscribe': LiveCompleter(self.known_attributes),
|
||||||
|
'unsubscribe': LiveCompleter(self.known_attributes),
|
||||||
'quit': None,
|
'quit': None,
|
||||||
'exit': None
|
'exit': None
|
||||||
})
|
})
|
||||||
@@ -330,9 +333,27 @@ class ConsoleApp:
|
|||||||
|
|
||||||
await self.show_attributes(attributes)
|
await self.show_attributes(attributes)
|
||||||
|
|
||||||
|
def find_characteristic(self, param):
|
||||||
|
parts = param.split('.')
|
||||||
|
if len(parts) == 2:
|
||||||
|
service_uuid = UUID(parts[0]) if parts[0] != '*' else None
|
||||||
|
characteristic_uuid = UUID(parts[1])
|
||||||
|
for service in self.connected_peer.services:
|
||||||
|
if service_uuid is None or service.uuid == service_uuid:
|
||||||
|
for characteristic in service.characteristics:
|
||||||
|
if characteristic.uuid == characteristic_uuid:
|
||||||
|
return characteristic
|
||||||
|
elif len(parts) == 1:
|
||||||
|
if parts[0].startswith('#'):
|
||||||
|
attribute_handle = int(f'{parts[0][1:]}', 16)
|
||||||
|
for service in self.connected_peer.services:
|
||||||
|
for characteristic in service.characteristics:
|
||||||
|
if characteristic.handle == attribute_handle:
|
||||||
|
return characteristic
|
||||||
|
|
||||||
async def command(self, command):
|
async def command(self, command):
|
||||||
try:
|
try:
|
||||||
(keyword, *params) = command.strip().split(' ', 1)
|
(keyword, *params) = command.strip().split(' ')
|
||||||
keyword = keyword.replace('-', '_').lower()
|
keyword = keyword.replace('-', '_').lower()
|
||||||
handler = getattr(self, f'do_{keyword}', None)
|
handler = getattr(self, f'do_{keyword}', None)
|
||||||
if handler:
|
if handler:
|
||||||
@@ -441,26 +462,73 @@ class ConsoleApp:
|
|||||||
self.show_error('invalid syntax', 'expected read <attribute>')
|
self.show_error('invalid syntax', 'expected read <attribute>')
|
||||||
return
|
return
|
||||||
|
|
||||||
parts = params[0].split('.')
|
characteristic = self.find_characteristic(params[0])
|
||||||
if len(parts) == 2:
|
if characteristic is None:
|
||||||
service_uuid = UUID(parts[0]) if parts[0] != '*' else None
|
|
||||||
characteristic_uuid = UUID(parts[1])
|
|
||||||
for service in self.connected_peer.services:
|
|
||||||
if service_uuid is None or service.uuid == service_uuid:
|
|
||||||
for characteristic in service.characteristics:
|
|
||||||
if characteristic.uuid == characteristic_uuid:
|
|
||||||
value = await self.connected_peer.read_value(characteristic)
|
|
||||||
self.append_to_output(f'VALUE: {value}')
|
|
||||||
return
|
|
||||||
self.show_error('no such characteristic')
|
self.show_error('no such characteristic')
|
||||||
elif len(parts) == 1:
|
return
|
||||||
if parts[0].startswith('#'):
|
|
||||||
attribute_handle = int(f'{parts[0][1:]}', 16)
|
value = await characteristic.read_value()
|
||||||
value = await self.connected_peer.read_value(attribute_handle)
|
self.append_to_output(f'VALUE: 0x{value.hex()}')
|
||||||
self.append_to_output(f'VALUE: {value}')
|
|
||||||
return
|
async def do_write(self, params):
|
||||||
|
if not self.connected_peer:
|
||||||
|
self.show_error('not connected')
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(params) != 2:
|
||||||
|
self.show_error('invalid syntax', 'expected write <attribute> <value>')
|
||||||
|
return
|
||||||
|
|
||||||
|
if params[1].upper().startswith("0X"):
|
||||||
|
value = bytes.fromhex(params[1][2:]) # parse as hex string
|
||||||
else:
|
else:
|
||||||
|
try:
|
||||||
|
value = int(params[1]) # try as integer
|
||||||
|
except ValueError:
|
||||||
|
value = str.encode(params[1]) # must be a string
|
||||||
|
|
||||||
|
characteristic = self.find_characteristic(params[0])
|
||||||
|
if characteristic is None:
|
||||||
self.show_error('no such characteristic')
|
self.show_error('no such characteristic')
|
||||||
|
return
|
||||||
|
|
||||||
|
# use write with response if supported
|
||||||
|
with_response = characteristic.properties & Characteristic.WRITE
|
||||||
|
await characteristic.write_value(value, with_response=with_response)
|
||||||
|
|
||||||
|
async def do_subscribe(self, params):
|
||||||
|
if not self.connected_peer:
|
||||||
|
self.show_error('not connected')
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(params) != 1:
|
||||||
|
self.show_error('invalid syntax', 'expected subscribe <attribute>')
|
||||||
|
return
|
||||||
|
|
||||||
|
characteristic = self.find_characteristic(params[0])
|
||||||
|
if characteristic is None:
|
||||||
|
self.show_error('no such characteristic')
|
||||||
|
return
|
||||||
|
|
||||||
|
await characteristic.subscribe(
|
||||||
|
lambda value: self.append_to_output(f"{characteristic} VALUE: 0x{value.hex()}"),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def do_unsubscribe(self, params):
|
||||||
|
if not self.connected_peer:
|
||||||
|
self.show_error('not connected')
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(params) != 1:
|
||||||
|
self.show_error('invalid syntax', 'expected subscribe <attribute>')
|
||||||
|
return
|
||||||
|
|
||||||
|
characteristic = self.find_characteristic(params[0])
|
||||||
|
if characteristic is None:
|
||||||
|
self.show_error('no such characteristic')
|
||||||
|
return
|
||||||
|
|
||||||
|
await characteristic.unsubscribe()
|
||||||
|
|
||||||
async def do_exit(self, params):
|
async def do_exit(self, params):
|
||||||
self.ui.exit()
|
self.ui.exit()
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
# Copyright 2021-2022 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 os
|
||||||
|
import logging
|
||||||
|
import click
|
||||||
|
from colors import color
|
||||||
|
from bumble.company_ids import COMPANY_IDENTIFIERS
|
||||||
|
|
||||||
|
from bumble.core import name_or_number
|
||||||
|
from bumble.hci import (
|
||||||
|
map_null_terminated_utf8_string,
|
||||||
|
HCI_LE_SUPPORTED_FEATURES_NAMES,
|
||||||
|
HCI_SUCCESS,
|
||||||
|
HCI_VERSION_NAMES,
|
||||||
|
LMP_VERSION_NAMES,
|
||||||
|
HCI_Command,
|
||||||
|
HCI_Read_BD_ADDR_Command,
|
||||||
|
HCI_READ_BD_ADDR_COMMAND,
|
||||||
|
HCI_Read_Local_Name_Command,
|
||||||
|
HCI_READ_LOCAL_NAME_COMMAND
|
||||||
|
)
|
||||||
|
from bumble.host import Host
|
||||||
|
from bumble.transport import open_transport_or_link
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
async def get_classic_info(host):
|
||||||
|
if host.supports_command(HCI_READ_BD_ADDR_COMMAND):
|
||||||
|
response = await host.send_command(HCI_Read_BD_ADDR_Command())
|
||||||
|
if response.return_parameters.status == HCI_SUCCESS:
|
||||||
|
print()
|
||||||
|
print(color('Classic Address:', 'yellow'), response.return_parameters.bd_addr)
|
||||||
|
|
||||||
|
if host.supports_command(HCI_READ_LOCAL_NAME_COMMAND):
|
||||||
|
response = await host.send_command(HCI_Read_Local_Name_Command())
|
||||||
|
if response.return_parameters.status == HCI_SUCCESS:
|
||||||
|
print()
|
||||||
|
print(color('Local Name:', 'yellow'), map_null_terminated_utf8_string(response.return_parameters.local_name))
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
async def get_le_info(host):
|
||||||
|
print()
|
||||||
|
print(color('LE Features:', 'yellow'))
|
||||||
|
for feature in host.supported_le_features:
|
||||||
|
print(' ', name_or_number(HCI_LE_SUPPORTED_FEATURES_NAMES, feature))
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
async def async_main(transport):
|
||||||
|
print('<<< connecting to HCI...')
|
||||||
|
async with await open_transport_or_link(transport) as (hci_source, hci_sink):
|
||||||
|
print('<<< connected')
|
||||||
|
|
||||||
|
host = Host(hci_source, hci_sink)
|
||||||
|
await host.reset()
|
||||||
|
|
||||||
|
# Print version
|
||||||
|
print(color('Version:', 'yellow'))
|
||||||
|
print(color(' Manufacturer: ', 'green'), name_or_number(COMPANY_IDENTIFIERS, host.local_version.company_identifier))
|
||||||
|
print(color(' HCI Version: ', 'green'), name_or_number(HCI_VERSION_NAMES, host.local_version.hci_version))
|
||||||
|
print(color(' HCI Subversion:', 'green'), host.local_version.hci_subversion)
|
||||||
|
print(color(' LMP Version: ', 'green'), name_or_number(LMP_VERSION_NAMES, host.local_version.lmp_version))
|
||||||
|
print(color(' LMP Subversion:', 'green'), host.local_version.lmp_subversion)
|
||||||
|
|
||||||
|
# Get the Classic info
|
||||||
|
await get_classic_info(host)
|
||||||
|
|
||||||
|
# Get the LE info
|
||||||
|
await get_le_info(host)
|
||||||
|
|
||||||
|
# Print the list of commands supported by the controller
|
||||||
|
print()
|
||||||
|
print(color('Supported Commands:', 'yellow'))
|
||||||
|
for command in host.supported_commands:
|
||||||
|
print(' ', HCI_Command.command_name(command))
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
@click.command()
|
||||||
|
@click.argument('transport')
|
||||||
|
def main(transport):
|
||||||
|
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
|
||||||
|
asyncio.run(async_main(transport))
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
+12
-12
@@ -77,7 +77,7 @@ class Controller:
|
|||||||
self.le_features = bytes.fromhex('ff49010000000000')
|
self.le_features = bytes.fromhex('ff49010000000000')
|
||||||
self.le_states = bytes.fromhex('ffff3fffff030000')
|
self.le_states = bytes.fromhex('ffff3fffff030000')
|
||||||
self.avertising_channel_tx_power = 0
|
self.avertising_channel_tx_power = 0
|
||||||
self.white_list_size = 8
|
self.filter_accept_list_size = 8
|
||||||
self.resolving_list_size = 8
|
self.resolving_list_size = 8
|
||||||
self.supported_max_tx_octets = 27
|
self.supported_max_tx_octets = 27
|
||||||
self.supported_max_tx_time = 10000 # microseconds
|
self.supported_max_tx_time = 10000 # microseconds
|
||||||
@@ -731,27 +731,27 @@ class Controller:
|
|||||||
'''
|
'''
|
||||||
return bytes([HCI_SUCCESS])
|
return bytes([HCI_SUCCESS])
|
||||||
|
|
||||||
def on_hci_le_read_white_list_size_command(self, command):
|
def on_hci_le_read_filter_accept_list_size_command(self, command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 2, Part E - 7.8.14 LE Read White List Size Command
|
See Bluetooth spec Vol 2, Part E - 7.8.14 LE Read Filter Accept List Size Command
|
||||||
'''
|
'''
|
||||||
return bytes([HCI_SUCCESS, self.white_list_size])
|
return bytes([HCI_SUCCESS, self.filter_accept_list_size])
|
||||||
|
|
||||||
def on_hci_le_clear_white_list_command(self, command):
|
def on_hci_le_clear_filter_accept_list_command(self, command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 2, Part E - 7.8.15 LE Clear White List Command
|
See Bluetooth spec Vol 2, Part E - 7.8.15 LE Clear Filter Accept List Command
|
||||||
'''
|
'''
|
||||||
return bytes([HCI_SUCCESS])
|
return bytes([HCI_SUCCESS])
|
||||||
|
|
||||||
def on_hci_le_add_device_to_white_list_command(self, command):
|
def on_hci_le_add_device_to_filter_accept_list_command(self, command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 2, Part E - 7.8.16 LE Add Device To White List Command
|
See Bluetooth spec Vol 2, Part E - 7.8.16 LE Add Device To Filter Accept List Command
|
||||||
'''
|
'''
|
||||||
return bytes([HCI_SUCCESS])
|
return bytes([HCI_SUCCESS])
|
||||||
|
|
||||||
def on_hci_le_remove_device_from_white_list_command(self, command):
|
def on_hci_le_remove_device_from_filter_accept_list_command(self, command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 2, Part E - 7.8.17 LE Remove Device From White List Command
|
See Bluetooth spec Vol 2, Part E - 7.8.17 LE Remove Device From Filter Accept List Command
|
||||||
'''
|
'''
|
||||||
return bytes([HCI_SUCCESS])
|
return bytes([HCI_SUCCESS])
|
||||||
|
|
||||||
@@ -780,9 +780,9 @@ class Controller:
|
|||||||
'''
|
'''
|
||||||
return bytes([HCI_SUCCESS]) + struct.pack('Q', random.randint(0, 1 << 64))
|
return bytes([HCI_SUCCESS]) + struct.pack('Q', random.randint(0, 1 << 64))
|
||||||
|
|
||||||
def on_hci_le_start_encryption_command(self, command):
|
def on_hci_le_enable_encryption_command(self, command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 2, Part E - 7.8.24 LE Start Encryption Command
|
See Bluetooth spec Vol 2, Part E - 7.8.24 LE Enable Encryption Command
|
||||||
'''
|
'''
|
||||||
|
|
||||||
# Check the parameters
|
# Check the parameters
|
||||||
|
|||||||
+65
-10
@@ -18,6 +18,7 @@
|
|||||||
import json
|
import json
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
from contextlib import asynccontextmanager, AsyncExitStack
|
||||||
|
|
||||||
from .hci import *
|
from .hci import *
|
||||||
from .host import Host
|
from .host import Host
|
||||||
@@ -122,6 +123,9 @@ class Peer:
|
|||||||
async def subscribe(self, characteristic, subscriber=None):
|
async def subscribe(self, characteristic, subscriber=None):
|
||||||
return await self.gatt_client.subscribe(characteristic, subscriber)
|
return await self.gatt_client.subscribe(characteristic, subscriber)
|
||||||
|
|
||||||
|
async def unsubscribe(self, characteristic, subscriber=None):
|
||||||
|
return await self.gatt_client.unsubscribe(characteristic, subscriber)
|
||||||
|
|
||||||
async def read_value(self, attribute):
|
async def read_value(self, attribute):
|
||||||
return await self.gatt_client.read_value(attribute)
|
return await self.gatt_client.read_value(attribute)
|
||||||
|
|
||||||
@@ -148,10 +152,24 @@ class Peer:
|
|||||||
await service.discover_characteristics()
|
await service.discover_characteristics()
|
||||||
return self.create_service_proxy(proxy_class)
|
return self.create_service_proxy(proxy_class)
|
||||||
|
|
||||||
|
async def sustain(self, timeout=None):
|
||||||
|
await self.connection.sustain(timeout)
|
||||||
|
|
||||||
# [Classic only]
|
# [Classic only]
|
||||||
async def request_name(self):
|
async def request_name(self):
|
||||||
return await self.connection.request_remote_name()
|
return await self.connection.request_remote_name()
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
await self.discover_services()
|
||||||
|
for service in self.services:
|
||||||
|
await self.discover_characteristics()
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc_value, traceback):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'{self.connection.peer_address} as {self.connection.role_name}'
|
return f'{self.connection.peer_address} as {self.connection.role_name}'
|
||||||
|
|
||||||
@@ -232,6 +250,21 @@ class Connection(CompositeEventEmitter):
|
|||||||
async def encrypt(self):
|
async def encrypt(self):
|
||||||
return await self.device.encrypt(self)
|
return await self.device.encrypt(self)
|
||||||
|
|
||||||
|
async def sustain(self, timeout=None):
|
||||||
|
""" Idles the current task waiting for a disconnect or timeout """
|
||||||
|
|
||||||
|
abort = asyncio.get_running_loop().create_future()
|
||||||
|
self.on('disconnection', abort.set_result)
|
||||||
|
self.on('disconnection_failure', abort.set_exception)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(abort, timeout)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.remove_listener('disconnection', abort.set_result)
|
||||||
|
self.remove_listener('disconnection_failure', abort.set_exception)
|
||||||
|
|
||||||
async def update_parameters(
|
async def update_parameters(
|
||||||
self,
|
self,
|
||||||
conn_interval_min,
|
conn_interval_min,
|
||||||
@@ -251,6 +284,18 @@ class Connection(CompositeEventEmitter):
|
|||||||
async def request_remote_name(self):
|
async def request_remote_name(self):
|
||||||
return await self.device.request_remote_name(self)
|
return await self.device.request_remote_name(self)
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc_value, traceback):
|
||||||
|
if exc_type is None:
|
||||||
|
try:
|
||||||
|
await self.disconnect()
|
||||||
|
except HCI_StatusError as e:
|
||||||
|
# Invalid parameter means the connection is no longer valid
|
||||||
|
if e.error_code != HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR:
|
||||||
|
raise
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'Connection(handle=0x{self.handle:04X}, role={self.role_name}, address={self.peer_address})'
|
return f'Connection(handle=0x{self.handle:04X}, role={self.role_name}, address={self.peer_address})'
|
||||||
|
|
||||||
@@ -314,6 +359,7 @@ class DeviceConfiguration:
|
|||||||
# within a class requires unnecessarily complicated acrobatics)
|
# within a class requires unnecessarily complicated acrobatics)
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
# Decorator that converts the first argument from a connection handle to a connection
|
# Decorator that converts the first argument from a connection handle to a connection
|
||||||
def with_connection_from_handle(function):
|
def with_connection_from_handle(function):
|
||||||
@functools.wraps(function)
|
@functools.wraps(function)
|
||||||
@@ -704,7 +750,7 @@ class Device(CompositeEventEmitter):
|
|||||||
))
|
))
|
||||||
if response.status != HCI_Command_Status_Event.PENDING:
|
if response.status != HCI_Command_Status_Event.PENDING:
|
||||||
self.discovering = False
|
self.discovering = False
|
||||||
raise RuntimeError(f'HCI_Inquiry command failed: {HCI_Constant.status_name(response.status)} ({response.status})')
|
raise HCI_StatusError(response)
|
||||||
|
|
||||||
self.discovering = True
|
self.discovering = True
|
||||||
|
|
||||||
@@ -785,7 +831,7 @@ class Device(CompositeEventEmitter):
|
|||||||
try:
|
try:
|
||||||
peer_address = Address(peer_address)
|
peer_address = Address(peer_address)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# If the address is not parssable, assume it is a name instead
|
# If the address is not parsable, assume it is a name instead
|
||||||
logger.debug('looking for peer by name')
|
logger.debug('looking for peer by name')
|
||||||
peer_address = await self.find_peer_by_name(peer_address, transport)
|
peer_address = await self.find_peer_by_name(peer_address, transport)
|
||||||
|
|
||||||
@@ -824,16 +870,25 @@ class Device(CompositeEventEmitter):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
if result.status != HCI_Command_Status_Event.PENDING:
|
if result.status != HCI_Command_Status_Event.PENDING:
|
||||||
raise RuntimeError(f'HCI_LE_Create_Connection_Command failed: {HCI_Constant.status_name(result.status)} ({result.status})')
|
raise HCI_StatusError(result)
|
||||||
|
|
||||||
# Wait for the connection process to complete
|
# Wait for the connection process to complete
|
||||||
self.connecting = True
|
self.connecting = True
|
||||||
return await pending_connection
|
return await pending_connection
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
self.remove_listener('connection', pending_connection.set_result)
|
self.remove_listener('connection', pending_connection.set_result)
|
||||||
self.remove_listener('connection_failure', pending_connection.set_exception)
|
self.remove_listener('connection_failure', pending_connection.set_exception)
|
||||||
self.connecting = False
|
self.connecting = False
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def connect_as_gatt(self, peer_address):
|
||||||
|
async with AsyncExitStack() as stack:
|
||||||
|
connection = await stack.enter_async_context(await self.connect(peer_address))
|
||||||
|
peer = await stack.enter_async_context(Peer(connection))
|
||||||
|
|
||||||
|
yield peer
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_connecting(self):
|
def is_connecting(self):
|
||||||
return self.connecting
|
return self.connecting
|
||||||
@@ -858,7 +913,7 @@ class Device(CompositeEventEmitter):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
if result.status != HCI_Command_Status_Event.PENDING:
|
if result.status != HCI_Command_Status_Event.PENDING:
|
||||||
raise RuntimeError(f'HCI_Disconnect_Command failed: {HCI_Constant.status_name(result.status)} ({result.status})')
|
raise HCI_StatusError(result)
|
||||||
|
|
||||||
# Wait for the disconnection process to complete
|
# Wait for the disconnection process to complete
|
||||||
self.disconnecting = True
|
self.disconnecting = True
|
||||||
@@ -1010,7 +1065,7 @@ class Device(CompositeEventEmitter):
|
|||||||
)
|
)
|
||||||
if result.status != HCI_COMMAND_STATUS_PENDING:
|
if result.status != HCI_COMMAND_STATUS_PENDING:
|
||||||
logger.warn(f'HCI_Authentication_Requested_Command failed: {HCI_Constant.error_name(result.status)}')
|
logger.warn(f'HCI_Authentication_Requested_Command failed: {HCI_Constant.error_name(result.status)}')
|
||||||
raise HCI_Error(result.status)
|
raise HCI_StatusError(result)
|
||||||
|
|
||||||
# Wait for the authentication to complete
|
# Wait for the authentication to complete
|
||||||
await pending_authentication
|
await pending_authentication
|
||||||
@@ -1057,7 +1112,7 @@ class Device(CompositeEventEmitter):
|
|||||||
raise InvalidStateError('only centrals can start encryption')
|
raise InvalidStateError('only centrals can start encryption')
|
||||||
|
|
||||||
result = await self.send_command(
|
result = await self.send_command(
|
||||||
HCI_LE_Start_Encryption_Command(
|
HCI_LE_Enable_Encryption_Command(
|
||||||
connection_handle = connection.handle,
|
connection_handle = connection.handle,
|
||||||
random_number = rand,
|
random_number = rand,
|
||||||
encrypted_diversifier = ediv,
|
encrypted_diversifier = ediv,
|
||||||
@@ -1066,8 +1121,8 @@ class Device(CompositeEventEmitter):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if result.status != HCI_COMMAND_STATUS_PENDING:
|
if result.status != HCI_COMMAND_STATUS_PENDING:
|
||||||
logger.warn(f'HCI_LE_Start_Encryption_Command failed: {HCI_Constant.error_name(result.status)}')
|
logger.warn(f'HCI_LE_Enable_Encryption_Command failed: {HCI_Constant.error_name(result.status)}')
|
||||||
raise HCI_Error(result.status)
|
raise HCI_StatusError(result)
|
||||||
else:
|
else:
|
||||||
result = await self.send_command(
|
result = await self.send_command(
|
||||||
HCI_Set_Connection_Encryption_Command(
|
HCI_Set_Connection_Encryption_Command(
|
||||||
@@ -1078,7 +1133,7 @@ class Device(CompositeEventEmitter):
|
|||||||
|
|
||||||
if result.status != HCI_COMMAND_STATUS_PENDING:
|
if result.status != HCI_COMMAND_STATUS_PENDING:
|
||||||
logger.warn(f'HCI_Set_Connection_Encryption_Command failed: {HCI_Constant.error_name(result.status)}')
|
logger.warn(f'HCI_Set_Connection_Encryption_Command failed: {HCI_Constant.error_name(result.status)}')
|
||||||
raise HCI_Error(result.status)
|
raise HCI_StatusError(result)
|
||||||
|
|
||||||
# Wait for the result
|
# Wait for the result
|
||||||
await pending_encryption
|
await pending_encryption
|
||||||
@@ -1112,7 +1167,7 @@ class Device(CompositeEventEmitter):
|
|||||||
|
|
||||||
if result.status != HCI_COMMAND_STATUS_PENDING:
|
if result.status != HCI_COMMAND_STATUS_PENDING:
|
||||||
logger.warn(f'HCI_Set_Connection_Encryption_Command failed: {HCI_Constant.error_name(result.status)}')
|
logger.warn(f'HCI_Set_Connection_Encryption_Command failed: {HCI_Constant.error_name(result.status)}')
|
||||||
raise HCI_Error(result.status)
|
raise HCI_StatusError(result)
|
||||||
|
|
||||||
# Wait for the result
|
# Wait for the result
|
||||||
return await pending_name
|
return await pending_name
|
||||||
|
|||||||
@@ -320,6 +320,12 @@ class CharacteristicAdapter:
|
|||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
return getattr(self.wrapped_characteristic, name)
|
return getattr(self.wrapped_characteristic, name)
|
||||||
|
|
||||||
|
def __setattr__(self, name, value):
|
||||||
|
if name in {'wrapped_characteristic', 'read_value', 'write_value', 'subscribe'}:
|
||||||
|
super().__setattr__(name, value)
|
||||||
|
else:
|
||||||
|
setattr(self.wrapped_characteristic, name, value)
|
||||||
|
|
||||||
def read_encoded_value(self, connection):
|
def read_encoded_value(self, connection):
|
||||||
return self.encode_value(self.wrapped_characteristic.read_value(connection))
|
return self.encode_value(self.wrapped_characteristic.read_value(connection))
|
||||||
|
|
||||||
@@ -343,6 +349,10 @@ class CharacteristicAdapter:
|
|||||||
None if subscriber is None else lambda value: subscriber(self.decode_value(value))
|
None if subscriber is None else lambda value: subscriber(self.decode_value(value))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
wrapped = str(self.wrapped_characteristic)
|
||||||
|
return f'{self.__class__.__name__}({wrapped})'
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class DelegatedCharacteristicAdapter(CharacteristicAdapter):
|
class DelegatedCharacteristicAdapter(CharacteristicAdapter):
|
||||||
|
|||||||
+38
-3
@@ -110,6 +110,9 @@ class CharacteristicProxy(AttributeProxy):
|
|||||||
async def subscribe(self, subscriber=None):
|
async def subscribe(self, subscriber=None):
|
||||||
return await self.client.subscribe(self, subscriber)
|
return await self.client.subscribe(self, subscriber)
|
||||||
|
|
||||||
|
async def unsubscribe(self, subscriber=None):
|
||||||
|
return await self.client.unsubscribe(self, subscriber)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'Characteristic(handle=0x{self.handle:04X}, uuid={self.uuid}, properties={Characteristic.properties_as_string(self.properties)})'
|
return f'Characteristic(handle=0x{self.handle:04X}, uuid={self.uuid}, properties={Characteristic.properties_as_string(self.properties)})'
|
||||||
|
|
||||||
@@ -544,10 +547,36 @@ class Client:
|
|||||||
for subscriber_set in subscriber_sets:
|
for subscriber_set in subscriber_sets:
|
||||||
if subscriber is not None:
|
if subscriber is not None:
|
||||||
subscriber_set.add(subscriber)
|
subscriber_set.add(subscriber)
|
||||||
subscriber_set.add(lambda value: characteristic.emit('update', self.connection, value))
|
# Add the characteristic as a subscriber, which will result in the characteristic
|
||||||
|
# emitting an 'update' event when a notification or indication is received
|
||||||
|
subscriber_set.add(characteristic)
|
||||||
|
|
||||||
await self.write_value(cccd, struct.pack('<H', bits), with_response=True)
|
await self.write_value(cccd, struct.pack('<H', bits), with_response=True)
|
||||||
|
|
||||||
|
async def unsubscribe(self, characteristic, subscriber=None):
|
||||||
|
# If we haven't already discovered the descriptors for this characteristic, do it now
|
||||||
|
if not characteristic.descriptors_discovered:
|
||||||
|
await self.discover_descriptors(characteristic)
|
||||||
|
|
||||||
|
# Look for the CCCD descriptor
|
||||||
|
cccd = characteristic.get_descriptor(GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR)
|
||||||
|
if not cccd:
|
||||||
|
logger.warning('unsubscribing from characteristic with no CCCD descriptor')
|
||||||
|
return
|
||||||
|
|
||||||
|
if subscriber is not None:
|
||||||
|
# Remove matching subscriber from subscriber sets
|
||||||
|
for subscriber_set in (self.notification_subscribers, self.indication_subscribers):
|
||||||
|
subscribers = subscriber_set.get(characteristic.handle, [])
|
||||||
|
if subscriber in subscribers:
|
||||||
|
subscribers.remove(subscriber)
|
||||||
|
else:
|
||||||
|
# Remove all subscribers for this attribute from the sets!
|
||||||
|
self.notification_subscribers.pop(characteristic.handle, None)
|
||||||
|
self.indication_subscribers.pop(characteristic.handle, None)
|
||||||
|
|
||||||
|
await self.write_value(cccd, b'\x00\x00', with_response=True)
|
||||||
|
|
||||||
async def read_value(self, attribute, no_long_read=False):
|
async def read_value(self, attribute, no_long_read=False):
|
||||||
'''
|
'''
|
||||||
See Vol 3, Part G - 4.8.1 Read Characteristic Value
|
See Vol 3, Part G - 4.8.1 Read Characteristic Value
|
||||||
@@ -714,7 +743,10 @@ class Client:
|
|||||||
if not subscribers:
|
if not subscribers:
|
||||||
logger.warning('!!! received notification with no subscriber')
|
logger.warning('!!! received notification with no subscriber')
|
||||||
for subscriber in subscribers:
|
for subscriber in subscribers:
|
||||||
subscriber(notification.attribute_value)
|
if callable(subscriber):
|
||||||
|
subscriber(notification.attribute_value)
|
||||||
|
else:
|
||||||
|
subscriber.emit('update', notification.attribute_value)
|
||||||
|
|
||||||
def on_att_handle_value_indication(self, indication):
|
def on_att_handle_value_indication(self, indication):
|
||||||
# Call all subscribers
|
# Call all subscribers
|
||||||
@@ -722,7 +754,10 @@ class Client:
|
|||||||
if not subscribers:
|
if not subscribers:
|
||||||
logger.warning('!!! received indication with no subscriber')
|
logger.warning('!!! received indication with no subscriber')
|
||||||
for subscriber in subscribers:
|
for subscriber in subscribers:
|
||||||
subscriber(indication.attribute_value)
|
if callable(subscriber):
|
||||||
|
subscriber(indication.attribute_value)
|
||||||
|
else:
|
||||||
|
subscriber.emit('update', indication.attribute_value)
|
||||||
|
|
||||||
# Confirm that we received the indication
|
# Confirm that we received the indication
|
||||||
self.send_confirmation(ATT_Handle_Value_Confirmation())
|
self.send_confirmation(ATT_Handle_Value_Confirmation())
|
||||||
|
|||||||
@@ -545,13 +545,13 @@ class Server(EventEmitter):
|
|||||||
value = attribute.read_value(connection)
|
value = attribute.read_value(connection)
|
||||||
if request.value_offset > len(value):
|
if request.value_offset > len(value):
|
||||||
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 = ATT_INVALID_OFFSET_ERROR
|
||||||
)
|
)
|
||||||
elif len(value) <= mtu - 1:
|
elif len(value) <= mtu - 1:
|
||||||
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_ATTRIBUTE_NOT_LONG_ERROR
|
error_code = ATT_ATTRIBUTE_NOT_LONG_ERROR
|
||||||
)
|
)
|
||||||
|
|||||||
+1245
-541
File diff suppressed because it is too large
Load Diff
+71
-21
@@ -81,7 +81,9 @@ class Host(EventEmitter):
|
|||||||
self.hc_total_num_acl_data_packets = HOST_HC_TOTAL_NUM_ACL_DATA_PACKETS
|
self.hc_total_num_acl_data_packets = HOST_HC_TOTAL_NUM_ACL_DATA_PACKETS
|
||||||
self.acl_packet_queue = collections.deque()
|
self.acl_packet_queue = collections.deque()
|
||||||
self.acl_packets_in_flight = 0
|
self.acl_packets_in_flight = 0
|
||||||
|
self.local_version = None
|
||||||
self.local_supported_commands = bytes(64)
|
self.local_supported_commands = bytes(64)
|
||||||
|
self.local_le_features = 0
|
||||||
self.command_semaphore = asyncio.Semaphore(1)
|
self.command_semaphore = asyncio.Semaphore(1)
|
||||||
self.long_term_key_provider = None
|
self.long_term_key_provider = None
|
||||||
self.link_key_provider = None
|
self.link_key_provider = None
|
||||||
@@ -97,34 +99,51 @@ class Host(EventEmitter):
|
|||||||
await self.send_command(HCI_Reset_Command())
|
await self.send_command(HCI_Reset_Command())
|
||||||
self.ready = True
|
self.ready = True
|
||||||
|
|
||||||
response = await self.send_command(HCI_Read_Local_Supported_Commands_Command())
|
|
||||||
if response.return_parameters.status != HCI_SUCCESS:
|
|
||||||
raise ProtocolError(response.return_parameters.status, 'hci')
|
|
||||||
self.local_supported_commands = response.return_parameters.supported_commands
|
|
||||||
|
|
||||||
await self.send_command(HCI_Set_Event_Mask_Command(event_mask = bytes.fromhex('FFFFFFFFFFFFFFFF')))
|
await self.send_command(HCI_Set_Event_Mask_Command(event_mask = bytes.fromhex('FFFFFFFFFFFFFFFF')))
|
||||||
await self.send_command(HCI_LE_Set_Event_Mask_Command(le_event_mask = bytes.fromhex('FFFFF00000000000')))
|
await self.send_command(HCI_LE_Set_Event_Mask_Command(le_event_mask = bytes.fromhex('FFFFF00000000000')))
|
||||||
await self.send_command(HCI_Read_Local_Version_Information_Command())
|
|
||||||
await self.send_command(HCI_Write_LE_Host_Support_Command(le_supported_host = 1, simultaneous_le_host = 0))
|
|
||||||
|
|
||||||
response = await self.send_command(HCI_LE_Read_Buffer_Size_Command())
|
response = await self.send_command(HCI_Read_Local_Supported_Commands_Command())
|
||||||
if response.return_parameters.status == HCI_SUCCESS:
|
if response.return_parameters.status == HCI_SUCCESS:
|
||||||
self.hc_le_acl_data_packet_length = response.return_parameters.hc_le_acl_data_packet_length
|
self.local_supported_commands = response.return_parameters.supported_commands
|
||||||
self.hc_total_num_le_acl_data_packets = response.return_parameters.hc_total_num_le_acl_data_packets
|
|
||||||
logger.debug(f'HCI LE ACL flow control: hc_le_acl_data_packet_length={response.return_parameters.hc_le_acl_data_packet_length}, hc_total_num_le_acl_data_packets={response.return_parameters.hc_total_num_le_acl_data_packets}')
|
|
||||||
else:
|
else:
|
||||||
logger.warn(f'HCI_LE_Read_Buffer_Size_Command failed: {response.return_parameters.status}')
|
logger.warn(f'HCI_Read_Local_Supported_Commands_Command failed: {response.return_parameters.status}')
|
||||||
if response.return_parameters.hc_le_acl_data_packet_length == 0 or response.return_parameters.hc_total_num_le_acl_data_packets == 0:
|
|
||||||
# Read the non-LE-specific values
|
if self.supports_command(HCI_WRITE_LE_HOST_SUPPORT_COMMAND):
|
||||||
response = await self.send_command(HCI_Read_Buffer_Size_Command())
|
await self.send_command(HCI_Write_LE_Host_Support_Command(le_supported_host = 1, simultaneous_le_host = 0))
|
||||||
|
|
||||||
|
if self.supports_command(HCI_READ_LOCAL_VERSION_INFORMATION_COMMAND):
|
||||||
|
response = await self.send_command(HCI_Read_Local_Version_Information_Command())
|
||||||
if response.return_parameters.status == HCI_SUCCESS:
|
if response.return_parameters.status == HCI_SUCCESS:
|
||||||
self.hc_acl_data_packet_length = response.return_parameters.hc_le_acl_data_packet_length
|
self.local_version = response.return_parameters
|
||||||
self.hc_le_acl_data_packet_length = self.hc_le_acl_data_packet_length or self.hc_acl_data_packet_length
|
|
||||||
self.hc_total_num_acl_data_packets = response.return_parameters.hc_total_num_le_acl_data_packets
|
|
||||||
self.hc_total_num_le_acl_data_packets = self.hc_total_num_le_acl_data_packets or self.hc_total_num_acl_data_packets
|
|
||||||
logger.debug(f'HCI LE ACL flow control: hc_le_acl_data_packet_length={self.hc_le_acl_data_packet_length}, hc_total_num_le_acl_data_packets={self.hc_total_num_le_acl_data_packets}')
|
|
||||||
else:
|
else:
|
||||||
logger.warn(f'HCI_Read_Buffer_Size_Command failed: {response.return_parameters.status}')
|
logger.warn(f'HCI_Read_Local_Version_Information_Command failed: {response.return_parameters.status}')
|
||||||
|
|
||||||
|
if self.supports_command(HCI_LE_READ_BUFFER_SIZE_COMMAND):
|
||||||
|
response = await self.send_command(HCI_LE_Read_Buffer_Size_Command())
|
||||||
|
if response.return_parameters.status == HCI_SUCCESS:
|
||||||
|
self.hc_le_acl_data_packet_length = response.return_parameters.hc_le_acl_data_packet_length
|
||||||
|
self.hc_total_num_le_acl_data_packets = response.return_parameters.hc_total_num_le_acl_data_packets
|
||||||
|
logger.debug(f'HCI LE ACL flow control: hc_le_acl_data_packet_length={response.return_parameters.hc_le_acl_data_packet_length}, hc_total_num_le_acl_data_packets={response.return_parameters.hc_total_num_le_acl_data_packets}')
|
||||||
|
else:
|
||||||
|
logger.warn(f'HCI_LE_Read_Buffer_Size_Command failed: {response.return_parameters.status}')
|
||||||
|
if response.return_parameters.hc_le_acl_data_packet_length == 0 or response.return_parameters.hc_total_num_le_acl_data_packets == 0:
|
||||||
|
# Read the non-LE-specific values
|
||||||
|
response = await self.send_command(HCI_Read_Buffer_Size_Command())
|
||||||
|
if response.return_parameters.status == HCI_SUCCESS:
|
||||||
|
self.hc_acl_data_packet_length = response.return_parameters.hc_le_acl_data_packet_length
|
||||||
|
self.hc_le_acl_data_packet_length = self.hc_le_acl_data_packet_length or self.hc_acl_data_packet_length
|
||||||
|
self.hc_total_num_acl_data_packets = response.return_parameters.hc_total_num_le_acl_data_packets
|
||||||
|
self.hc_total_num_le_acl_data_packets = self.hc_total_num_le_acl_data_packets or self.hc_total_num_acl_data_packets
|
||||||
|
logger.debug(f'HCI LE ACL flow control: hc_le_acl_data_packet_length={self.hc_le_acl_data_packet_length}, hc_total_num_le_acl_data_packets={self.hc_total_num_le_acl_data_packets}')
|
||||||
|
else:
|
||||||
|
logger.warn(f'HCI_Read_Buffer_Size_Command failed: {response.return_parameters.status}')
|
||||||
|
|
||||||
|
if self.supports_command(HCI_LE_READ_LOCAL_SUPPORTED_FEATURES_COMMAND):
|
||||||
|
response = await self.send_command(HCI_LE_Read_Local_Supported_Features_Command())
|
||||||
|
if response.return_parameters.status == HCI_SUCCESS:
|
||||||
|
self.local_le_features = struct.unpack('<Q', response.return_parameters.le_features)[0]
|
||||||
|
else:
|
||||||
|
logger.warn(f'HCI_LE_Read_Supported_Features_Command failed: {response.return_parameters.status}')
|
||||||
|
|
||||||
self.reset_done = True
|
self.reset_done = True
|
||||||
|
|
||||||
@@ -211,6 +230,37 @@ class Host(EventEmitter):
|
|||||||
self.send_hci_packet(packet)
|
self.send_hci_packet(packet)
|
||||||
self.acl_packets_in_flight += 1
|
self.acl_packets_in_flight += 1
|
||||||
|
|
||||||
|
def supports_command(self, command):
|
||||||
|
# Find the support flag position for this command
|
||||||
|
for (octet, flags) in enumerate(HCI_SUPPORTED_COMMANDS_FLAGS):
|
||||||
|
for (flag_position, value) in enumerate(flags):
|
||||||
|
if value == command:
|
||||||
|
# Check if the flag is set
|
||||||
|
if octet < len(self.local_supported_commands) and flag_position < 8:
|
||||||
|
return (self.local_supported_commands[octet] & (1 << flag_position)) != 0
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_commands(self):
|
||||||
|
commands = []
|
||||||
|
for (octet, flags) in enumerate(self.local_supported_commands):
|
||||||
|
if octet < len(HCI_SUPPORTED_COMMANDS_FLAGS):
|
||||||
|
for flag in range(8):
|
||||||
|
if flags & (1 << flag) != 0:
|
||||||
|
command = HCI_SUPPORTED_COMMANDS_FLAGS[octet][flag]
|
||||||
|
if command is not None:
|
||||||
|
commands.append(command)
|
||||||
|
|
||||||
|
return commands
|
||||||
|
|
||||||
|
def supports_le_feature(self, feature):
|
||||||
|
return (self.local_le_features & (1 << feature)) != 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_le_features(self):
|
||||||
|
return [feature for feature in range(64) if self.local_le_features & (1 << feature)]
|
||||||
|
|
||||||
# Packet Sink protocol (packets coming from the controller via HCI)
|
# Packet Sink protocol (packets coming from the controller via HCI)
|
||||||
def on_packet(self, packet):
|
def on_packet(self, packet):
|
||||||
hci_packet = HCI_Packet.from_bytes(packet)
|
hci_packet = HCI_Packet.from_bytes(packet)
|
||||||
|
|||||||
+1
-1
@@ -868,7 +868,7 @@ class Session:
|
|||||||
# distribute the long term and/or other keys over an encrypted connection
|
# distribute the long term and/or other keys over an encrypted connection
|
||||||
asyncio.create_task(
|
asyncio.create_task(
|
||||||
self.manager.device.host.send_command(
|
self.manager.device.host.send_command(
|
||||||
HCI_LE_Start_Encryption_Command(
|
HCI_LE_Enable_Encryption_Command(
|
||||||
connection_handle = self.connection.handle,
|
connection_handle = self.connection.handle,
|
||||||
random_number = bytes(8),
|
random_number = bytes(8),
|
||||||
encrypted_diversifier = 0,
|
encrypted_diversifier = 0,
|
||||||
|
|||||||
+15
-19
@@ -43,28 +43,24 @@ async def main():
|
|||||||
# Connect to the peer
|
# Connect to the peer
|
||||||
target_address = sys.argv[2]
|
target_address = sys.argv[2]
|
||||||
print(f'=== Connecting to {target_address}...')
|
print(f'=== Connecting to {target_address}...')
|
||||||
connection = await device.connect(target_address)
|
async with device.connect_as_gatt(target_address) as peer:
|
||||||
print(f'=== Connected to {connection}')
|
print(f'=== Connected to {peer}')
|
||||||
|
battery_service = peer.create_service_proxy(BatteryServiceProxy)
|
||||||
|
|
||||||
# Discover the Battery Service
|
# Check that the service was found
|
||||||
peer = Peer(connection)
|
if not battery_service:
|
||||||
print('=== Discovering Battery Service')
|
print('!!! Service not found')
|
||||||
battery_service = await peer.discover_service_and_create_proxy(BatteryServiceProxy)
|
return
|
||||||
|
|
||||||
# Check that the service was found
|
# Subscribe to and read the battery level
|
||||||
if not battery_service:
|
if battery_service.battery_level:
|
||||||
print('!!! Service not found')
|
await battery_service.battery_level.subscribe(
|
||||||
return
|
lambda value: print(f'{color("Battery Level Update:", "green")} {value}')
|
||||||
|
)
|
||||||
|
value = await battery_service.battery_level.read_value()
|
||||||
|
print(f'{color("Initial Battery Level:", "green")} {value}')
|
||||||
|
|
||||||
# Subscribe to and read the battery level
|
await peer.sustain()
|
||||||
if battery_service.battery_level:
|
|
||||||
await battery_service.battery_level.subscribe(
|
|
||||||
lambda value: print(f'{color("Battery Level Update:", "green")} {value}')
|
|
||||||
)
|
|
||||||
value = await battery_service.battery_level.read_value()
|
|
||||||
print(f'{color("Initial Battery Level:", "green")} {value}')
|
|
||||||
|
|
||||||
await hci_source.wait_for_termination()
|
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -43,31 +43,28 @@ async def main():
|
|||||||
# Connect to the peer
|
# Connect to the peer
|
||||||
target_address = sys.argv[2]
|
target_address = sys.argv[2]
|
||||||
print(f'=== Connecting to {target_address}...')
|
print(f'=== Connecting to {target_address}...')
|
||||||
connection = await device.connect(target_address)
|
async with device.connect_as_gatt(target_address) as peer:
|
||||||
print(f'=== Connected to {connection}')
|
print(f'=== Connected to {peer}')
|
||||||
|
|
||||||
# Discover the Heart Rate Service
|
heart_rate_service = peer.create_service_proxy(HeartRateServiceProxy)
|
||||||
peer = Peer(connection)
|
|
||||||
print('=== Discovering Heart Rate Service')
|
|
||||||
heart_rate_service = await peer.discover_service_and_create_proxy(HeartRateServiceProxy)
|
|
||||||
|
|
||||||
# Check that the service was found
|
# Check that the service was found
|
||||||
if not heart_rate_service:
|
if not heart_rate_service:
|
||||||
print('!!! Service not found')
|
print('!!! Service not found')
|
||||||
return
|
return
|
||||||
|
|
||||||
# Read the body sensor location
|
# Read the body sensor location
|
||||||
if heart_rate_service.body_sensor_location:
|
if heart_rate_service.body_sensor_location:
|
||||||
location = await heart_rate_service.body_sensor_location.read_value()
|
location = await heart_rate_service.body_sensor_location.read_value()
|
||||||
print(color('Sensor Location:', 'green'), location)
|
print(color('Sensor Location:', 'green'), location)
|
||||||
|
|
||||||
# Subscribe to the heart rate measurement
|
# Subscribe to the heart rate measurement
|
||||||
if heart_rate_service.heart_rate_measurement:
|
if heart_rate_service.heart_rate_measurement:
|
||||||
await heart_rate_service.heart_rate_measurement.subscribe(
|
await heart_rate_service.heart_rate_measurement.subscribe(
|
||||||
lambda value: print(f'{color("Heart Rate Measurement:", "green")} {value}')
|
lambda value: print(f'{color("Heart Rate Measurement:", "green")} {value}')
|
||||||
)
|
)
|
||||||
|
|
||||||
await hci_source.wait_for_termination()
|
await peer.sustain()
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -16,13 +16,15 @@
|
|||||||
name = bumble
|
name = bumble
|
||||||
use_scm_version = True
|
use_scm_version = True
|
||||||
description = Bluetooth Stack for Apps, Emulation, Test and Experimentation
|
description = Bluetooth Stack for Apps, Emulation, Test and Experimentation
|
||||||
|
long_description = file: README.md
|
||||||
|
long_description_content_type = text/markdown
|
||||||
author = Google
|
author = Google
|
||||||
author_email = tbd@tbd.com
|
author_email = tbd@tbd.com
|
||||||
url = https://github.com/google/bumble
|
url = https://github.com/google/bumble
|
||||||
|
|
||||||
[options]
|
[options]
|
||||||
python_requires = >=3.8
|
python_requires = >=3.8
|
||||||
packages = bumble, bumble.transport, bumble.apps, bumble.apps.link_relay
|
packages = bumble, bumble.transport, bumble.profiles, bumble.apps, bumble.apps.link_relay
|
||||||
package_dir =
|
package_dir =
|
||||||
bumble = bumble
|
bumble = bumble
|
||||||
bumble.apps = apps
|
bumble.apps = apps
|
||||||
|
|||||||
+48
-17
@@ -419,10 +419,12 @@ async def test_subscribe_notify():
|
|||||||
assert(len(c) == 1)
|
assert(len(c) == 1)
|
||||||
c3 = c[0]
|
c3 = c[0]
|
||||||
|
|
||||||
|
c1._called = False
|
||||||
c1._last_update = None
|
c1._last_update = None
|
||||||
|
|
||||||
def on_c1_update(connection, value):
|
def on_c1_update(value):
|
||||||
c1._last_update = (connection, value)
|
c1._called = True
|
||||||
|
c1._last_update = value
|
||||||
|
|
||||||
c1.on('update', on_c1_update)
|
c1.on('update', on_c1_update)
|
||||||
await peer.subscribe(c1)
|
await peer.subscribe(c1)
|
||||||
@@ -434,44 +436,73 @@ async def test_subscribe_notify():
|
|||||||
assert(not characteristic1._last_subscription[2])
|
assert(not characteristic1._last_subscription[2])
|
||||||
await server.indicate_subscribers(characteristic1)
|
await server.indicate_subscribers(characteristic1)
|
||||||
await async_barrier()
|
await async_barrier()
|
||||||
assert(c1._last_update is None)
|
assert(not c1._called)
|
||||||
await server.notify_subscribers(characteristic1)
|
await server.notify_subscribers(characteristic1)
|
||||||
await async_barrier()
|
await async_barrier()
|
||||||
assert(c1._last_update is not None)
|
assert(c1._called)
|
||||||
assert(c1._last_update[1] == characteristic1.value)
|
assert(c1._last_update == characteristic1.value)
|
||||||
|
|
||||||
|
c1._called = False
|
||||||
|
await peer.unsubscribe(c1)
|
||||||
|
await server.notify_subscribers(characteristic1)
|
||||||
|
assert(not c1._called)
|
||||||
|
|
||||||
|
c2._called = False
|
||||||
c2._last_update = None
|
c2._last_update = None
|
||||||
|
|
||||||
def on_c2_update(value):
|
def on_c2_update(value):
|
||||||
c2._last_update = (connection, value)
|
c2._called = True
|
||||||
|
c2._last_update = value
|
||||||
|
|
||||||
await peer.subscribe(c2, on_c2_update)
|
await peer.subscribe(c2, on_c2_update)
|
||||||
await async_barrier()
|
await async_barrier()
|
||||||
await server.notify_subscriber(characteristic2._last_subscription[0], characteristic2)
|
await server.notify_subscriber(characteristic2._last_subscription[0], characteristic2)
|
||||||
await async_barrier()
|
await async_barrier()
|
||||||
assert(c2._last_update is None)
|
assert(not c2._called)
|
||||||
await server.indicate_subscriber(characteristic2._last_subscription[0], characteristic2)
|
await server.indicate_subscriber(characteristic2._last_subscription[0], characteristic2)
|
||||||
await async_barrier()
|
await async_barrier()
|
||||||
assert(c2._last_update is not None)
|
assert(c2._called)
|
||||||
assert(c2._last_update[1] == characteristic2.value)
|
assert(c2._last_update == characteristic2.value)
|
||||||
|
|
||||||
c3._last_update = None
|
c2._called = False
|
||||||
|
await peer.unsubscribe(c2, on_c2_update)
|
||||||
|
await server.indicate_subscriber(characteristic2._last_subscription[0], characteristic2)
|
||||||
|
await async_barrier()
|
||||||
|
assert(not c2._called)
|
||||||
|
|
||||||
def on_c3_update(connection, value):
|
def on_c3_update(value):
|
||||||
c3._last_update = (connection, value)
|
c3._called = True
|
||||||
|
c3._last_update = value
|
||||||
|
|
||||||
|
def on_c3_update_2(value):
|
||||||
|
c3._called_2 = True
|
||||||
|
c3._last_update_2 = value
|
||||||
|
|
||||||
c3.on('update', on_c3_update)
|
c3.on('update', on_c3_update)
|
||||||
await peer.subscribe(c3)
|
await peer.subscribe(c3, on_c3_update_2)
|
||||||
await async_barrier()
|
await async_barrier()
|
||||||
await server.notify_subscriber(characteristic3._last_subscription[0], characteristic3)
|
await server.notify_subscriber(characteristic3._last_subscription[0], characteristic3)
|
||||||
await async_barrier()
|
await async_barrier()
|
||||||
assert(c3._last_update is not None)
|
assert(c3._called)
|
||||||
assert(c3._last_update[1] == characteristic3.value)
|
assert(c3._last_update == characteristic3.value)
|
||||||
|
assert(c3._called_2)
|
||||||
|
assert(c3._last_update_2 == characteristic3.value)
|
||||||
characteristic3.value = bytes([1, 2, 3])
|
characteristic3.value = bytes([1, 2, 3])
|
||||||
await server.indicate_subscriber(characteristic3._last_subscription[0], characteristic3)
|
await server.indicate_subscriber(characteristic3._last_subscription[0], characteristic3)
|
||||||
await async_barrier()
|
await async_barrier()
|
||||||
assert(c3._last_update is not None)
|
assert(c3._called)
|
||||||
assert(c3._last_update[1] == characteristic3.value)
|
assert(c3._last_update == characteristic3.value)
|
||||||
|
assert(c3._called_2)
|
||||||
|
assert(c3._last_update_2 == characteristic3.value)
|
||||||
|
|
||||||
|
c3._called = False
|
||||||
|
c3._called_2 = False
|
||||||
|
await peer.unsubscribe(c3)
|
||||||
|
await server.notify_subscriber(characteristic3._last_subscription[0], characteristic3)
|
||||||
|
await server.indicate_subscriber(characteristic3._last_subscription[0], characteristic3)
|
||||||
|
await async_barrier()
|
||||||
|
assert(not c3._called)
|
||||||
|
assert(not c3._called_2)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|||||||
+24
-6
@@ -294,8 +294,8 @@ def test_HCI_LE_Create_Connection_Command():
|
|||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
def test_HCI_LE_Add_Device_To_White_List_Command():
|
def test_HCI_LE_Add_Device_To_Filter_Accept_List_Command():
|
||||||
command = HCI_LE_Add_Device_To_White_List_Command(
|
command = HCI_LE_Add_Device_To_Filter_Accept_List_Command(
|
||||||
address_type = 1,
|
address_type = 1,
|
||||||
address = Address('00:11:22:33:44:55')
|
address = Address('00:11:22:33:44:55')
|
||||||
)
|
)
|
||||||
@@ -303,8 +303,8 @@ def test_HCI_LE_Add_Device_To_White_List_Command():
|
|||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
def test_HCI_LE_Remove_Device_From_White_List_Command():
|
def test_HCI_LE_Remove_Device_From_Filter_Accept_List_Command():
|
||||||
command = HCI_LE_Remove_Device_From_White_List_Command(
|
command = HCI_LE_Remove_Device_From_Filter_Accept_List_Command(
|
||||||
address_type = 1,
|
address_type = 1,
|
||||||
address = Address('00:11:22:33:44:55')
|
address = Address('00:11:22:33:44:55')
|
||||||
)
|
)
|
||||||
@@ -343,6 +343,23 @@ def test_HCI_LE_Set_Default_PHY_Command():
|
|||||||
basic_check(command)
|
basic_check(command)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def test_HCI_LE_Set_Extended_Scan_Parameters_Command():
|
||||||
|
command = HCI_LE_Set_Extended_Scan_Parameters_Command(
|
||||||
|
own_address_type=Address.RANDOM_DEVICE_ADDRESS,
|
||||||
|
scanning_filter_policy=HCI_LE_Set_Extended_Scan_Parameters_Command.BASIC_FILTERED_POLICY,
|
||||||
|
scanning_phys=(1 << HCI_LE_Set_Extended_Scan_Parameters_Command.LE_1M_PHY | 1 << HCI_LE_Set_Extended_Scan_Parameters_Command.LE_CODED_PHY | 1 << 4),
|
||||||
|
scan_types=[
|
||||||
|
HCI_LE_Set_Extended_Scan_Parameters_Command.ACTIVE_SCANNING,
|
||||||
|
HCI_LE_Set_Extended_Scan_Parameters_Command.ACTIVE_SCANNING,
|
||||||
|
HCI_LE_Set_Extended_Scan_Parameters_Command.PASSIVE_SCANNING
|
||||||
|
],
|
||||||
|
scan_intervals=[1, 2, 3],
|
||||||
|
scan_windows=[4, 5, 6]
|
||||||
|
)
|
||||||
|
basic_check(command)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
def test_address():
|
def test_address():
|
||||||
a = Address('C4:F2:17:1A:1D:BB')
|
a = Address('C4:F2:17:1A:1D:BB')
|
||||||
@@ -391,11 +408,12 @@ def run_test_commands():
|
|||||||
test_HCI_LE_Set_Scan_Parameters_Command()
|
test_HCI_LE_Set_Scan_Parameters_Command()
|
||||||
test_HCI_LE_Set_Scan_Enable_Command()
|
test_HCI_LE_Set_Scan_Enable_Command()
|
||||||
test_HCI_LE_Create_Connection_Command()
|
test_HCI_LE_Create_Connection_Command()
|
||||||
test_HCI_LE_Add_Device_To_White_List_Command()
|
test_HCI_LE_Add_Device_To_Filter_Accept_List_Command()
|
||||||
test_HCI_LE_Remove_Device_From_White_List_Command()
|
test_HCI_LE_Remove_Device_From_Filter_Accept_List_Command()
|
||||||
test_HCI_LE_Connection_Update_Command()
|
test_HCI_LE_Connection_Update_Command()
|
||||||
test_HCI_LE_Read_Remote_Features_Command()
|
test_HCI_LE_Read_Remote_Features_Command()
|
||||||
test_HCI_LE_Set_Default_PHY_Command()
|
test_HCI_LE_Set_Default_PHY_Command()
|
||||||
|
test_HCI_LE_Set_Extended_Scan_Parameters_Command()
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -163,6 +163,44 @@ async def test_self_gatt():
|
|||||||
assert(result == c1.value)
|
assert(result == c1.value)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_self_gatt_long_read():
|
||||||
|
# Create two devices, each with a controller, attached to the same link
|
||||||
|
two_devices = TwoDevices()
|
||||||
|
|
||||||
|
# Add some GATT characteristics to device 1
|
||||||
|
characteristics = [
|
||||||
|
Characteristic(
|
||||||
|
f'3A143AD7-D4A7-436B-97D6-5B62C315{i:04X}',
|
||||||
|
Characteristic.READ,
|
||||||
|
Characteristic.READABLE,
|
||||||
|
bytes([x & 255 for x in range(i)])
|
||||||
|
)
|
||||||
|
for i in range(0, 513)
|
||||||
|
]
|
||||||
|
|
||||||
|
service = Service('8140E247-04F0-42C1-BC34-534C344DAFCA', characteristics)
|
||||||
|
two_devices.devices[1].add_service(service)
|
||||||
|
|
||||||
|
# Start
|
||||||
|
await two_devices.devices[0].power_on()
|
||||||
|
await two_devices.devices[1].power_on()
|
||||||
|
|
||||||
|
# Connect the two devices
|
||||||
|
connection = await two_devices.devices[0].connect(two_devices.devices[1].random_address)
|
||||||
|
peer = Peer(connection)
|
||||||
|
|
||||||
|
result = await peer.discover_service(service.uuid)
|
||||||
|
assert(len(result) == 1)
|
||||||
|
found_service = result[0]
|
||||||
|
found_characteristics = await found_service.discover_characteristics()
|
||||||
|
assert(len(found_characteristics) == 513)
|
||||||
|
for (i, characteristic) in enumerate(found_characteristics):
|
||||||
|
value = await characteristic.read_value()
|
||||||
|
assert(value == characteristics[i].value)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
async def _test_self_smp_with_configs(pairing_config1, pairing_config2):
|
async def _test_self_smp_with_configs(pairing_config1, pairing_config2):
|
||||||
# Create two devices, each with a controller, attached to the same link
|
# Create two devices, each with a controller, attached to the same link
|
||||||
@@ -323,6 +361,7 @@ async def test_self_smp_wrong_pin():
|
|||||||
async def run_test_self():
|
async def run_test_self():
|
||||||
await test_self_connection()
|
await test_self_connection()
|
||||||
await test_self_gatt()
|
await test_self_gatt()
|
||||||
|
await test_self_gatt_long_read()
|
||||||
await test_self_smp()
|
await test_self_smp()
|
||||||
await test_self_smp_reject()
|
await test_self_smp_reject()
|
||||||
await test_self_smp_wrong_pin()
|
await test_self_smp_wrong_pin()
|
||||||
|
|||||||
Reference in New Issue
Block a user