forked from auracaster/bumble_mirror
Compare commits
43 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 | |||
| 3040df3179 | |||
| c66b357de6 | |||
| e156ed3758 | |||
| 0ffed3deff | |||
| 2f949a1182 | |||
| 4e2fae5145 | |||
| 2b58364c51 | |||
| e3bf7c4b53 | |||
| 009ecfce96 | |||
| d6075df356 | |||
| ebd0a0c8ca | |||
| bd28892734 | |||
| b64fa65921 | |||
| 7d87c3cc3a | |||
| 94fc81c183 | |||
| b65b395fc4 | |||
| 0f157d55f7 | |||
| 925d79491f |
@@ -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 }}
|
||||||
@@ -8,3 +8,4 @@ docs/mkdocs/site
|
|||||||
tests/__pycache__
|
tests/__pycache__
|
||||||
test-results.xml
|
test-results.xml
|
||||||
bumble/transport/__pycache__
|
bumble/transport/__pycache__
|
||||||
|
bumble/profiles/__pycache__
|
||||||
|
|||||||
@@ -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()
|
||||||
+3
-3
@@ -32,10 +32,10 @@ async def dump_gatt_db(peer, done):
|
|||||||
# Discover all services
|
# Discover all services
|
||||||
print(color('### Discovering Services and Characteristics', 'magenta'))
|
print(color('### Discovering Services and Characteristics', 'magenta'))
|
||||||
await peer.discover_services()
|
await peer.discover_services()
|
||||||
await peer.discover_characteristics()
|
|
||||||
for service in peer.services:
|
for service in peer.services:
|
||||||
|
await service.discover_characteristics()
|
||||||
for characteristic in service.characteristics:
|
for characteristic in service.characteristics:
|
||||||
await peer.discover_descriptors(characteristic)
|
await characteristic.discover_descriptors()
|
||||||
|
|
||||||
print(color('=== Services ===', 'yellow'))
|
print(color('=== Services ===', 'yellow'))
|
||||||
show_services(peer.services)
|
show_services(peer.services)
|
||||||
@@ -47,7 +47,7 @@ async def dump_gatt_db(peer, done):
|
|||||||
for attribute in attributes:
|
for attribute in attributes:
|
||||||
print(attribute)
|
print(attribute)
|
||||||
try:
|
try:
|
||||||
value = await peer.read_value(attribute)
|
value = await attribute.read_value()
|
||||||
print(color(f'{value.hex()}', 'green'))
|
print(color(f'{value.hex()}', 'green'))
|
||||||
except ProtocolError as error:
|
except ProtocolError as error:
|
||||||
print(color(error, 'red'))
|
print(color(error, 'red'))
|
||||||
|
|||||||
+1
-1
@@ -73,7 +73,7 @@ class GattlinkHubBridge(Device.Listener):
|
|||||||
gattlink_service = services[0]
|
gattlink_service = services[0]
|
||||||
|
|
||||||
# Discover all the characteristics for the service
|
# Discover all the characteristics for the service
|
||||||
characteristics = await self.peer.discover_characteristics(service = gattlink_service)
|
characteristics = await gattlink_service.discover_characteristics()
|
||||||
print(color('=== Characteristics discovered', 'yellow'))
|
print(color('=== Characteristics discovered', 'yellow'))
|
||||||
for characteristic in characteristics:
|
for characteristic in characteristics:
|
||||||
if characteristic.uuid == GG_GATTLINK_RX_CHARACTERISTIC_UUID:
|
if characteristic.uuid == GG_GATTLINK_RX_CHARACTERISTIC_UUID:
|
||||||
|
|||||||
+13
-13
@@ -682,11 +682,14 @@ class Attribute(EventEmitter):
|
|||||||
|
|
||||||
def __init__(self, attribute_type, permissions, value = b''):
|
def __init__(self, attribute_type, permissions, value = b''):
|
||||||
EventEmitter.__init__(self)
|
EventEmitter.__init__(self)
|
||||||
self.handle = 0
|
self.handle = 0
|
||||||
self.permissions = permissions
|
self.end_group_handle = 0
|
||||||
|
self.permissions = permissions
|
||||||
|
|
||||||
# Convert the type to a UUID
|
# Convert the type to a UUID object if it isn't already
|
||||||
if type(attribute_type) is bytes:
|
if type(attribute_type) is str:
|
||||||
|
self.type = UUID(attribute_type)
|
||||||
|
elif type(attribute_type) is bytes:
|
||||||
self.type = UUID.from_bytes(attribute_type)
|
self.type = UUID.from_bytes(attribute_type)
|
||||||
else:
|
else:
|
||||||
self.type = attribute_type
|
self.type = attribute_type
|
||||||
@@ -698,16 +701,13 @@ class Attribute(EventEmitter):
|
|||||||
self.value = value
|
self.value = value
|
||||||
|
|
||||||
def read_value(self, connection):
|
def read_value(self, connection):
|
||||||
if type(self.value) is bytes:
|
if read := getattr(self.value, 'read', None):
|
||||||
return self.value
|
try:
|
||||||
|
return read(connection)
|
||||||
|
except ATT_Error as error:
|
||||||
|
raise ATT_Error(error_code=error.error_code, att_handle=self.handle)
|
||||||
else:
|
else:
|
||||||
if read := getattr(self.value, 'read', None):
|
return self.value
|
||||||
try:
|
|
||||||
return read(connection)
|
|
||||||
except ATT_Error as error:
|
|
||||||
raise ATT_Error(error_code=error.error_code, att_handle=self.handle)
|
|
||||||
else:
|
|
||||||
return bytes(self.value)
|
|
||||||
|
|
||||||
def write_value(self, connection, value):
|
def write_value(self, connection, value):
|
||||||
if write := getattr(self.value, 'write', None):
|
if write := getattr(self.value, 'write', None):
|
||||||
|
|||||||
+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
|
||||||
|
|||||||
@@ -227,3 +227,17 @@ def g2(u, v, x, y):
|
|||||||
aes_cmac(bytes(reversed(u)) + bytes(reversed(v)) + bytes(reversed(y)), bytes(reversed(x)))[-4:],
|
aes_cmac(bytes(reversed(u)) + bytes(reversed(v)) + bytes(reversed(y)), bytes(reversed(x)))[-4:],
|
||||||
byteorder='big'
|
byteorder='big'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def h6(w, key_id):
|
||||||
|
'''
|
||||||
|
See Bluetooth spec, Vol 3, Part H - 2.2.10 Link key conversion function h6
|
||||||
|
'''
|
||||||
|
return aes_cmac(key_id, w)
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def h7(salt, w):
|
||||||
|
'''
|
||||||
|
See Bluetooth spec, Vol 3, Part H - 2.2.11 Link key conversion function h7
|
||||||
|
'''
|
||||||
|
return aes_cmac(w, salt)
|
||||||
|
|||||||
+129
-40
@@ -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)
|
||||||
|
|
||||||
@@ -137,10 +141,35 @@ class Peer:
|
|||||||
def get_characteristics_by_uuid(self, uuid, service = None):
|
def get_characteristics_by_uuid(self, uuid, service = None):
|
||||||
return self.gatt_client.get_characteristics_by_uuid(uuid, service)
|
return self.gatt_client.get_characteristics_by_uuid(uuid, service)
|
||||||
|
|
||||||
|
def create_service_proxy(self, proxy_class):
|
||||||
|
return proxy_class.from_client(self.gatt_client)
|
||||||
|
|
||||||
|
async def discover_service_and_create_proxy(self, proxy_class):
|
||||||
|
# Discover the first matching service and its characteristics
|
||||||
|
services = await self.discover_service(proxy_class.SERVICE_CLASS.UUID)
|
||||||
|
if services:
|
||||||
|
service = services[0]
|
||||||
|
await service.discover_characteristics()
|
||||||
|
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}'
|
||||||
|
|
||||||
@@ -221,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,
|
||||||
@@ -240,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})'
|
||||||
|
|
||||||
@@ -254,39 +310,48 @@ class DeviceConfiguration:
|
|||||||
self.scan_response_data = DEVICE_DEFAULT_SCAN_RESPONSE_DATA
|
self.scan_response_data = DEVICE_DEFAULT_SCAN_RESPONSE_DATA
|
||||||
self.advertising_interval_min = DEVICE_DEFAULT_ADVERTISING_INTERVAL
|
self.advertising_interval_min = DEVICE_DEFAULT_ADVERTISING_INTERVAL
|
||||||
self.advertising_interval_max = DEVICE_DEFAULT_ADVERTISING_INTERVAL
|
self.advertising_interval_max = DEVICE_DEFAULT_ADVERTISING_INTERVAL
|
||||||
|
self.le_enabled = True
|
||||||
|
# LE host enable 2nd parameter
|
||||||
|
self.le_simultaneous_enabled = True
|
||||||
|
self.classic_sc_enabled = True
|
||||||
|
self.classic_ssp_enabled = True
|
||||||
self.advertising_data = bytes(
|
self.advertising_data = bytes(
|
||||||
AdvertisingData([(AdvertisingData.COMPLETE_LOCAL_NAME, bytes(self.name, 'utf-8'))])
|
AdvertisingData([(AdvertisingData.COMPLETE_LOCAL_NAME, bytes(self.name, 'utf-8'))])
|
||||||
)
|
)
|
||||||
self.irk = bytes(16) # This really must be changed for any level of security
|
self.irk = bytes(16) # This really must be changed for any level of security
|
||||||
self.keystore = None
|
self.keystore = None
|
||||||
|
|
||||||
|
def load_from_dict(self, config):
|
||||||
|
# Load simple properties
|
||||||
|
self.name = config.get('name', self.name)
|
||||||
|
self.address = Address(config.get('address', self.address))
|
||||||
|
self.class_of_device = config.get('class_of_device', self.class_of_device)
|
||||||
|
self.advertising_interval_min = config.get('advertising_interval', self.advertising_interval_min)
|
||||||
|
self.advertising_interval_max = self.advertising_interval_min
|
||||||
|
self.keystore = config.get('keystore')
|
||||||
|
self.le_enabled = config.get('le_enabled', self.le_enabled)
|
||||||
|
self.le_simultaneous_enabled = config.get('le_simultaneous_enabled', self.le_simultaneous_enabled)
|
||||||
|
self.classic_sc_enabled = config.get('classic_sc_enabled', self.classic_sc_enabled)
|
||||||
|
self.classic_ssp_enabled = config.get('classic_ssp_enabled', self.classic_ssp_enabled)
|
||||||
|
|
||||||
|
# Load or synthesize an IRK
|
||||||
|
irk = config.get('irk')
|
||||||
|
if irk:
|
||||||
|
self.irk = bytes.fromhex(irk)
|
||||||
|
else:
|
||||||
|
# Construct an IRK from the address bytes
|
||||||
|
# NOTE: this is not secure, but will always give the same IRK for the same address
|
||||||
|
address_bytes = bytes(self.address)
|
||||||
|
self.irk = (address_bytes * 3)[:16]
|
||||||
|
|
||||||
|
# Load advertising data
|
||||||
|
advertising_data = config.get('advertising_data')
|
||||||
|
if advertising_data:
|
||||||
|
self.advertising_data = bytes.fromhex(advertising_data)
|
||||||
|
|
||||||
def load_from_file(self, filename):
|
def load_from_file(self, filename):
|
||||||
with open(filename, 'r') as file:
|
with open(filename, 'r') as file:
|
||||||
config = json.load(file)
|
self.load_from_dict(json.load(file))
|
||||||
|
|
||||||
# Load simple properties
|
|
||||||
self.name = config.get('name', self.name)
|
|
||||||
self.address = Address(config.get('address', self.address))
|
|
||||||
self.class_of_device = config.get('class_of_device', self.class_of_device)
|
|
||||||
self.advertising_interval_min = config.get('advertising_interval', self.advertising_interval_min)
|
|
||||||
self.advertising_interval_max = self.advertising_interval_min
|
|
||||||
self.keystore = config.get('keystore')
|
|
||||||
|
|
||||||
# Load or synthesize an IRK
|
|
||||||
irk = config.get('irk')
|
|
||||||
if irk:
|
|
||||||
self.irk = bytes.fromhex(irk)
|
|
||||||
else:
|
|
||||||
# Construct an IRK from the address bytes
|
|
||||||
# NOTE: this is not secure, but will always give the same IRK for the same address
|
|
||||||
address_bytes = bytes(self.address)
|
|
||||||
self.irk = (address_bytes * 3)[:16]
|
|
||||||
|
|
||||||
# Load advertising data
|
|
||||||
advertising_data = config.get('advertising_data')
|
|
||||||
if advertising_data:
|
|
||||||
self.advertising_data = bytes.fromhex(advertising_data)
|
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Decorators used with the following Device class
|
# Decorators used with the following Device class
|
||||||
@@ -294,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)
|
||||||
@@ -387,7 +453,6 @@ class Device(CompositeEventEmitter):
|
|||||||
self.connecting = False
|
self.connecting = False
|
||||||
self.disconnecting = False
|
self.disconnecting = False
|
||||||
self.connections = {} # Connections, by connection handle
|
self.connections = {} # Connections, by connection handle
|
||||||
self.le_enabled = True
|
|
||||||
self.classic_enabled = False
|
self.classic_enabled = False
|
||||||
self.discoverable = False
|
self.discoverable = False
|
||||||
self.connectable = False
|
self.connectable = False
|
||||||
@@ -407,6 +472,10 @@ class Device(CompositeEventEmitter):
|
|||||||
self.advertising_interval_max = config.advertising_interval_max
|
self.advertising_interval_max = config.advertising_interval_max
|
||||||
self.keystore = keys.KeyStore.create_for_device(config)
|
self.keystore = keys.KeyStore.create_for_device(config)
|
||||||
self.irk = config.irk
|
self.irk = config.irk
|
||||||
|
self.le_enabled = config.le_enabled
|
||||||
|
self.le_simultaneous_enabled = config.le_simultaneous_enabled
|
||||||
|
self.classic_ssp_enabled = config.classic_ssp_enabled
|
||||||
|
self.classic_sc_enabled = config.classic_sc_enabled
|
||||||
|
|
||||||
# If a name is passed, override the name from the config
|
# If a name is passed, override the name from the config
|
||||||
if name:
|
if name:
|
||||||
@@ -472,10 +541,11 @@ class Device(CompositeEventEmitter):
|
|||||||
if connection := self.connections.get(connection_handle):
|
if connection := self.connections.get(connection_handle):
|
||||||
return connection
|
return connection
|
||||||
|
|
||||||
def find_connection_by_bd_addr(self, bd_addr):
|
def find_connection_by_bd_addr(self, bd_addr, transport=None):
|
||||||
for connection in self.connections.values():
|
for connection in self.connections.values():
|
||||||
if connection.peer_address == bd_addr:
|
if connection.peer_address.get_bytes() == bd_addr.get_bytes():
|
||||||
return connection
|
if transport is None or connection.transport == transport:
|
||||||
|
return connection
|
||||||
|
|
||||||
def register_l2cap_server(self, psm, server):
|
def register_l2cap_server(self, psm, server):
|
||||||
self.l2cap_channel_manager.register_server(psm, server)
|
self.l2cap_channel_manager.register_server(psm, server)
|
||||||
@@ -504,6 +574,11 @@ class Device(CompositeEventEmitter):
|
|||||||
logger.debug(color(f'BD_ADDR: {response.return_parameters.bd_addr}', 'yellow'))
|
logger.debug(color(f'BD_ADDR: {response.return_parameters.bd_addr}', 'yellow'))
|
||||||
self.public_address = response.return_parameters.bd_addr
|
self.public_address = response.return_parameters.bd_addr
|
||||||
|
|
||||||
|
|
||||||
|
await self.send_command(HCI_Write_LE_Host_Support_Command(
|
||||||
|
le_supported_host = int(self.le_enabled),
|
||||||
|
simultaneous_le_host = int(self.le_simultaneous_enabled),
|
||||||
|
))
|
||||||
if self.le_enabled:
|
if self.le_enabled:
|
||||||
# Set the controller address
|
# Set the controller address
|
||||||
await self.send_command(HCI_LE_Set_Random_Address_Command(
|
await self.send_command(HCI_LE_Set_Random_Address_Command(
|
||||||
@@ -541,7 +616,12 @@ class Device(CompositeEventEmitter):
|
|||||||
HCI_Write_Class_Of_Device_Command(class_of_device = self.class_of_device)
|
HCI_Write_Class_Of_Device_Command(class_of_device = self.class_of_device)
|
||||||
)
|
)
|
||||||
await self.send_command(
|
await self.send_command(
|
||||||
HCI_Write_Simple_Pairing_Mode_Command(simple_pairing_mode = 0x01)
|
HCI_Write_Simple_Pairing_Mode_Command(
|
||||||
|
simple_pairing_mode=int(self.classic_ssp_enabled))
|
||||||
|
)
|
||||||
|
await self.send_command(
|
||||||
|
HCI_Write_Secure_Connections_Host_Support_Command(
|
||||||
|
secure_connections_host_support=int(self.classic_sc_enabled))
|
||||||
)
|
)
|
||||||
|
|
||||||
# Let the SMP manager know about the address
|
# Let the SMP manager know about the address
|
||||||
@@ -670,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
|
||||||
|
|
||||||
@@ -751,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)
|
||||||
|
|
||||||
@@ -790,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
|
||||||
@@ -824,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
|
||||||
@@ -976,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
|
||||||
@@ -1023,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,
|
||||||
@@ -1032,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(
|
||||||
@@ -1044,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
|
||||||
@@ -1078,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
|
||||||
|
|||||||
+204
-68
@@ -22,6 +22,8 @@
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Imports
|
# Imports
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
import asyncio
|
||||||
|
import types
|
||||||
import logging
|
import logging
|
||||||
from colors import color
|
from colors import color
|
||||||
|
|
||||||
@@ -53,13 +55,13 @@ GATT_NEXT_DST_CHANGE_SERVICE = UUID.from_16_bits(0x1807, 'Next DS
|
|||||||
GATT_GLUCOSE_SERVICE = UUID.from_16_bits(0x1808, 'Glucose')
|
GATT_GLUCOSE_SERVICE = UUID.from_16_bits(0x1808, 'Glucose')
|
||||||
GATT_HEALTH_THERMOMETER_SERVICE = UUID.from_16_bits(0x1809, 'Health Thermometer')
|
GATT_HEALTH_THERMOMETER_SERVICE = UUID.from_16_bits(0x1809, 'Health Thermometer')
|
||||||
GATT_DEVICE_INFORMATION_SERVICE = UUID.from_16_bits(0x180A, 'Device Information')
|
GATT_DEVICE_INFORMATION_SERVICE = UUID.from_16_bits(0x180A, 'Device Information')
|
||||||
GATT_DEVICE_HEART_RATE_SERVICE = UUID.from_16_bits(0x180D, 'Heart Rate')
|
GATT_HEART_RATE_SERVICE = UUID.from_16_bits(0x180D, 'Heart Rate')
|
||||||
GATT_PHONE_ALTERT_STATUS_SERVICE = UUID.from_16_bits(0x180E, 'Phone Alert Status')
|
GATT_PHONE_ALERT_STATUS_SERVICE = UUID.from_16_bits(0x180E, 'Phone Alert Status')
|
||||||
GATT_DEVICE_BATTERY_SERVICE = UUID.from_16_bits(0x180F, 'Battery')
|
GATT_BATTERY_SERVICE = UUID.from_16_bits(0x180F, 'Battery')
|
||||||
GATT_BLOOD_PRESSURE_SERVICE = UUID.from_16_bits(0x1810, 'Blood Pressure')
|
GATT_BLOOD_PRESSURE_SERVICE = UUID.from_16_bits(0x1810, 'Blood Pressure')
|
||||||
GATT_ALTERT_NOTIFICATION_SERVICE = UUID.from_16_bits(0x1811, 'Alert Notification')
|
GATT_ALERT_NOTIFICATION_SERVICE = UUID.from_16_bits(0x1811, 'Alert Notification')
|
||||||
GATT_DEVICE_HUMAN_INTERFACE_DEVICE_SERVICE = UUID.from_16_bits(0x1812, 'Human Interface Device')
|
GATT_HUMAN_INTERFACE_DEVICE_SERVICE = UUID.from_16_bits(0x1812, 'Human Interface Device')
|
||||||
GATT_DEVICE_SCAN_PARAMETERS_SERVICE = UUID.from_16_bits(0x1813, 'Scan Parameters')
|
GATT_SCAN_PARAMETERS_SERVICE = UUID.from_16_bits(0x1813, 'Scan Parameters')
|
||||||
GATT_RUNNING_SPEED_AND_CADENCE_SERVICE = UUID.from_16_bits(0x1814, 'Running Speed and Cadence')
|
GATT_RUNNING_SPEED_AND_CADENCE_SERVICE = UUID.from_16_bits(0x1814, 'Running Speed and Cadence')
|
||||||
GATT_AUTOMATION_IO_SERVICE = UUID.from_16_bits(0x1815, 'Automation IO')
|
GATT_AUTOMATION_IO_SERVICE = UUID.from_16_bits(0x1815, 'Automation IO')
|
||||||
GATT_CYCLING_SPEED_AND_CADENCE_SERVICE = UUID.from_16_bits(0x1816, 'Cycling Speed and Cadence')
|
GATT_CYCLING_SPEED_AND_CADENCE_SERVICE = UUID.from_16_bits(0x1816, 'Cycling Speed and Cadence')
|
||||||
@@ -119,7 +121,7 @@ GATT_ENVIRONMENTAL_SENSING_CONFIGURATION_DESCRIPTOR = UUID.from_16_bits(0x290B,
|
|||||||
GATT_ENVIRONMENTAL_SENSING_MEASUREMENT_DESCRIPTOR = UUID.from_16_bits(0x290C, 'Environmental Sensing Measurement')
|
GATT_ENVIRONMENTAL_SENSING_MEASUREMENT_DESCRIPTOR = UUID.from_16_bits(0x290C, 'Environmental Sensing Measurement')
|
||||||
GATT_ENVIRONMENTAL_SENSING_TRIGGER_DESCRIPTOR = UUID.from_16_bits(0x290D, 'Environmental Sensing Trigger Setting')
|
GATT_ENVIRONMENTAL_SENSING_TRIGGER_DESCRIPTOR = UUID.from_16_bits(0x290D, 'Environmental Sensing Trigger Setting')
|
||||||
GATT_TIME_TRIGGER_DESCRIPTOR = UUID.from_16_bits(0x290E, 'Time Trigger Setting')
|
GATT_TIME_TRIGGER_DESCRIPTOR = UUID.from_16_bits(0x290E, 'Time Trigger Setting')
|
||||||
GATT_COMPLETE_BE_EDR_TRANSPORT_BLOCK_DATA_DESCRIPTOR = UUID.from_16_bits(0x290F, 'Complete BR-EDR Transport Block Data')
|
GATT_COMPLETE_BR_EDR_TRANSPORT_BLOCK_DATA_DESCRIPTOR = UUID.from_16_bits(0x290F, 'Complete BR-EDR Transport Block Data')
|
||||||
|
|
||||||
# Device Information Service
|
# Device Information Service
|
||||||
GATT_SYSTEM_ID_CHARACTERISTIC = UUID.from_16_bits(0x2A23, 'System ID')
|
GATT_SYSTEM_ID_CHARACTERISTIC = UUID.from_16_bits(0x2A23, 'System ID')
|
||||||
@@ -132,27 +134,34 @@ GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC = UUID.from_16_bits(0x2A2
|
|||||||
GATT_REGULATORY_CERTIFICATION_DATA_LIST_CHARACTERISTIC = UUID.from_16_bits(0x2A2A, 'IEEE 11073-20601 Regulatory Certification Data List')
|
GATT_REGULATORY_CERTIFICATION_DATA_LIST_CHARACTERISTIC = UUID.from_16_bits(0x2A2A, 'IEEE 11073-20601 Regulatory Certification Data List')
|
||||||
GATT_PNP_ID_CHARACTERISTIC = UUID.from_16_bits(0x2A50, 'PnP ID')
|
GATT_PNP_ID_CHARACTERISTIC = UUID.from_16_bits(0x2A50, 'PnP ID')
|
||||||
|
|
||||||
# Human Interface Device
|
# Human Interface Device Service
|
||||||
GATT_HID_INFORMATION_CHARACTERISTIC = UUID.from_16_bits(0x2A4A, 'HID Information')
|
GATT_HID_INFORMATION_CHARACTERISTIC = UUID.from_16_bits(0x2A4A, 'HID Information')
|
||||||
GATT_REPORT_MAP_CHARACTERISTIC = UUID.from_16_bits(0x2A4B, 'Report Map')
|
GATT_REPORT_MAP_CHARACTERISTIC = UUID.from_16_bits(0x2A4B, 'Report Map')
|
||||||
GATT_HID_CONTROL_POINT_CHARACTERISTIC = UUID.from_16_bits(0x2A4C, 'HID Control Point')
|
GATT_HID_CONTROL_POINT_CHARACTERISTIC = UUID.from_16_bits(0x2A4C, 'HID Control Point')
|
||||||
GATT_REPORT_CHARACTERISTIC = UUID.from_16_bits(0x2A4D, 'Report')
|
GATT_REPORT_CHARACTERISTIC = UUID.from_16_bits(0x2A4D, 'Report')
|
||||||
GATT_PROTOCOL_MODE_CHARACTERISTIC = UUID.from_16_bits(0x2A4E, 'Protocol Mode')
|
GATT_PROTOCOL_MODE_CHARACTERISTIC = UUID.from_16_bits(0x2A4E, 'Protocol Mode')
|
||||||
|
|
||||||
|
# Heart Rate Service
|
||||||
|
GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC = UUID.from_16_bits(0x2A37, 'Heart Rate Measurement')
|
||||||
|
GATT_BODY_SENSOR_LOCATION_CHARACTERISTIC = UUID.from_16_bits(0x2A38, 'Body Sensor Location')
|
||||||
|
GATT_HEART_RATE_CONTROL_POINT_CHARACTERISTIC = UUID.from_16_bits(0x2A39, 'Heart Rate Control Point')
|
||||||
|
|
||||||
|
# Battery Service
|
||||||
|
GATT_BATTERY_LEVEL_CHARACTERISTIC = UUID.from_16_bits(0x2A19, 'Battery Level')
|
||||||
|
|
||||||
# Misc
|
# Misc
|
||||||
GATT_DEVICE_NAME_CHARACTERISTIC = UUID.from_16_bits(0x2A00, 'Device Name')
|
GATT_DEVICE_NAME_CHARACTERISTIC = UUID.from_16_bits(0x2A00, 'Device Name')
|
||||||
GATT_APPEARANCE_CHARACTERISTIC = UUID.from_16_bits(0x2A01, 'Appearance')
|
GATT_APPEARANCE_CHARACTERISTIC = UUID.from_16_bits(0x2A01, 'Appearance')
|
||||||
GATT_PERIPHERAL_PRIVACY_FLAG_CHARACTERISTIC = UUID.from_16_bits(0x2A02, 'Peripheral Privacy Flag')
|
GATT_PERIPHERAL_PRIVACY_FLAG_CHARACTERISTIC = UUID.from_16_bits(0x2A02, 'Peripheral Privacy Flag')
|
||||||
GATT_RECONNECTION_ADDRESS_CHARACTERISTIC = UUID.from_16_bits(0x2A03, 'Reconnection Address')
|
GATT_RECONNECTION_ADDRESS_CHARACTERISTIC = UUID.from_16_bits(0x2A03, 'Reconnection Address')
|
||||||
GATT_PERIPHERAL_PREFERRREED_CONNECTION_PARAMETERS_CHARACTERISTIC = UUID.from_16_bits(0x2A04, 'Peripheral Preferred Connection Parameters')
|
GATT_PERIPHERAL_PREFERRED_CONNECTION_PARAMETERS_CHARACTERISTIC = UUID.from_16_bits(0x2A04, 'Peripheral Preferred Connection Parameters')
|
||||||
GATT_SERVICE_CHANGED_CHARACTERISTIC = UUID.from_16_bits(0x2A05, 'Service Changed')
|
GATT_SERVICE_CHANGED_CHARACTERISTIC = UUID.from_16_bits(0x2A05, 'Service Changed')
|
||||||
GATT_ALERT_LEVEL_CHARACTERISTIC = UUID.from_16_bits(0x2A06, 'Alert Level')
|
GATT_ALERT_LEVEL_CHARACTERISTIC = UUID.from_16_bits(0x2A06, 'Alert Level')
|
||||||
GATT_TX_POWER_LEVEL_CHARACTERISTIC = UUID.from_16_bits(0x2A07, 'Tx Power Level')
|
GATT_TX_POWER_LEVEL_CHARACTERISTIC = UUID.from_16_bits(0x2A07, 'Tx Power Level')
|
||||||
GATT_BATTERY_LEVEL_CHARACTERISTIC = UUID.from_16_bits(0x2A19, 'Battery Level')
|
GATT_BOOT_KEYBOARD_INPUT_REPORT_CHARACTERISTIC = UUID.from_16_bits(0x2A22, 'Boot Keyboard Input Report')
|
||||||
GATT_BOOT_KEYBOARD_INPUT_REPORT_CHARACTERISTIC = UUID.from_16_bits(0x2A22, 'Boot Keyboard Input Report')
|
GATT_CURRENT_TIME_CHARACTERISTIC = UUID.from_16_bits(0x2A2B, 'Current Time')
|
||||||
GATT_CURRENT_TIME_CHARACTERISTIC = UUID.from_16_bits(0x2A2B, 'Current Time')
|
GATT_BOOT_KEYBOARD_OUTPUT_REPORT_CHARACTERISTIC = UUID.from_16_bits(0x2A32, 'Boot Keyboard Output Report')
|
||||||
GATT_BOOT_KEYBOARD_OUTPUT_REPORT_CHARACTERISTIC = UUID.from_16_bits(0x2A32, 'Boot Keyboard Output Report')
|
GATT_CENTRAL_ADDRESS_RESOLUTION__CHARACTERISTIC = UUID.from_16_bits(0x2AA6, 'Central Address Resolution')
|
||||||
GATT_CENTRAL_ADDRESS_RESOLUTION__CHARACTERISTIC = UUID.from_16_bits(0x2AA6, 'Central Address Resolution')
|
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -189,13 +198,24 @@ class Service(Attribute):
|
|||||||
self.uuid = uuid
|
self.uuid = uuid
|
||||||
self.included_services = []
|
self.included_services = []
|
||||||
self.characteristics = characteristics[:]
|
self.characteristics = characteristics[:]
|
||||||
self.end_group_handle = 0
|
|
||||||
self.primary = primary
|
self.primary = primary
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'Service(handle=0x{self.handle:04X}, end=0x{self.end_group_handle:04X}, uuid={self.uuid}){"" if self.primary else "*"}'
|
return f'Service(handle=0x{self.handle:04X}, end=0x{self.end_group_handle:04X}, uuid={self.uuid}){"" if self.primary else "*"}'
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class TemplateService(Service):
|
||||||
|
'''
|
||||||
|
Convenience abstract class that can be used by profile-specific subclasses that want
|
||||||
|
to expose their UUID as a class property
|
||||||
|
'''
|
||||||
|
UUID = None
|
||||||
|
|
||||||
|
def __init__(self, characteristics, primary=True):
|
||||||
|
super().__init__(self.UUID, characteristics, primary)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class Characteristic(Attribute):
|
class Characteristic(Attribute):
|
||||||
'''
|
'''
|
||||||
@@ -227,56 +247,34 @@ class Characteristic(Attribute):
|
|||||||
def property_name(property):
|
def property_name(property):
|
||||||
return Characteristic.PROPERTY_NAMES.get(property, '')
|
return Characteristic.PROPERTY_NAMES.get(property, '')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def properties_as_string(properties):
|
||||||
|
return ','.join([
|
||||||
|
Characteristic.property_name(p) for p in Characteristic.PROPERTY_NAMES.keys()
|
||||||
|
if properties & p
|
||||||
|
])
|
||||||
|
|
||||||
def __init__(self, uuid, properties, permissions, value = b'', descriptors = []):
|
def __init__(self, uuid, properties, permissions, value = b'', descriptors = []):
|
||||||
# Convert the uuid to a UUID object if it isn't already
|
|
||||||
if type(uuid) is str:
|
|
||||||
uuid = UUID(uuid)
|
|
||||||
|
|
||||||
super().__init__(uuid, permissions, value)
|
super().__init__(uuid, permissions, value)
|
||||||
self.uuid = uuid
|
self.uuid = self.type
|
||||||
self.properties = properties
|
self.properties = properties
|
||||||
self._descriptors = descriptors
|
self.descriptors = descriptors
|
||||||
self._descriptors_discovered = False
|
|
||||||
self.end_group_handle = 0
|
|
||||||
self.attach_descriptors()
|
|
||||||
|
|
||||||
def attach_descriptors(self):
|
|
||||||
""" Let all the descriptors know they are attached to this characteristic """
|
|
||||||
for descriptor in self._descriptors:
|
|
||||||
descriptor.characteristic = self
|
|
||||||
|
|
||||||
def add_descriptor(self, descriptor):
|
|
||||||
descriptor.characteristic = self
|
|
||||||
self.descriptors.append(descriptor)
|
|
||||||
|
|
||||||
def get_descriptor(self, descriptor_type):
|
def get_descriptor(self, descriptor_type):
|
||||||
for descriptor in self.descriptors:
|
for descriptor in self.descriptors:
|
||||||
if descriptor.uuid == descriptor_type:
|
if descriptor.uuid == descriptor_type:
|
||||||
return descriptor
|
return descriptor
|
||||||
|
|
||||||
@property
|
|
||||||
def descriptors(self):
|
|
||||||
return self._descriptors
|
|
||||||
|
|
||||||
@descriptors.setter
|
|
||||||
def descriptors(self, value):
|
|
||||||
self._descriptors = value
|
|
||||||
self._descriptors_discovered = True
|
|
||||||
self.attach_descriptors()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def descriptors_discovered(self):
|
|
||||||
return self._descriptors_discovered
|
|
||||||
|
|
||||||
def get_properties_as_string(self):
|
|
||||||
return ','.join([self.property_name(p) for p in self.PROPERTY_NAMES.keys() if self.properties & p])
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'Characteristic(handle=0x{self.handle:04X}, end=0x{self.end_group_handle:04X}, uuid={self.uuid}, properties={self.get_properties_as_string()})'
|
return f'Characteristic(handle=0x{self.handle:04X}, end=0x{self.end_group_handle:04X}, uuid={self.uuid}, properties={Characteristic.properties_as_string(self.properties)})'
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class CharacteristicValue:
|
class CharacteristicValue:
|
||||||
|
'''
|
||||||
|
Characteristic value where reading and/or writing is delegated to functions
|
||||||
|
passed as arguments to the constructor.
|
||||||
|
'''
|
||||||
def __init__(self, read=None, write=None):
|
def __init__(self, read=None, write=None):
|
||||||
self._read = read
|
self._read = read
|
||||||
self._write = write
|
self._write = write
|
||||||
@@ -289,20 +287,158 @@ class CharacteristicValue:
|
|||||||
self._write(connection, value)
|
self._write(connection, value)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class CharacteristicAdapter:
|
||||||
|
'''
|
||||||
|
An adapter that can adapt any object with `read_value` and `write_value`
|
||||||
|
methods (like Characteristic and CharacteristicProxy objects) by wrapping
|
||||||
|
those methods with ones that return/accept encoded/decoded values.
|
||||||
|
Objects with async methods are considered proxies, so the adaptation is one
|
||||||
|
where the return value of `read_value` is decoded and the value passed to
|
||||||
|
`write_value` is encoded. Other objects are considered local characteristics
|
||||||
|
so the adaptation is one where the return value of `read_value` is encoded
|
||||||
|
and the value passed to `write_value` is decoded.
|
||||||
|
If the characteristic has a `subscribe` method, it is wrapped with one where
|
||||||
|
the values are decoded before being passed to the subscriber.
|
||||||
|
'''
|
||||||
|
def __init__(self, characteristic):
|
||||||
|
self.wrapped_characteristic = characteristic
|
||||||
|
|
||||||
|
if (
|
||||||
|
asyncio.iscoroutinefunction(characteristic.read_value) and
|
||||||
|
asyncio.iscoroutinefunction(characteristic.write_value)
|
||||||
|
):
|
||||||
|
self.read_value = self.read_decoded_value
|
||||||
|
self.write_value = self.write_decoded_value
|
||||||
|
else:
|
||||||
|
self.read_value = self.read_encoded_value
|
||||||
|
self.write_value = self.write_encoded_value
|
||||||
|
|
||||||
|
if hasattr(self.wrapped_characteristic, 'subscribe'):
|
||||||
|
self.subscribe = self.wrapped_subscribe
|
||||||
|
|
||||||
|
def __getattr__(self, 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):
|
||||||
|
return self.encode_value(self.wrapped_characteristic.read_value(connection))
|
||||||
|
|
||||||
|
def write_encoded_value(self, connection, value):
|
||||||
|
return self.wrapped_characteristic.write_value(connection, self.decode_value(value))
|
||||||
|
|
||||||
|
async def read_decoded_value(self):
|
||||||
|
return self.decode_value(await self.wrapped_characteristic.read_value())
|
||||||
|
|
||||||
|
async def write_decoded_value(self, value):
|
||||||
|
return await self.wrapped_characteristic.write_value(self.encode_value(value))
|
||||||
|
|
||||||
|
def encode_value(self, value):
|
||||||
|
return value
|
||||||
|
|
||||||
|
def decode_value(self, value):
|
||||||
|
return value
|
||||||
|
|
||||||
|
def wrapped_subscribe(self, subscriber=None):
|
||||||
|
return self.wrapped_characteristic.subscribe(
|
||||||
|
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):
|
||||||
|
'''
|
||||||
|
Adapter that converts bytes values using an encode and a decode function.
|
||||||
|
'''
|
||||||
|
def __init__(self, characteristic, encode=None, decode=None):
|
||||||
|
super().__init__(characteristic)
|
||||||
|
self.encode = encode
|
||||||
|
self.decode = decode
|
||||||
|
|
||||||
|
def encode_value(self, value):
|
||||||
|
return self.encode(value) if self.encode else value
|
||||||
|
|
||||||
|
def decode_value(self, value):
|
||||||
|
return self.decode(value) if self.decode else value
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class PackedCharacteristicAdapter(CharacteristicAdapter):
|
||||||
|
'''
|
||||||
|
Adapter that packs/unpacks characteristic values according to a standard
|
||||||
|
Python `struct` format.
|
||||||
|
For formats with a single value, the adapted `read_value` and `write_value`
|
||||||
|
methods return/accept single values. For formats with multiple values,
|
||||||
|
they return/accept a tuple with the same number of elements as is required for
|
||||||
|
the format.
|
||||||
|
'''
|
||||||
|
def __init__(self, characteristic, format):
|
||||||
|
super().__init__(characteristic)
|
||||||
|
self.struct = struct.Struct(format)
|
||||||
|
|
||||||
|
def pack(self, *values):
|
||||||
|
return self.struct.pack(*values)
|
||||||
|
|
||||||
|
def unpack(self, buffer):
|
||||||
|
return self.struct.unpack(buffer)
|
||||||
|
|
||||||
|
def encode_value(self, value):
|
||||||
|
return self.pack(*value if type(value) is tuple else (value,))
|
||||||
|
|
||||||
|
def decode_value(self, value):
|
||||||
|
unpacked = self.unpack(value)
|
||||||
|
return unpacked[0] if len(unpacked) == 1 else unpacked
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class MappedCharacteristicAdapter(PackedCharacteristicAdapter):
|
||||||
|
'''
|
||||||
|
Adapter that packs/unpacks characteristic values according to a standard
|
||||||
|
Python `struct` format.
|
||||||
|
The adapted `read_value` and `write_value` methods return/accept aa dictionary which
|
||||||
|
is packed/unpacked according to format, with the arguments extracted from the dictionary
|
||||||
|
by key, in the same order as they occur in the `keys` parameter.
|
||||||
|
'''
|
||||||
|
def __init__(self, characteristic, format, keys):
|
||||||
|
super().__init__(characteristic, format)
|
||||||
|
self.keys = keys
|
||||||
|
|
||||||
|
def pack(self, values):
|
||||||
|
return super().pack(*(values[key] for key in self.keys))
|
||||||
|
|
||||||
|
def unpack(self, buffer):
|
||||||
|
return dict(zip(self.keys, super().unpack(buffer)))
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class UTF8CharacteristicAdapter(CharacteristicAdapter):
|
||||||
|
'''
|
||||||
|
Adapter that converts strings to/from bytes using UTF-8 encoding
|
||||||
|
'''
|
||||||
|
def encode_value(self, value):
|
||||||
|
return value.encode('utf-8')
|
||||||
|
|
||||||
|
def decode_value(self, value):
|
||||||
|
return value.decode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class Descriptor(Attribute):
|
class Descriptor(Attribute):
|
||||||
'''
|
'''
|
||||||
See Vol 3, Part G - 3.3.3 Characteristic Descriptor Declarations
|
See Vol 3, Part G - 3.3.3 Characteristic Descriptor Declarations
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def __init__(self, uuid, permissions, value = b''):
|
def __init__(self, descriptor_type, permissions, value = b''):
|
||||||
# Convert the uuid to a UUID object if it isn't already
|
super().__init__(descriptor_type, permissions, value)
|
||||||
if type(uuid) is str:
|
|
||||||
uuid = UUID(uuid)
|
|
||||||
|
|
||||||
super().__init__(uuid, permissions, value)
|
|
||||||
self.uuid = uuid
|
|
||||||
self.characteristic = None
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'Descriptor(handle=0x{self.handle:04X}, uuid={self.uuid}, value={self.read_value(None).hex()})'
|
return f'Descriptor(handle=0x{self.handle:04X}, type={self.type}, value={self.read_value(None).hex()})'
|
||||||
|
|||||||
+138
-20
@@ -35,10 +35,9 @@ from .gatt import (
|
|||||||
GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
|
GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
|
||||||
GATT_REQUEST_TIMEOUT,
|
GATT_REQUEST_TIMEOUT,
|
||||||
GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
|
GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
|
||||||
|
GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE,
|
||||||
GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
|
GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
|
||||||
Service,
|
Characteristic
|
||||||
Characteristic,
|
|
||||||
Descriptor
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -47,6 +46,94 @@ from .gatt import (
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Proxies
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class AttributeProxy(EventEmitter):
|
||||||
|
def __init__(self, client, handle, end_group_handle, attribute_type):
|
||||||
|
EventEmitter.__init__(self)
|
||||||
|
self.client = client
|
||||||
|
self.handle = handle
|
||||||
|
self.end_group_handle = end_group_handle
|
||||||
|
self.type = attribute_type
|
||||||
|
|
||||||
|
async def read_value(self, no_long_read=False):
|
||||||
|
return await self.client.read_value(self.handle, no_long_read)
|
||||||
|
|
||||||
|
async def write_value(self, value, with_response=False):
|
||||||
|
return await self.client.write_value(self.handle, value, with_response)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'Attribute(handle=0x{self.handle:04X}, type={self.uuid})'
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceProxy(AttributeProxy):
|
||||||
|
@staticmethod
|
||||||
|
def from_client(cls, client, service_uuid):
|
||||||
|
# The service and its characteristics are considered to have already been discovered
|
||||||
|
services = client.get_services_by_uuid(service_uuid)
|
||||||
|
service = services[0] if services else None
|
||||||
|
return cls(service) if service else None
|
||||||
|
|
||||||
|
def __init__(self, client, handle, end_group_handle, uuid, primary=True):
|
||||||
|
attribute_type = GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE if primary else GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE
|
||||||
|
super().__init__(client, handle, end_group_handle, attribute_type)
|
||||||
|
self.uuid = uuid
|
||||||
|
self.characteristics = []
|
||||||
|
|
||||||
|
async def discover_characteristics(self, uuids=[]):
|
||||||
|
return await self.client.discover_characteristics(uuids, self)
|
||||||
|
|
||||||
|
def get_characteristics_by_uuid(self, uuid):
|
||||||
|
return self.client.get_characteristics_by_uuid(uuid, self)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'Service(handle=0x{self.handle:04X}, uuid={self.uuid})'
|
||||||
|
|
||||||
|
|
||||||
|
class CharacteristicProxy(AttributeProxy):
|
||||||
|
def __init__(self, client, handle, end_group_handle, uuid, properties):
|
||||||
|
super().__init__(client, handle, end_group_handle, uuid)
|
||||||
|
self.uuid = uuid
|
||||||
|
self.properties = properties
|
||||||
|
self.descriptors = []
|
||||||
|
self.descriptors_discovered = False
|
||||||
|
|
||||||
|
def get_descriptor(self, descriptor_type):
|
||||||
|
for descriptor in self.descriptors:
|
||||||
|
if descriptor.type == descriptor_type:
|
||||||
|
return descriptor
|
||||||
|
|
||||||
|
async def discover_descriptors(self):
|
||||||
|
return await self.client.discover_descriptors(self)
|
||||||
|
|
||||||
|
async def subscribe(self, subscriber=None):
|
||||||
|
return await self.client.subscribe(self, subscriber)
|
||||||
|
|
||||||
|
async def unsubscribe(self, subscriber=None):
|
||||||
|
return await self.client.unsubscribe(self, subscriber)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'Characteristic(handle=0x{self.handle:04X}, uuid={self.uuid}, properties={Characteristic.properties_as_string(self.properties)})'
|
||||||
|
|
||||||
|
|
||||||
|
class DescriptorProxy(AttributeProxy):
|
||||||
|
def __init__(self, client, handle, descriptor_type):
|
||||||
|
super().__init__(client, handle, 0, descriptor_type)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'Descriptor(handle=0x{self.handle:04X}, type={self.type})'
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileServiceProxy:
|
||||||
|
'''
|
||||||
|
Base class for profile-specific service proxies
|
||||||
|
'''
|
||||||
|
@classmethod
|
||||||
|
def from_client(cls, client):
|
||||||
|
return ServiceProxy.from_client(cls, client, cls.SERVICE_CLASS.UUID)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# GATT Client
|
# GATT Client
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -173,10 +260,14 @@ class Client:
|
|||||||
logger.warning(f'bogus handle values: {attribute_handle} {end_group_handle}')
|
logger.warning(f'bogus handle values: {attribute_handle} {end_group_handle}')
|
||||||
return
|
return
|
||||||
|
|
||||||
# Create a primary service object
|
# Create a service proxy for this service
|
||||||
service = Service(UUID.from_bytes(attribute_value), [], True)
|
service = ServiceProxy(
|
||||||
service.handle = attribute_handle
|
self,
|
||||||
service.end_group_handle = end_group_handle
|
attribute_handle,
|
||||||
|
end_group_handle,
|
||||||
|
UUID.from_bytes(attribute_value),
|
||||||
|
True
|
||||||
|
)
|
||||||
|
|
||||||
# Filter out returned services based on the given uuids list
|
# Filter out returned services based on the given uuids list
|
||||||
if (not uuids) or (service.uuid in uuids):
|
if (not uuids) or (service.uuid in uuids):
|
||||||
@@ -233,10 +324,8 @@ class Client:
|
|||||||
logger.warning(f'bogus handle values: {attribute_handle} {end_group_handle}')
|
logger.warning(f'bogus handle values: {attribute_handle} {end_group_handle}')
|
||||||
return
|
return
|
||||||
|
|
||||||
# Create a primary service object
|
# Create a service proxy for this service
|
||||||
service = Service(uuid, [], True)
|
service = ServiceProxy(self, attribute_handle, end_group_handle, uuid, True)
|
||||||
service.handle = attribute_handle
|
|
||||||
service.end_group_handle = end_group_handle
|
|
||||||
|
|
||||||
# Add the service to the peer's service list
|
# Add the service to the peer's service list
|
||||||
services.append(service)
|
services.append(service)
|
||||||
@@ -314,8 +403,7 @@ class Client:
|
|||||||
|
|
||||||
properties, handle = struct.unpack_from('<BH', attribute_value)
|
properties, handle = struct.unpack_from('<BH', attribute_value)
|
||||||
characteristic_uuid = UUID.from_bytes(attribute_value[3:])
|
characteristic_uuid = UUID.from_bytes(attribute_value[3:])
|
||||||
characteristic = Characteristic(characteristic_uuid, properties, 0)
|
characteristic = CharacteristicProxy(self, handle, 0, characteristic_uuid, properties)
|
||||||
characteristic.handle = handle
|
|
||||||
|
|
||||||
# Set the previous characteristic's end handle
|
# Set the previous characteristic's end handle
|
||||||
if characteristics:
|
if characteristics:
|
||||||
@@ -382,8 +470,7 @@ class Client:
|
|||||||
logger.warning(f'bogus handle value: {attribute_handle}')
|
logger.warning(f'bogus handle value: {attribute_handle}')
|
||||||
return []
|
return []
|
||||||
|
|
||||||
descriptor = Descriptor(UUID.from_bytes(attribute_uuid), 0)
|
descriptor = DescriptorProxy(self, attribute_handle, UUID.from_bytes(attribute_uuid))
|
||||||
descriptor.handle = attribute_handle
|
|
||||||
descriptors.append(descriptor)
|
descriptors.append(descriptor)
|
||||||
# TODO: read descriptor value
|
# TODO: read descriptor value
|
||||||
|
|
||||||
@@ -427,8 +514,7 @@ class Client:
|
|||||||
logger.warning(f'bogus handle value: {attribute_handle}')
|
logger.warning(f'bogus handle value: {attribute_handle}')
|
||||||
return []
|
return []
|
||||||
|
|
||||||
attribute = Attribute(attribute_uuid, 0)
|
attribute = AttributeProxy(self, attribute_handle, 0, UUID.from_bytes(attribute_uuid))
|
||||||
attribute.handle = attribute_handle
|
|
||||||
attributes.append(attribute)
|
attributes.append(attribute)
|
||||||
|
|
||||||
# Move on to the next attributes
|
# Move on to the next attributes
|
||||||
@@ -461,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
|
||||||
@@ -631,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
|
||||||
@@ -639,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)
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# 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
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
from ..gatt_client import ProfileServiceProxy
|
||||||
|
from ..gatt import (
|
||||||
|
GATT_BATTERY_SERVICE,
|
||||||
|
GATT_BATTERY_LEVEL_CHARACTERISTIC,
|
||||||
|
TemplateService,
|
||||||
|
Characteristic,
|
||||||
|
CharacteristicValue,
|
||||||
|
PackedCharacteristicAdapter
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class BatteryService(TemplateService):
|
||||||
|
UUID = GATT_BATTERY_SERVICE
|
||||||
|
BATTERY_LEVEL_FORMAT = 'B'
|
||||||
|
|
||||||
|
def __init__(self, read_battery_level):
|
||||||
|
self.battery_level_characteristic = PackedCharacteristicAdapter(
|
||||||
|
Characteristic(
|
||||||
|
GATT_BATTERY_LEVEL_CHARACTERISTIC,
|
||||||
|
Characteristic.READ | Characteristic.NOTIFY,
|
||||||
|
Characteristic.READABLE,
|
||||||
|
CharacteristicValue(read=read_battery_level)
|
||||||
|
),
|
||||||
|
format=BatteryService.BATTERY_LEVEL_FORMAT
|
||||||
|
)
|
||||||
|
super().__init__([self.battery_level_characteristic])
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class BatteryServiceProxy(ProfileServiceProxy):
|
||||||
|
SERVICE_CLASS = BatteryService
|
||||||
|
|
||||||
|
def __init__(self, service_proxy):
|
||||||
|
self.service_proxy = service_proxy
|
||||||
|
|
||||||
|
if characteristics := service_proxy.get_characteristics_by_uuid(GATT_BATTERY_LEVEL_CHARACTERISTIC):
|
||||||
|
self.battery_level = PackedCharacteristicAdapter(
|
||||||
|
characteristics[0],
|
||||||
|
format=BatteryService.BATTERY_LEVEL_FORMAT
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.battery_level = None
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
# 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 struct
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
from ..gatt_client import ProfileServiceProxy
|
||||||
|
from ..gatt import (
|
||||||
|
GATT_DEVICE_INFORMATION_SERVICE,
|
||||||
|
GATT_FIRMWARE_REVISION_STRING_CHARACTERISTIC,
|
||||||
|
GATT_HARDWARE_REVISION_STRING_CHARACTERISTIC,
|
||||||
|
GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC,
|
||||||
|
GATT_MODEL_NUMBER_STRING_CHARACTERISTIC,
|
||||||
|
GATT_SERIAL_NUMBER_STRING_CHARACTERISTIC,
|
||||||
|
GATT_SOFTWARE_REVISION_STRING_CHARACTERISTIC,
|
||||||
|
GATT_SYSTEM_ID_CHARACTERISTIC,
|
||||||
|
GATT_REGULATORY_CERTIFICATION_DATA_LIST_CHARACTERISTIC,
|
||||||
|
TemplateService,
|
||||||
|
Characteristic,
|
||||||
|
DelegatedCharacteristicAdapter,
|
||||||
|
UTF8CharacteristicAdapter
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class DeviceInformationService(TemplateService):
|
||||||
|
UUID = GATT_DEVICE_INFORMATION_SERVICE
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def pack_system_id(oui, manufacturer_id):
|
||||||
|
return struct.pack('<Q', oui << 40 | manufacturer_id)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def unpack_system_id(buffer):
|
||||||
|
system_id = struct.unpack('<Q', buffer)[0]
|
||||||
|
return (system_id >> 40, system_id & 0xFFFFFFFFFF)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
manufacturer_name: str = None,
|
||||||
|
model_number: str = None,
|
||||||
|
serial_number: str = None,
|
||||||
|
hardware_revision: str = None,
|
||||||
|
firmware_revision: str = None,
|
||||||
|
software_revision: str = None,
|
||||||
|
system_id: Tuple[int, int] = None, # (OUI, Manufacturer ID)
|
||||||
|
ieee_regulatory_certification_data_list: bytes = None
|
||||||
|
# TODO: pnp_id
|
||||||
|
):
|
||||||
|
characteristics = [
|
||||||
|
Characteristic(
|
||||||
|
uuid,
|
||||||
|
Characteristic.READ,
|
||||||
|
Characteristic.READABLE,
|
||||||
|
field
|
||||||
|
)
|
||||||
|
for (field, uuid) in (
|
||||||
|
(manufacturer_name, GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC),
|
||||||
|
(model_number, GATT_MODEL_NUMBER_STRING_CHARACTERISTIC),
|
||||||
|
(serial_number, GATT_SERIAL_NUMBER_STRING_CHARACTERISTIC),
|
||||||
|
(hardware_revision, GATT_HARDWARE_REVISION_STRING_CHARACTERISTIC),
|
||||||
|
(firmware_revision, GATT_FIRMWARE_REVISION_STRING_CHARACTERISTIC),
|
||||||
|
(software_revision, GATT_SOFTWARE_REVISION_STRING_CHARACTERISTIC)
|
||||||
|
)
|
||||||
|
if field is not None
|
||||||
|
]
|
||||||
|
|
||||||
|
if system_id is not None:
|
||||||
|
characteristics.append(Characteristic(
|
||||||
|
GATT_SYSTEM_ID_CHARACTERISTIC,
|
||||||
|
Characteristic.READ,
|
||||||
|
Characteristic.READABLE,
|
||||||
|
self.pack_system_id(*system_id)
|
||||||
|
))
|
||||||
|
|
||||||
|
if ieee_regulatory_certification_data_list is not None:
|
||||||
|
characteristics.append(Characteristic(
|
||||||
|
GATT_REGULATORY_CERTIFICATION_DATA_LIST_CHARACTERISTIC,
|
||||||
|
Characteristic.READ,
|
||||||
|
Characteristic.READABLE,
|
||||||
|
ieee_regulatory_certification_data_list
|
||||||
|
))
|
||||||
|
|
||||||
|
super().__init__(characteristics)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class DeviceInformationServiceProxy(ProfileServiceProxy):
|
||||||
|
SERVICE_CLASS = DeviceInformationService
|
||||||
|
|
||||||
|
def __init__(self, service_proxy):
|
||||||
|
self.service_proxy = service_proxy
|
||||||
|
|
||||||
|
for (field, uuid) in (
|
||||||
|
('manufacturer_name', GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC),
|
||||||
|
('model_number', GATT_MODEL_NUMBER_STRING_CHARACTERISTIC),
|
||||||
|
('serial_number', GATT_SERIAL_NUMBER_STRING_CHARACTERISTIC),
|
||||||
|
('hardware_revision', GATT_HARDWARE_REVISION_STRING_CHARACTERISTIC),
|
||||||
|
('firmware_revision', GATT_FIRMWARE_REVISION_STRING_CHARACTERISTIC),
|
||||||
|
('software_revision', GATT_SOFTWARE_REVISION_STRING_CHARACTERISTIC)
|
||||||
|
):
|
||||||
|
if characteristics := service_proxy.get_characteristics_by_uuid(uuid):
|
||||||
|
characteristic = UTF8CharacteristicAdapter(characteristics[0])
|
||||||
|
else:
|
||||||
|
characteristic = None
|
||||||
|
self.__setattr__(field, characteristic)
|
||||||
|
|
||||||
|
if characteristics := service_proxy.get_characteristics_by_uuid(GATT_SYSTEM_ID_CHARACTERISTIC):
|
||||||
|
self.system_id = DelegatedCharacteristicAdapter(
|
||||||
|
characteristics[0],
|
||||||
|
encode=lambda v: DeviceInformationService.pack_system_id(*v),
|
||||||
|
decode=DeviceInformationService.unpack_system_id
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.system_id = None
|
||||||
|
|
||||||
|
if characteristics := service_proxy.get_characteristics_by_uuid(GATT_REGULATORY_CERTIFICATION_DATA_LIST_CHARACTERISTIC):
|
||||||
|
self.ieee_regulatory_certification_data_list = characteristics[0]
|
||||||
|
else:
|
||||||
|
self.ieee_regulatory_certification_data_list = None
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
# 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
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
from enum import IntEnum
|
||||||
|
import struct
|
||||||
|
|
||||||
|
from ..gatt_client import ProfileServiceProxy
|
||||||
|
from ..att import ATT_Error
|
||||||
|
from ..gatt import (
|
||||||
|
GATT_HEART_RATE_SERVICE,
|
||||||
|
GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC,
|
||||||
|
GATT_BODY_SENSOR_LOCATION_CHARACTERISTIC,
|
||||||
|
GATT_HEART_RATE_CONTROL_POINT_CHARACTERISTIC,
|
||||||
|
TemplateService,
|
||||||
|
Characteristic,
|
||||||
|
CharacteristicValue,
|
||||||
|
DelegatedCharacteristicAdapter,
|
||||||
|
PackedCharacteristicAdapter
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class HeartRateService(TemplateService):
|
||||||
|
UUID = GATT_HEART_RATE_SERVICE
|
||||||
|
HEART_RATE_CONTROL_POINT_FORMAT = 'B'
|
||||||
|
CONTROL_POINT_NOT_SUPPORTED = 0x80
|
||||||
|
RESET_ENERGY_EXPENDED = 0x01
|
||||||
|
|
||||||
|
class BodySensorLocation(IntEnum):
|
||||||
|
OTHER = 0,
|
||||||
|
CHEST = 1,
|
||||||
|
WRIST = 2,
|
||||||
|
FINGER = 3,
|
||||||
|
HAND = 4,
|
||||||
|
EAR_LOBE = 5,
|
||||||
|
FOOT = 6
|
||||||
|
|
||||||
|
class HeartRateMeasurement:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
heart_rate,
|
||||||
|
sensor_contact_detected=None,
|
||||||
|
energy_expended=None,
|
||||||
|
rr_intervals=None
|
||||||
|
):
|
||||||
|
if heart_rate < 0 or heart_rate > 0xFFFF:
|
||||||
|
raise ValueError('heart_rate out of range')
|
||||||
|
|
||||||
|
if energy_expended is not None and (energy_expended < 0 or energy_expended > 0xFFFF):
|
||||||
|
raise ValueError('energy_expended out of range')
|
||||||
|
|
||||||
|
if rr_intervals:
|
||||||
|
for rr_interval in rr_intervals:
|
||||||
|
if rr_interval < 0 or rr_interval * 1024 > 0xFFFF:
|
||||||
|
raise ValueError('rr_intervals out of range')
|
||||||
|
|
||||||
|
self.heart_rate = heart_rate
|
||||||
|
self.sensor_contact_detected = sensor_contact_detected
|
||||||
|
self.energy_expended = energy_expended
|
||||||
|
self.rr_intervals = rr_intervals
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_bytes(cls, data):
|
||||||
|
flags = data[0]
|
||||||
|
offset = 1
|
||||||
|
|
||||||
|
if flags & 1:
|
||||||
|
hr = struct.unpack_from('<H', data, offset)[0]
|
||||||
|
offset += 2
|
||||||
|
else:
|
||||||
|
hr = struct.unpack_from('B', data, offset)[0]
|
||||||
|
offset += 1
|
||||||
|
|
||||||
|
if flags & (1 << 2):
|
||||||
|
sensor_contact_detected = (flags & (1 << 1) != 0)
|
||||||
|
else:
|
||||||
|
sensor_contact_detected = None
|
||||||
|
|
||||||
|
if flags & (1 << 3):
|
||||||
|
energy_expended = struct.unpack_from('<H', data, offset)[0]
|
||||||
|
offset += 2
|
||||||
|
else:
|
||||||
|
energy_expended = None
|
||||||
|
|
||||||
|
if flags & (1 << 4):
|
||||||
|
rr_intervals = tuple(
|
||||||
|
struct.unpack_from('<H', data, offset + i * 2)[0] / 1024
|
||||||
|
for i in range((len(data) - offset) // 2)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
rr_intervals = ()
|
||||||
|
|
||||||
|
return cls(hr, sensor_contact_detected, energy_expended, rr_intervals)
|
||||||
|
|
||||||
|
def __bytes__(self):
|
||||||
|
if self.heart_rate < 256:
|
||||||
|
flags = 0
|
||||||
|
data = struct.pack('B', self.heart_rate)
|
||||||
|
else:
|
||||||
|
flags = 1
|
||||||
|
data = struct.pack('<H', self.heart_rate)
|
||||||
|
|
||||||
|
if self.sensor_contact_detected is not None:
|
||||||
|
flags |= ((1 if self.sensor_contact_detected else 0) << 1) | (1 << 2)
|
||||||
|
|
||||||
|
if self.energy_expended is not None:
|
||||||
|
flags |= (1 << 3)
|
||||||
|
data += struct.pack('<H', self.energy_expended)
|
||||||
|
|
||||||
|
if self.rr_intervals:
|
||||||
|
flags |= (1 << 4)
|
||||||
|
data += b''.join([
|
||||||
|
struct.pack('<H', int(rr_interval * 1024))
|
||||||
|
for rr_interval in self.rr_intervals
|
||||||
|
])
|
||||||
|
|
||||||
|
return bytes([flags]) + data
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'HeartRateMeasurement(heart_rate={self.heart_rate},'\
|
||||||
|
f' sensor_contact_detected={self.sensor_contact_detected},'\
|
||||||
|
f' energy_expended={self.energy_expended},'\
|
||||||
|
f' rr_intervals={self.rr_intervals})'
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
read_heart_rate_measurement,
|
||||||
|
body_sensor_location=None,
|
||||||
|
reset_energy_expended=None
|
||||||
|
):
|
||||||
|
self.heart_rate_measurement_characteristic = DelegatedCharacteristicAdapter(
|
||||||
|
Characteristic(
|
||||||
|
GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC,
|
||||||
|
Characteristic.NOTIFY,
|
||||||
|
0,
|
||||||
|
CharacteristicValue(read=read_heart_rate_measurement)
|
||||||
|
),
|
||||||
|
encode=lambda value: bytes(value)
|
||||||
|
)
|
||||||
|
characteristics = [self.heart_rate_measurement_characteristic]
|
||||||
|
|
||||||
|
if body_sensor_location is not None:
|
||||||
|
self.body_sensor_location_characteristic = Characteristic(
|
||||||
|
GATT_BODY_SENSOR_LOCATION_CHARACTERISTIC,
|
||||||
|
Characteristic.READ,
|
||||||
|
Characteristic.READABLE,
|
||||||
|
bytes([int(body_sensor_location)])
|
||||||
|
)
|
||||||
|
characteristics.append(self.body_sensor_location_characteristic)
|
||||||
|
|
||||||
|
if reset_energy_expended:
|
||||||
|
def write_heart_rate_control_point_value(connection, value):
|
||||||
|
if value == self.RESET_ENERGY_EXPENDED:
|
||||||
|
if reset_energy_expended is not None:
|
||||||
|
reset_energy_expended(connection)
|
||||||
|
else:
|
||||||
|
raise ATT_Error(self.CONTROL_POINT_NOT_SUPPORTED)
|
||||||
|
|
||||||
|
self.heart_rate_control_point_characteristic = PackedCharacteristicAdapter(
|
||||||
|
Characteristic(
|
||||||
|
GATT_HEART_RATE_CONTROL_POINT_CHARACTERISTIC,
|
||||||
|
Characteristic.WRITE,
|
||||||
|
Characteristic.WRITEABLE,
|
||||||
|
CharacteristicValue(write=write_heart_rate_control_point_value)
|
||||||
|
),
|
||||||
|
format=HeartRateService.HEART_RATE_CONTROL_POINT_FORMAT
|
||||||
|
)
|
||||||
|
characteristics.append(self.heart_rate_control_point_characteristic)
|
||||||
|
|
||||||
|
super().__init__(characteristics)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class HeartRateServiceProxy(ProfileServiceProxy):
|
||||||
|
SERVICE_CLASS = HeartRateService
|
||||||
|
|
||||||
|
def __init__(self, service_proxy):
|
||||||
|
self.service_proxy = service_proxy
|
||||||
|
|
||||||
|
if characteristics := service_proxy.get_characteristics_by_uuid(GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC):
|
||||||
|
self.heart_rate_measurement = DelegatedCharacteristicAdapter(
|
||||||
|
characteristics[0],
|
||||||
|
decode=HeartRateService.HeartRateMeasurement.from_bytes
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.heart_rate_measurement = None
|
||||||
|
|
||||||
|
if characteristics := service_proxy.get_characteristics_by_uuid(GATT_BODY_SENSOR_LOCATION_CHARACTERISTIC):
|
||||||
|
self.body_sensor_location = DelegatedCharacteristicAdapter(
|
||||||
|
characteristics[0],
|
||||||
|
decode=lambda value: HeartRateService.BodySensorLocation(value[0])
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.body_sensor_location = None
|
||||||
|
|
||||||
|
if characteristics := service_proxy.get_characteristics_by_uuid(GATT_HEART_RATE_CONTROL_POINT_CHARACTERISTIC):
|
||||||
|
self.heart_rate_control_point = PackedCharacteristicAdapter(
|
||||||
|
characteristics[0],
|
||||||
|
format=HeartRateService.HEART_RATE_CONTROL_POINT_FORMAT
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.heart_rate_control_point = None
|
||||||
|
|
||||||
|
async def reset_energy_expended(self):
|
||||||
|
if self.heart_rate_control_point is not None:
|
||||||
|
return await self.heart_rate_control_point.write_value(HeartRateService.RESET_ENERGY_EXPENDED)
|
||||||
+59
-28
@@ -150,6 +150,8 @@ SMP_SC_AUTHREQ = 0b00001000
|
|||||||
SMP_KEYPRESS_AUTHREQ = 0b00010000
|
SMP_KEYPRESS_AUTHREQ = 0b00010000
|
||||||
SMP_CT2_AUTHREQ = 0b00100000
|
SMP_CT2_AUTHREQ = 0b00100000
|
||||||
|
|
||||||
|
# Crypto salt
|
||||||
|
SMP_CTKD_H7_LEBR_SALT = bytes.fromhex('00000000000000000000000000000000746D7031')
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Utils
|
# Utils
|
||||||
@@ -457,9 +459,17 @@ class PairingDelegate:
|
|||||||
DISPLAY_OUTPUT_ONLY = SMP_DISPLAY_ONLY_IO_CAPABILITY
|
DISPLAY_OUTPUT_ONLY = SMP_DISPLAY_ONLY_IO_CAPABILITY
|
||||||
DISPLAY_OUTPUT_AND_YES_NO_INPUT = SMP_DISPLAY_YES_NO_IO_CAPABILITY
|
DISPLAY_OUTPUT_AND_YES_NO_INPUT = SMP_DISPLAY_YES_NO_IO_CAPABILITY
|
||||||
DISPLAY_OUTPUT_AND_KEYBOARD_INPUT = SMP_KEYBOARD_DISPLAY_IO_CAPABILITY
|
DISPLAY_OUTPUT_AND_KEYBOARD_INPUT = SMP_KEYBOARD_DISPLAY_IO_CAPABILITY
|
||||||
|
DEFAULT_KEY_DISTRIBUTION = (SMP_ENC_KEY_DISTRIBUTION_FLAG | SMP_ID_KEY_DISTRIBUTION_FLAG)
|
||||||
|
|
||||||
def __init__(self, io_capability = NO_OUTPUT_NO_INPUT):
|
def __init__(
|
||||||
|
self,
|
||||||
|
io_capability=NO_OUTPUT_NO_INPUT,
|
||||||
|
local_initiator_key_distribution=DEFAULT_KEY_DISTRIBUTION,
|
||||||
|
local_responder_key_distribution=DEFAULT_KEY_DISTRIBUTION
|
||||||
|
):
|
||||||
self.io_capability = io_capability
|
self.io_capability = io_capability
|
||||||
|
self.local_initiator_key_distribution = local_initiator_key_distribution
|
||||||
|
self.local_responder_key_distribution = local_responder_key_distribution
|
||||||
|
|
||||||
async def accept(self):
|
async def accept(self):
|
||||||
return True
|
return True
|
||||||
@@ -473,6 +483,14 @@ class PairingDelegate:
|
|||||||
async def display_number(self, number, digits=6):
|
async def display_number(self, number, digits=6):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
async def key_distribution_response(self, peer_initiator_key_distribution, peer_responder_key_distribution):
|
||||||
|
return (
|
||||||
|
(peer_initiator_key_distribution &
|
||||||
|
self.local_initiator_key_distribution),
|
||||||
|
(peer_responder_key_distribution &
|
||||||
|
self.local_responder_key_distribution)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class PairingConfig:
|
class PairingConfig:
|
||||||
@@ -559,6 +577,7 @@ class Session:
|
|||||||
self.ltk = None
|
self.ltk = None
|
||||||
self.ltk_ediv = 0
|
self.ltk_ediv = 0
|
||||||
self.ltk_rand = bytes(8)
|
self.ltk_rand = bytes(8)
|
||||||
|
self.link_key = None
|
||||||
self.initiator_key_distribution = 0
|
self.initiator_key_distribution = 0
|
||||||
self.responder_key_distribution = 0
|
self.responder_key_distribution = 0
|
||||||
self.peer_random_value = None
|
self.peer_random_value = None
|
||||||
@@ -596,11 +615,8 @@ class Session:
|
|||||||
self.pairing_result = None
|
self.pairing_result = None
|
||||||
|
|
||||||
# Key Distribution (default values before negotiation)
|
# Key Distribution (default values before negotiation)
|
||||||
self.initiator_key_distribution = (
|
self.initiator_key_distribution = pairing_config.delegate.local_initiator_key_distribution
|
||||||
SMP_ENC_KEY_DISTRIBUTION_FLAG |
|
self.responder_key_distribution = pairing_config.delegate.local_responder_key_distribution
|
||||||
SMP_ID_KEY_DISTRIBUTION_FLAG # |SMP_SIGN_KEY_DISTRIBUTION_FLAG
|
|
||||||
)
|
|
||||||
self.responder_key_distribution = self.initiator_key_distribution
|
|
||||||
|
|
||||||
# Authentication Requirements Flags - Vol 3, Part H, Figure 3.3
|
# Authentication Requirements Flags - Vol 3, Part H, Figure 3.3
|
||||||
self.bonding = pairing_config.bonding
|
self.bonding = pairing_config.bonding
|
||||||
@@ -852,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,
|
||||||
@@ -870,47 +886,56 @@ class Session:
|
|||||||
self.send_command(SMP_Encryption_Information_Command(long_term_key=self.ltk))
|
self.send_command(SMP_Encryption_Information_Command(long_term_key=self.ltk))
|
||||||
self.send_command(SMP_Master_Identification_Command(ediv=self.ltk_ediv, rand=self.ltk_rand))
|
self.send_command(SMP_Master_Identification_Command(ediv=self.ltk_ediv, rand=self.ltk_rand))
|
||||||
|
|
||||||
# Distribute IRK
|
# Distribute IRK & BD ADDR
|
||||||
if self.initiator_key_distribution & SMP_ID_KEY_DISTRIBUTION_FLAG:
|
if self.initiator_key_distribution & SMP_ID_KEY_DISTRIBUTION_FLAG:
|
||||||
self.send_command(
|
self.send_command(
|
||||||
SMP_Identity_Information_Command(identity_resolving_key=self.manager.device.irk)
|
SMP_Identity_Information_Command(identity_resolving_key=self.manager.device.irk)
|
||||||
)
|
)
|
||||||
|
self.send_command(SMP_Identity_Address_Information_Command(
|
||||||
# Distribute BD ADDR
|
addr_type = self.manager.address.address_type,
|
||||||
self.send_command(SMP_Identity_Address_Information_Command(
|
bd_addr = self.manager.address
|
||||||
addr_type = self.manager.address.address_type,
|
))
|
||||||
bd_addr = self.manager.address
|
|
||||||
))
|
|
||||||
|
|
||||||
# Distribute CSRK
|
# Distribute CSRK
|
||||||
csrk = bytes(16) # FIXME: testing
|
csrk = bytes(16) # FIXME: testing
|
||||||
if self.initiator_key_distribution & SMP_SIGN_KEY_DISTRIBUTION_FLAG:
|
if self.initiator_key_distribution & SMP_SIGN_KEY_DISTRIBUTION_FLAG:
|
||||||
self.send_command(SMP_Signing_Information_Command(signature_key=csrk))
|
self.send_command(SMP_Signing_Information_Command(signature_key=csrk))
|
||||||
|
|
||||||
|
# CTKD, calculate BR/EDR link key
|
||||||
|
if self.initiator_key_distribution & SMP_LINK_KEY_DISTRIBUTION_FLAG:
|
||||||
|
ilk = crypto.h7(
|
||||||
|
salt=SMP_CTKD_H7_LEBR_SALT,
|
||||||
|
w=self.ltk) if self.ct2 else crypto.h6(self.ltk, b'tmp1')
|
||||||
|
self.link_key = crypto.h6(ilk, b'lebr')
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Distribute the LTK
|
# Distribute the LTK, EDIV and RAND
|
||||||
if not self.sc:
|
if not self.sc:
|
||||||
if self.responder_key_distribution & SMP_ENC_KEY_DISTRIBUTION_FLAG:
|
if self.responder_key_distribution & SMP_ENC_KEY_DISTRIBUTION_FLAG:
|
||||||
self.send_command(SMP_Encryption_Information_Command(long_term_key=self.ltk))
|
self.send_command(SMP_Encryption_Information_Command(long_term_key=self.ltk))
|
||||||
|
self.send_command(SMP_Master_Identification_Command(ediv=self.ltk_ediv, rand=self.ltk_rand))
|
||||||
|
|
||||||
# Distribute EDIV and RAND
|
# Distribute IRK & BD ADDR
|
||||||
self.send_command(SMP_Master_Identification_Command(ediv=self.ltk_ediv, rand=self.ltk_rand))
|
|
||||||
|
|
||||||
# Distribute IRK
|
|
||||||
if self.responder_key_distribution & SMP_ID_KEY_DISTRIBUTION_FLAG:
|
if self.responder_key_distribution & SMP_ID_KEY_DISTRIBUTION_FLAG:
|
||||||
self.send_command(
|
self.send_command(
|
||||||
SMP_Identity_Information_Command(identity_resolving_key=self.manager.device.irk)
|
SMP_Identity_Information_Command(identity_resolving_key=self.manager.device.irk)
|
||||||
)
|
)
|
||||||
|
self.send_command(SMP_Identity_Address_Information_Command(
|
||||||
# Distribute BD ADDR
|
addr_type = self.manager.address.address_type,
|
||||||
self.send_command(SMP_Identity_Address_Information_Command(
|
bd_addr = self.manager.address
|
||||||
addr_type = self.manager.address.address_type,
|
))
|
||||||
bd_addr = self.manager.address
|
|
||||||
))
|
|
||||||
|
|
||||||
# Distribute CSRK
|
# Distribute CSRK
|
||||||
csrk = bytes(16) # FIXME: testing
|
csrk = bytes(16) # FIXME: testing
|
||||||
if self.responder_key_distribution & SMP_SIGN_KEY_DISTRIBUTION_FLAG:
|
if self.responder_key_distribution & SMP_SIGN_KEY_DISTRIBUTION_FLAG:
|
||||||
self.send_command(SMP_Signing_Information_Command(signature_key=csrk))
|
self.send_command(SMP_Signing_Information_Command(signature_key=csrk))
|
||||||
|
|
||||||
|
# CTKD, calculate BR/EDR link key
|
||||||
|
if self.responder_key_distribution & SMP_LINK_KEY_DISTRIBUTION_FLAG:
|
||||||
|
ilk = crypto.h7(
|
||||||
|
salt=SMP_CTKD_H7_LEBR_SALT,
|
||||||
|
w=self.ltk) if self.ct2 else crypto.h6(self.ltk, b'tmp1')
|
||||||
|
self.link_key = crypto.h6(ilk, b'lebr')
|
||||||
|
|
||||||
def compute_peer_expected_distributions(self, key_distribution_flags):
|
def compute_peer_expected_distributions(self, key_distribution_flags):
|
||||||
# Set our expectations for what to wait for in the key distribution phase
|
# Set our expectations for what to wait for in the key distribution phase
|
||||||
@@ -945,7 +970,7 @@ class Session:
|
|||||||
# Nothing left to expect, we're done
|
# Nothing left to expect, we're done
|
||||||
self.on_pairing()
|
self.on_pairing()
|
||||||
else:
|
else:
|
||||||
logger.warn(color('!!! unexpected key distribution command', 'red'))
|
logger.warn(color(f'!!! unexpected key distribution command: {command_class.__name__}', 'red'))
|
||||||
self.send_pairing_failed(SMP_UNSPECIFIED_REASON_ERROR)
|
self.send_pairing_failed(SMP_UNSPECIFIED_REASON_ERROR)
|
||||||
|
|
||||||
async def pair(self):
|
async def pair(self):
|
||||||
@@ -1029,6 +1054,11 @@ class Session:
|
|||||||
value = self.peer_signature_key,
|
value = self.peer_signature_key,
|
||||||
authenticated = authenticated
|
authenticated = authenticated
|
||||||
)
|
)
|
||||||
|
if self.link_key is not None:
|
||||||
|
keys.link_key = PairingKeys.Key(
|
||||||
|
value = self.link_key,
|
||||||
|
authenticated = authenticated
|
||||||
|
)
|
||||||
|
|
||||||
self.manager.on_pairing(self, peer_address, keys)
|
self.manager.on_pairing(self, peer_address, keys)
|
||||||
|
|
||||||
@@ -1076,6 +1106,7 @@ class Session:
|
|||||||
# Bonding and SC require both sides to request/support it
|
# Bonding and SC require both sides to request/support it
|
||||||
self.bonding = self.bonding and (command.auth_req & SMP_BONDING_AUTHREQ != 0)
|
self.bonding = self.bonding and (command.auth_req & SMP_BONDING_AUTHREQ != 0)
|
||||||
self.sc = self.sc and (command.auth_req & SMP_SC_AUTHREQ != 0)
|
self.sc = self.sc and (command.auth_req & SMP_SC_AUTHREQ != 0)
|
||||||
|
self.ct2 = self.ct2 and (command.auth_req & SMP_CT2_AUTHREQ != 0)
|
||||||
|
|
||||||
# Check for OOB
|
# Check for OOB
|
||||||
if command.oob_data_flag != 0:
|
if command.oob_data_flag != 0:
|
||||||
@@ -1091,8 +1122,8 @@ class Session:
|
|||||||
logger.debug(f'pairing method: {self.PAIRING_METHOD_NAMES[self.pairing_method]}')
|
logger.debug(f'pairing method: {self.PAIRING_METHOD_NAMES[self.pairing_method]}')
|
||||||
|
|
||||||
# Key distribution
|
# Key distribution
|
||||||
self.initiator_key_distribution &= command.initiator_key_distribution
|
self.initiator_key_distribution, self.responder_key_distribution = await self.pairing_config.delegate.key_distribution_response(
|
||||||
self.responder_key_distribution &= command.responder_key_distribution
|
command.initiator_key_distribution, command.responder_key_distribution)
|
||||||
self.compute_peer_expected_distributions(self.initiator_key_distribution)
|
self.compute_peer_expected_distributions(self.initiator_key_distribution)
|
||||||
|
|
||||||
# The pairing is now starting
|
# The pairing is now starting
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ async def open_usb_transport(spec):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
logger.debug('USB event loop done')
|
logger.debug('USB event loop done')
|
||||||
self.event_loop_done.set_result(None)
|
self.loop.call_soon_threadsafe(self.event_loop_done.set_result, None)
|
||||||
|
|
||||||
async def close(self):
|
async def close(self):
|
||||||
self.closed = True
|
self.closed = True
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# 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 sys
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from colors import color
|
||||||
|
from bumble.device import Device, Peer
|
||||||
|
from bumble.transport import open_transport
|
||||||
|
from bumble.profiles.battery_service import BatteryServiceProxy
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
async def main():
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
print('Usage: battery_client.py <transport-spec> <bluetooth-address>')
|
||||||
|
print('example: battery_client.py usb:0 E1:CA:72:48:C4:E8')
|
||||||
|
return
|
||||||
|
|
||||||
|
print('<<< connecting to HCI...')
|
||||||
|
async with await open_transport(sys.argv[1]) as (hci_source, hci_sink):
|
||||||
|
print('<<< connected')
|
||||||
|
|
||||||
|
# Create and start a device
|
||||||
|
device = Device.with_hci('Bumble', 'F0:F1:F2:F3:F4:F5', hci_source, hci_sink)
|
||||||
|
await device.power_on()
|
||||||
|
|
||||||
|
# Connect to the peer
|
||||||
|
target_address = sys.argv[2]
|
||||||
|
print(f'=== Connecting to {target_address}...')
|
||||||
|
async with device.connect_as_gatt(target_address) as peer:
|
||||||
|
print(f'=== Connected to {peer}')
|
||||||
|
battery_service = peer.create_service_proxy(BatteryServiceProxy)
|
||||||
|
|
||||||
|
# Check that the service was found
|
||||||
|
if not battery_service:
|
||||||
|
print('!!! Service not found')
|
||||||
|
return
|
||||||
|
|
||||||
|
# Subscribe to and read the battery level
|
||||||
|
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 peer.sustain()
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
|
||||||
|
asyncio.run(main())
|
||||||
@@ -25,59 +25,41 @@ import struct
|
|||||||
from bumble.core import AdvertisingData
|
from bumble.core import AdvertisingData
|
||||||
from bumble.device import Device
|
from bumble.device import Device
|
||||||
from bumble.transport import open_transport_or_link
|
from bumble.transport import open_transport_or_link
|
||||||
from bumble.gatt import (
|
from bumble.profiles.battery_service import BatteryService
|
||||||
Service,
|
|
||||||
Characteristic,
|
|
||||||
CharacteristicValue,
|
|
||||||
GATT_DEVICE_BATTERY_SERVICE,
|
|
||||||
GATT_BATTERY_LEVEL_CHARACTERISTIC
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
def read_battery_level(connection):
|
|
||||||
return bytes([random.randint(0, 100)])
|
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
async def main():
|
async def main():
|
||||||
if len(sys.argv) != 3:
|
if len(sys.argv) != 3:
|
||||||
print('Usage: python battery_service.py <device-config> <transport-spec>')
|
print('Usage: python battery_server.py <device-config> <transport-spec>')
|
||||||
print('example: python battery_service.py device1.json usb:0')
|
print('example: python battery_server.py device1.json usb:0')
|
||||||
return
|
return
|
||||||
|
|
||||||
async with await open_transport_or_link(sys.argv[2]) as (hci_source, hci_sink):
|
async with await open_transport_or_link(sys.argv[2]) as (hci_source, hci_sink):
|
||||||
# Create a device to manage the host
|
|
||||||
device = Device.from_config_file_with_hci(sys.argv[1], hci_source, hci_sink)
|
device = Device.from_config_file_with_hci(sys.argv[1], hci_source, hci_sink)
|
||||||
|
|
||||||
# Add a Battery Service to the GATT sever
|
# Add a Battery Service to the GATT sever
|
||||||
device.add_services([
|
battery_service = BatteryService(lambda _: random.randint(0, 100))
|
||||||
Service(
|
device.add_service(battery_service)
|
||||||
GATT_DEVICE_BATTERY_SERVICE,
|
|
||||||
[
|
|
||||||
Characteristic(
|
|
||||||
GATT_BATTERY_LEVEL_CHARACTERISTIC,
|
|
||||||
Characteristic.READ,
|
|
||||||
Characteristic.READABLE,
|
|
||||||
CharacteristicValue(read=read_battery_level)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
])
|
|
||||||
|
|
||||||
# Set the advertising data
|
# Set the advertising data
|
||||||
device.advertising_data = bytes(
|
device.advertising_data = bytes(
|
||||||
AdvertisingData([
|
AdvertisingData([
|
||||||
(AdvertisingData.COMPLETE_LOCAL_NAME, bytes('Bumble Battery', 'utf-8')),
|
(AdvertisingData.COMPLETE_LOCAL_NAME, bytes('Bumble Battery', 'utf-8')),
|
||||||
(AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS, struct.pack('<H', 0x180F)),
|
(AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS, bytes(battery_service.uuid)),
|
||||||
(AdvertisingData.APPEARANCE, struct.pack('<H', 0x0340))
|
(AdvertisingData.APPEARANCE, struct.pack('<H', 0x0340))
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
|
|
||||||
# Go!
|
# Go!
|
||||||
await device.power_on()
|
await device.power_on()
|
||||||
await device.start_advertising()
|
await device.start_advertising(auto_restart=True)
|
||||||
await hci_source.wait_for_termination()
|
|
||||||
|
# Notify every 3 seconds
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(3.0)
|
||||||
|
await device.notify_subscribers(battery_service.battery_level_characteristic)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
|
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# 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 sys
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from colors import color
|
||||||
|
from bumble.device import Device, Peer
|
||||||
|
from bumble.profiles.device_information_service import DeviceInformationServiceProxy
|
||||||
|
from bumble.transport import open_transport
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
async def main():
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
print('Usage: device_information_client.py <transport-spec> <bluetooth-address>')
|
||||||
|
print('example: device_information_client.py usb:0 E1:CA:72:48:C4:E8')
|
||||||
|
return
|
||||||
|
|
||||||
|
print('<<< connecting to HCI...')
|
||||||
|
async with await open_transport(sys.argv[1]) as (hci_source, hci_sink):
|
||||||
|
print('<<< connected')
|
||||||
|
|
||||||
|
# Create and start a device
|
||||||
|
device = Device.with_hci('Bumble', 'F0:F1:F2:F3:F4:F5', hci_source, hci_sink)
|
||||||
|
await device.power_on()
|
||||||
|
|
||||||
|
# Connect to the peer
|
||||||
|
target_address = sys.argv[2]
|
||||||
|
print(f'=== Connecting to {target_address}...')
|
||||||
|
connection = await device.connect(target_address)
|
||||||
|
print(f'=== Connected to {connection}')
|
||||||
|
|
||||||
|
# Discover the Device Information service
|
||||||
|
peer = Peer(connection)
|
||||||
|
print('=== Discovering Device Information Service')
|
||||||
|
device_information_service = await peer.discover_service_and_create_proxy(DeviceInformationServiceProxy)
|
||||||
|
|
||||||
|
# Check that the service was found
|
||||||
|
if device_information_service is None:
|
||||||
|
print('!!! Service not found')
|
||||||
|
return
|
||||||
|
|
||||||
|
# Read and print the fields
|
||||||
|
if device_information_service.manufacturer_name is not None:
|
||||||
|
print(color('Manufacturer Name: ', 'green'), await device_information_service.manufacturer_name.read_value())
|
||||||
|
if device_information_service.model_number is not None:
|
||||||
|
print(color('Model Number: ', 'green'), await device_information_service.model_number.read_value())
|
||||||
|
if device_information_service.serial_number is not None:
|
||||||
|
print(color('Serial Number: ', 'green'), await device_information_service.serial_number.read_value())
|
||||||
|
if device_information_service.hardware_revision is not None:
|
||||||
|
print(color('Hardware Revision: ', 'green'), await device_information_service.hardware_revision.read_value())
|
||||||
|
if device_information_service.firmware_revision is not None:
|
||||||
|
print(color('Firmware Revision: ', 'green'), await device_information_service.firmware_revision.read_value())
|
||||||
|
if device_information_service.software_revision is not None:
|
||||||
|
print(color('Software Revision: ', 'green'), await device_information_service.software_revision.read_value())
|
||||||
|
if device_information_service.system_id is not None:
|
||||||
|
print(color('System ID: ', 'green'), await device_information_service.system_id.read_value())
|
||||||
|
if device_information_service.ieee_regulatory_certification_data_list is not None:
|
||||||
|
print(color('Regulatory Certification:', 'green'), (await device_information_service.ieee_regulatory_certification_data_list.read_value()).hex())
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
|
||||||
|
asyncio.run(main())
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# 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 sys
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import struct
|
||||||
|
|
||||||
|
from bumble.core import AdvertisingData
|
||||||
|
from bumble.device import Device
|
||||||
|
from bumble.transport import open_transport_or_link
|
||||||
|
from bumble.profiles.device_information_service import DeviceInformationService
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
async def main():
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
print('Usage: python device_info_server.py <device-config> <transport-spec>')
|
||||||
|
print('example: python device_info_server.py device1.json usb:0')
|
||||||
|
return
|
||||||
|
|
||||||
|
async with await open_transport_or_link(sys.argv[2]) as (hci_source, hci_sink):
|
||||||
|
device = Device.from_config_file_with_hci(sys.argv[1], hci_source, hci_sink)
|
||||||
|
|
||||||
|
# Add a Device Information Service to the GATT sever
|
||||||
|
device_information_service = DeviceInformationService(
|
||||||
|
manufacturer_name = 'ACME',
|
||||||
|
model_number = 'AB-102',
|
||||||
|
serial_number = '7654321',
|
||||||
|
hardware_revision = '1.1.3',
|
||||||
|
software_revision = '2.5.6',
|
||||||
|
system_id = (0x123456, 0x8877665544)
|
||||||
|
)
|
||||||
|
device.add_service(device_information_service)
|
||||||
|
|
||||||
|
# Set the advertising data
|
||||||
|
device.advertising_data = bytes(
|
||||||
|
AdvertisingData([
|
||||||
|
(AdvertisingData.COMPLETE_LOCAL_NAME, bytes('Bumble Device', 'utf-8')),
|
||||||
|
(AdvertisingData.APPEARANCE, struct.pack('<H', 0x0340))
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
# Go!
|
||||||
|
await device.power_on()
|
||||||
|
await device.start_advertising(auto_restart=True)
|
||||||
|
await hci_source.wait_for_termination()
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
|
||||||
|
asyncio.run(main())
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
# 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 sys
|
|
||||||
import os
|
|
||||||
import logging
|
|
||||||
from colors import color
|
|
||||||
from bumble.device import Device, Peer
|
|
||||||
from bumble.host import Host
|
|
||||||
from bumble.transport import open_transport
|
|
||||||
from bumble.utils import AsyncRunner
|
|
||||||
from bumble import gatt
|
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
class Listener(Device.Listener):
|
|
||||||
def __init__(self, device):
|
|
||||||
self.device = device
|
|
||||||
self.done = asyncio.get_running_loop().create_future()
|
|
||||||
|
|
||||||
@AsyncRunner.run_in_task()
|
|
||||||
async def on_connection(self, connection):
|
|
||||||
print(f'=== Connected to {connection}')
|
|
||||||
|
|
||||||
# Discover the Device Info service
|
|
||||||
peer = Peer(connection)
|
|
||||||
print('=== Discovering Device Info')
|
|
||||||
await peer.discover_services([gatt.GATT_DEVICE_INFORMATION_SERVICE])
|
|
||||||
|
|
||||||
# Check that the service was found
|
|
||||||
device_info_services = peer.get_services_by_uuid(gatt.GATT_DEVICE_INFORMATION_SERVICE)
|
|
||||||
if not device_info_services:
|
|
||||||
print('!!! Service not found')
|
|
||||||
return
|
|
||||||
|
|
||||||
# Get the characteristics we want from the (first) device info service
|
|
||||||
service = device_info_services[0]
|
|
||||||
await peer.discover_characteristics([
|
|
||||||
gatt.GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC
|
|
||||||
], service)
|
|
||||||
|
|
||||||
# Read the manufacturer name
|
|
||||||
manufacturer_name = peer.get_characteristics_by_uuid(gatt.GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC, service)
|
|
||||||
if manufacturer_name:
|
|
||||||
value = await peer.read_value(manufacturer_name[0])
|
|
||||||
print(color('Manufacturer Name:', 'green'), value.decode('utf-8'))
|
|
||||||
else:
|
|
||||||
print('>>> No manufacturer name')
|
|
||||||
|
|
||||||
self.done.set_result(None)
|
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
async def main():
|
|
||||||
if len(sys.argv) != 3:
|
|
||||||
print('Usage: get_peer_device_info.py <transport-spec> <bluetooth-address>')
|
|
||||||
print('example: get_peer_device_info.py usb:0 E1:CA:72:48:C4:E8')
|
|
||||||
return
|
|
||||||
|
|
||||||
print('<<< connecting to HCI...')
|
|
||||||
packet_source, packet_sink = await open_transport(sys.argv[1])
|
|
||||||
print('<<< connected')
|
|
||||||
|
|
||||||
# Create a host using the packet source and sink as controller
|
|
||||||
host = Host(controller_source=packet_source, controller_sink=packet_sink)
|
|
||||||
|
|
||||||
# Create a device to manage the host, with a custom listener
|
|
||||||
device = Device('Bumble', address = 'F0:F1:F2:F3:F4:F5', host = host)
|
|
||||||
device.listener = Listener(device)
|
|
||||||
await device.power_on()
|
|
||||||
|
|
||||||
# Connect to a peer
|
|
||||||
target_address = sys.argv[2]
|
|
||||||
print(f'=== Connecting to {target_address}...')
|
|
||||||
await device.connect(target_address)
|
|
||||||
await device.listener.done
|
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
|
|
||||||
asyncio.run(main())
|
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# 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 sys
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from colors import color
|
||||||
|
from bumble.device import Device, Peer
|
||||||
|
from bumble.transport import open_transport
|
||||||
|
from bumble.profiles.heart_rate_service import HeartRateServiceProxy
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
async def main():
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
print('Usage: heart_rate_client.py <transport-spec> <bluetooth-address>')
|
||||||
|
print('example: heart_rate_client.py usb:0 E1:CA:72:48:C4:E8')
|
||||||
|
return
|
||||||
|
|
||||||
|
print('<<< connecting to HCI...')
|
||||||
|
async with await open_transport(sys.argv[1]) as (hci_source, hci_sink):
|
||||||
|
print('<<< connected')
|
||||||
|
|
||||||
|
# Create and start a device
|
||||||
|
device = Device.with_hci('Bumble', 'F0:F1:F2:F3:F4:F5', hci_source, hci_sink)
|
||||||
|
await device.power_on()
|
||||||
|
|
||||||
|
# Connect to the peer
|
||||||
|
target_address = sys.argv[2]
|
||||||
|
print(f'=== Connecting to {target_address}...')
|
||||||
|
async with device.connect_as_gatt(target_address) as peer:
|
||||||
|
print(f'=== Connected to {peer}')
|
||||||
|
|
||||||
|
heart_rate_service = peer.create_service_proxy(HeartRateServiceProxy)
|
||||||
|
|
||||||
|
# Check that the service was found
|
||||||
|
if not heart_rate_service:
|
||||||
|
print('!!! Service not found')
|
||||||
|
return
|
||||||
|
|
||||||
|
# Read the body sensor location
|
||||||
|
if heart_rate_service.body_sensor_location:
|
||||||
|
location = await heart_rate_service.body_sensor_location.read_value()
|
||||||
|
print(color('Sensor Location:', 'green'), location)
|
||||||
|
|
||||||
|
# Subscribe to the heart rate measurement
|
||||||
|
if heart_rate_service.heart_rate_measurement:
|
||||||
|
await heart_rate_service.heart_rate_measurement.subscribe(
|
||||||
|
lambda value: print(f'{color("Heart Rate Measurement:", "green")} {value}')
|
||||||
|
)
|
||||||
|
|
||||||
|
await peer.sustain()
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
|
||||||
|
asyncio.run(main())
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
# 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 sys
|
||||||
|
import time
|
||||||
|
import math
|
||||||
|
import random
|
||||||
|
import struct
|
||||||
|
import logging
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
|
||||||
|
from bumble.core import AdvertisingData
|
||||||
|
from bumble.device import Device
|
||||||
|
from bumble.transport import open_transport_or_link
|
||||||
|
from bumble.profiles.device_information_service import DeviceInformationService
|
||||||
|
from bumble.profiles.heart_rate_service import HeartRateService
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
async def main():
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
print('Usage: python heart_rate_server.py <device-config> <transport-spec>')
|
||||||
|
print('example: python heart_rate_server.py device1.json usb:0')
|
||||||
|
return
|
||||||
|
|
||||||
|
async with await open_transport_or_link(sys.argv[2]) as (hci_source, hci_sink):
|
||||||
|
device = Device.from_config_file_with_hci(sys.argv[1], hci_source, hci_sink)
|
||||||
|
|
||||||
|
# Keep track of accumulated expended energy
|
||||||
|
energy_start_time = time.time()
|
||||||
|
|
||||||
|
def reset_energy_expended():
|
||||||
|
nonlocal energy_start_time
|
||||||
|
energy_start_time = time.time()
|
||||||
|
|
||||||
|
# Add a Device Information Service and Heart Rate Service to the GATT sever
|
||||||
|
device_information_service = DeviceInformationService(
|
||||||
|
manufacturer_name = 'ACME',
|
||||||
|
model_number = 'HR-102',
|
||||||
|
serial_number = '7654321',
|
||||||
|
hardware_revision = '1.1.3',
|
||||||
|
software_revision = '2.5.6',
|
||||||
|
system_id = (0x123456, 0x8877665544)
|
||||||
|
)
|
||||||
|
|
||||||
|
heart_rate_service = HeartRateService(
|
||||||
|
read_heart_rate_measurement = lambda _: HeartRateService.HeartRateMeasurement(
|
||||||
|
heart_rate = 100 + int(50 * math.sin(time.time() * math.pi / 60)),
|
||||||
|
sensor_contact_detected = random.choice((True, False, None)),
|
||||||
|
energy_expended = random.choice((int((time.time() - energy_start_time) * 100), None)),
|
||||||
|
rr_intervals = random.choice(((random.randint(900, 1100) / 1000, random.randint(900, 1100) / 1000), None))
|
||||||
|
),
|
||||||
|
body_sensor_location=HeartRateService.BodySensorLocation.WRIST,
|
||||||
|
reset_energy_expended=lambda _: reset_energy_expended()
|
||||||
|
)
|
||||||
|
|
||||||
|
device.add_services([device_information_service, heart_rate_service])
|
||||||
|
|
||||||
|
# Set the advertising data
|
||||||
|
device.advertising_data = bytes(
|
||||||
|
AdvertisingData([
|
||||||
|
(AdvertisingData.COMPLETE_LOCAL_NAME, bytes('Bumble Heart', 'utf-8')),
|
||||||
|
(AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS, bytes(heart_rate_service.uuid)),
|
||||||
|
(AdvertisingData.APPEARANCE, struct.pack('<H', 0x0340))
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
# Go!
|
||||||
|
await device.power_on()
|
||||||
|
await device.start_advertising(auto_restart=True)
|
||||||
|
|
||||||
|
# Notify every 3 seconds
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(3.0)
|
||||||
|
await device.notify_subscribers(heart_rate_service.heart_rate_measurement_characteristic)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
|
||||||
|
asyncio.run(main())
|
||||||
@@ -45,8 +45,7 @@ async def main():
|
|||||||
|
|
||||||
# Create a first controller using the packet source/sink as its host interface
|
# Create a first controller using the packet source/sink as its host interface
|
||||||
controller1 = Controller('C1', host_source = hci_source, host_sink = hci_sink, link = link)
|
controller1 = Controller('C1', host_source = hci_source, host_sink = hci_sink, link = link)
|
||||||
print("====", sys.argv)
|
controller1.random_address = sys.argv[1]
|
||||||
controller1.address = sys.argv[1]
|
|
||||||
|
|
||||||
# Create a second controller using the same link
|
# Create a second controller using the same link
|
||||||
controller2 = Controller('C2', link = link)
|
controller2 = Controller('C2', link = link)
|
||||||
|
|||||||
@@ -41,10 +41,10 @@ class Listener(Device.Listener):
|
|||||||
print('=== Discovering services')
|
print('=== Discovering services')
|
||||||
peer = Peer(connection)
|
peer = Peer(connection)
|
||||||
await peer.discover_services()
|
await peer.discover_services()
|
||||||
await peer.discover_characteristics()
|
|
||||||
for service in peer.services:
|
for service in peer.services:
|
||||||
|
await service.discover_characteristics()
|
||||||
for characteristic in service.characteristics:
|
for characteristic in service.characteristics:
|
||||||
await peer.discover_descriptors(characteristic)
|
await characteristic.discover_descriptors()
|
||||||
|
|
||||||
print('=== Services discovered')
|
print('=== Services discovered')
|
||||||
show_services(peer.services)
|
show_services(peer.services)
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ from bumble.controller import Controller
|
|||||||
from bumble.device import Device, Peer
|
from bumble.device import Device, Peer
|
||||||
from bumble.host import Host
|
from bumble.host import Host
|
||||||
from bumble.link import LocalLink
|
from bumble.link import LocalLink
|
||||||
from bumble.utils import AsyncRunner
|
|
||||||
from bumble.gatt import (
|
from bumble.gatt import (
|
||||||
Service,
|
Service,
|
||||||
Characteristic,
|
Characteristic,
|
||||||
@@ -37,43 +36,6 @@ from bumble.gatt import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
class ClientListener(Device.Listener):
|
|
||||||
def __init__(self, device):
|
|
||||||
self.device = device
|
|
||||||
|
|
||||||
@AsyncRunner.run_in_task()
|
|
||||||
async def on_connection(self, connection):
|
|
||||||
print(f'=== Client: connected to {connection}')
|
|
||||||
|
|
||||||
# Discover all services
|
|
||||||
print('=== Discovering services')
|
|
||||||
peer = Peer(connection)
|
|
||||||
await peer.discover_services()
|
|
||||||
await peer.discover_characteristics()
|
|
||||||
for service in peer.services:
|
|
||||||
for characteristic in service.characteristics:
|
|
||||||
await peer.discover_descriptors(characteristic)
|
|
||||||
|
|
||||||
print('=== Services discovered')
|
|
||||||
show_services(peer.services)
|
|
||||||
|
|
||||||
# Discover all attributes
|
|
||||||
print('=== Discovering attributes')
|
|
||||||
attributes = await peer.discover_attributes()
|
|
||||||
for attribute in attributes:
|
|
||||||
print(attribute)
|
|
||||||
print('=== Attributes discovered')
|
|
||||||
|
|
||||||
# Read all attributes
|
|
||||||
for attribute in attributes:
|
|
||||||
try:
|
|
||||||
value = await peer.read_value(attribute)
|
|
||||||
print(color(f'0x{attribute.handle:04X} = {value.hex()}', 'green'))
|
|
||||||
except ProtocolError as error:
|
|
||||||
print(color(f'cannot read {attribute.handle:04X}:', 'red'), error)
|
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class ServerListener(Device.Listener):
|
class ServerListener(Device.Listener):
|
||||||
def on_connection(self, connection):
|
def on_connection(self, connection):
|
||||||
@@ -90,7 +52,6 @@ async def main():
|
|||||||
client_host = Host()
|
client_host = Host()
|
||||||
client_host.controller = client_controller
|
client_host.controller = client_controller
|
||||||
client_device = Device("client", address = 'F0:F1:F2:F3:F4:F5', host = client_host)
|
client_device = Device("client", address = 'F0:F1:F2:F3:F4:F5', host = client_host)
|
||||||
client_device.listener = ClientListener(client_device)
|
|
||||||
await client_device.power_on()
|
await client_device.power_on()
|
||||||
|
|
||||||
# Setup a stack for the server
|
# Setup a stack for the server
|
||||||
@@ -116,7 +77,36 @@ async def main():
|
|||||||
server_device.add_service(device_info_service)
|
server_device.add_service(device_info_service)
|
||||||
|
|
||||||
# Connect the client to the server
|
# Connect the client to the server
|
||||||
await client_device.connect(server_device.address)
|
connection = await client_device.connect(server_device.random_address)
|
||||||
|
print(f'=== Client: connected to {connection}')
|
||||||
|
|
||||||
|
# Discover all services
|
||||||
|
print('=== Discovering services')
|
||||||
|
peer = Peer(connection)
|
||||||
|
await peer.discover_services()
|
||||||
|
for service in peer.services:
|
||||||
|
await service.discover_characteristics()
|
||||||
|
for characteristic in service.characteristics:
|
||||||
|
await characteristic.discover_descriptors()
|
||||||
|
|
||||||
|
print('=== Services discovered')
|
||||||
|
show_services(peer.services)
|
||||||
|
|
||||||
|
# Discover all attributes
|
||||||
|
print('=== Discovering attributes')
|
||||||
|
attributes = await peer.discover_attributes()
|
||||||
|
for attribute in attributes:
|
||||||
|
print(attribute)
|
||||||
|
print('=== Attributes discovered')
|
||||||
|
|
||||||
|
# Read all attributes
|
||||||
|
for attribute in attributes:
|
||||||
|
try:
|
||||||
|
value = await attribute.read_value()
|
||||||
|
print(color(f'0x{attribute.handle:04X} = {value.hex()}', 'green'))
|
||||||
|
except ProtocolError as error:
|
||||||
|
print(color(f'cannot read {attribute.handle:04X}:', 'red'), error)
|
||||||
|
|
||||||
await asyncio.get_running_loop().create_future()
|
await asyncio.get_running_loop().create_future()
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+198
-17
@@ -18,6 +18,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import struct
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from bumble.controller import Controller
|
from bumble.controller import Controller
|
||||||
@@ -25,6 +26,12 @@ from bumble.link import LocalLink
|
|||||||
from bumble.device import Device, Peer
|
from bumble.device import Device, Peer
|
||||||
from bumble.host import Host
|
from bumble.host import Host
|
||||||
from bumble.gatt import (
|
from bumble.gatt import (
|
||||||
|
GATT_BATTERY_LEVEL_CHARACTERISTIC,
|
||||||
|
CharacteristicAdapter,
|
||||||
|
DelegatedCharacteristicAdapter,
|
||||||
|
PackedCharacteristicAdapter,
|
||||||
|
MappedCharacteristicAdapter,
|
||||||
|
UTF8CharacteristicAdapter,
|
||||||
Service,
|
Service,
|
||||||
Characteristic,
|
Characteristic,
|
||||||
CharacteristicValue
|
CharacteristicValue
|
||||||
@@ -91,6 +98,96 @@ def test_ATT_Read_By_Group_Type_Request():
|
|||||||
basic_check(pdu)
|
basic_check(pdu)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def test_CharacteristicAdapter():
|
||||||
|
# Check that the CharacteristicAdapter base class is transparent
|
||||||
|
v = bytes([1, 2, 3])
|
||||||
|
c = Characteristic(GATT_BATTERY_LEVEL_CHARACTERISTIC, Characteristic.READ, Characteristic.READABLE, v)
|
||||||
|
a = CharacteristicAdapter(c)
|
||||||
|
|
||||||
|
value = a.read_value(None)
|
||||||
|
assert(value == v)
|
||||||
|
|
||||||
|
v = bytes([3, 4, 5])
|
||||||
|
a.write_value(None, v)
|
||||||
|
assert(c.value == v)
|
||||||
|
|
||||||
|
# Simple delegated adapter
|
||||||
|
a = DelegatedCharacteristicAdapter(c, lambda x: bytes(reversed(x)), lambda x: bytes(reversed(x)))
|
||||||
|
|
||||||
|
value = a.read_value(None)
|
||||||
|
assert(value == bytes(reversed(v)))
|
||||||
|
|
||||||
|
v = bytes([3, 4, 5])
|
||||||
|
a.write_value(None, v)
|
||||||
|
assert(a.value == bytes(reversed(v)))
|
||||||
|
|
||||||
|
# Packed adapter with single element format
|
||||||
|
v = 1234
|
||||||
|
pv = struct.pack('>H', v)
|
||||||
|
c.value = v
|
||||||
|
a = PackedCharacteristicAdapter(c, '>H')
|
||||||
|
|
||||||
|
value = a.read_value(None)
|
||||||
|
assert(value == pv)
|
||||||
|
c.value = None
|
||||||
|
a.write_value(None, pv)
|
||||||
|
assert(a.value == v)
|
||||||
|
|
||||||
|
# Packed adapter with multi-element format
|
||||||
|
v1 = 1234
|
||||||
|
v2 = 5678
|
||||||
|
pv = struct.pack('>HH', v1, v2)
|
||||||
|
c.value = (v1, v2)
|
||||||
|
a = PackedCharacteristicAdapter(c, '>HH')
|
||||||
|
|
||||||
|
value = a.read_value(None)
|
||||||
|
assert(value == pv)
|
||||||
|
c.value = None
|
||||||
|
a.write_value(None, pv)
|
||||||
|
assert(a.value == (v1, v2))
|
||||||
|
|
||||||
|
# Mapped adapter
|
||||||
|
v1 = 1234
|
||||||
|
v2 = 5678
|
||||||
|
pv = struct.pack('>HH', v1, v2)
|
||||||
|
mapped = {'v1': v1, 'v2': v2}
|
||||||
|
c.value = mapped
|
||||||
|
a = MappedCharacteristicAdapter(c, '>HH', ('v1', 'v2'))
|
||||||
|
|
||||||
|
value = a.read_value(None)
|
||||||
|
assert(value == pv)
|
||||||
|
c.value = None
|
||||||
|
a.write_value(None, pv)
|
||||||
|
assert(a.value == mapped)
|
||||||
|
|
||||||
|
# UTF-8 adapter
|
||||||
|
v = 'Hello π'
|
||||||
|
ev = v.encode('utf-8')
|
||||||
|
c.value = v
|
||||||
|
a = UTF8CharacteristicAdapter(c)
|
||||||
|
|
||||||
|
value = a.read_value(None)
|
||||||
|
assert(value == ev)
|
||||||
|
c.value = None
|
||||||
|
a.write_value(None, ev)
|
||||||
|
assert(a.value == v)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def test_CharacteristicValue():
|
||||||
|
b = bytes([1, 2, 3])
|
||||||
|
c = CharacteristicValue(read=lambda _: b)
|
||||||
|
x = c.read(None)
|
||||||
|
assert(x == b)
|
||||||
|
|
||||||
|
result = []
|
||||||
|
c = CharacteristicValue(write=lambda connection, value: result.append((connection, value)))
|
||||||
|
z = object()
|
||||||
|
c.write(z, b)
|
||||||
|
assert(result == [(z, b)])
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class TwoDevices:
|
class TwoDevices:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -199,6 +296,56 @@ async def test_read_write():
|
|||||||
assert(characteristic2._last_value[1] == b)
|
assert(characteristic2._last_value[1] == b)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_read_write2():
|
||||||
|
[client, server] = TwoDevices().devices
|
||||||
|
|
||||||
|
v = bytes([0x11, 0x22, 0x33, 0x44])
|
||||||
|
characteristic1 = Characteristic(
|
||||||
|
'FDB159DB-036C-49E3-B3DB-6325AC750806',
|
||||||
|
Characteristic.READ | Characteristic.WRITE,
|
||||||
|
Characteristic.READABLE | Characteristic.WRITEABLE,
|
||||||
|
value=v
|
||||||
|
)
|
||||||
|
|
||||||
|
service1 = Service(
|
||||||
|
'3A657F47-D34F-46B3-B1EC-698E29B6B829',
|
||||||
|
[
|
||||||
|
characteristic1
|
||||||
|
]
|
||||||
|
)
|
||||||
|
server.add_services([service1])
|
||||||
|
|
||||||
|
await client.power_on()
|
||||||
|
await server.power_on()
|
||||||
|
connection = await client.connect(server.random_address)
|
||||||
|
peer = Peer(connection)
|
||||||
|
|
||||||
|
await peer.discover_services()
|
||||||
|
c = peer.get_services_by_uuid(service1.uuid)
|
||||||
|
assert(len(c) == 1)
|
||||||
|
s = c[0]
|
||||||
|
await s.discover_characteristics()
|
||||||
|
c = s.get_characteristics_by_uuid(characteristic1.uuid)
|
||||||
|
assert(len(c) == 1)
|
||||||
|
c1 = c[0]
|
||||||
|
|
||||||
|
v1 = await c1.read_value()
|
||||||
|
assert(v1 == v)
|
||||||
|
|
||||||
|
a1 = PackedCharacteristicAdapter(c1, '>I')
|
||||||
|
v1 = await a1.read_value()
|
||||||
|
assert(v1 == struct.unpack('>I', v)[0])
|
||||||
|
|
||||||
|
b = bytes([0x55, 0x66, 0x77, 0x88])
|
||||||
|
await a1.write_value(struct.unpack('>I', b)[0])
|
||||||
|
await async_barrier()
|
||||||
|
assert(characteristic1.value == b)
|
||||||
|
v1 = await a1.read_value()
|
||||||
|
assert(v1 == struct.unpack('>I', b)[0])
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_subscribe_notify():
|
async def test_subscribe_notify():
|
||||||
@@ -272,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)
|
||||||
@@ -287,49 +436,79 @@ 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)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
async def async_main():
|
async def async_main():
|
||||||
await test_read_write()
|
await test_read_write()
|
||||||
|
await test_read_write2()
|
||||||
await test_subscribe_notify()
|
await test_subscribe_notify()
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -338,4 +517,6 @@ if __name__ == '__main__':
|
|||||||
test_UUID()
|
test_UUID()
|
||||||
test_ATT_Error_Response()
|
test_ATT_Error_Response()
|
||||||
test_ATT_Read_By_Group_Type_Request()
|
test_ATT_Read_By_Group_Type_Request()
|
||||||
|
test_CharacteristicValue()
|
||||||
|
test_CharacteristicAdapter()
|
||||||
asyncio.run(async_main())
|
asyncio.run(async_main())
|
||||||
|
|||||||
+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()
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|||||||
+66
-15
@@ -16,6 +16,7 @@
|
|||||||
# Imports
|
# Imports
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import pytest
|
import pytest
|
||||||
@@ -30,7 +31,8 @@ from bumble.smp import (
|
|||||||
PairingConfig,
|
PairingConfig,
|
||||||
PairingDelegate,
|
PairingDelegate,
|
||||||
SMP_PAIRING_NOT_SUPPORTED_ERROR,
|
SMP_PAIRING_NOT_SUPPORTED_ERROR,
|
||||||
SMP_CONFIRM_VALUE_FAILED_ERROR
|
SMP_CONFIRM_VALUE_FAILED_ERROR,
|
||||||
|
SMP_ID_KEY_DISTRIBUTION_FLAG,
|
||||||
)
|
)
|
||||||
from bumble.core import ProtocolError
|
from bumble.core import ProtocolError
|
||||||
|
|
||||||
@@ -161,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
|
||||||
@@ -196,11 +236,28 @@ async def _test_self_smp_with_configs(pairing_config1, pairing_config2):
|
|||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
IO_CAP = [
|
||||||
|
PairingDelegate.NO_OUTPUT_NO_INPUT,
|
||||||
|
PairingDelegate.KEYBOARD_INPUT_ONLY,
|
||||||
|
PairingDelegate.DISPLAY_OUTPUT_ONLY,
|
||||||
|
PairingDelegate.DISPLAY_OUTPUT_AND_YES_NO_INPUT,
|
||||||
|
PairingDelegate.DISPLAY_OUTPUT_AND_KEYBOARD_INPUT
|
||||||
|
]
|
||||||
|
SC = [False, True]
|
||||||
|
MITM = [False, True]
|
||||||
|
# Key distribution is a 4-bit bitmask
|
||||||
|
# IdKey is necessary for current SMP structure
|
||||||
|
KEY_DIST = [i for i in range(16) if (i & SMP_ID_KEY_DISTRIBUTION_FLAG)]
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_self_smp():
|
@pytest.mark.parametrize('io_cap, sc, mitm, key_dist',
|
||||||
|
itertools.product(IO_CAP, SC, MITM, KEY_DIST)
|
||||||
|
)
|
||||||
|
async def test_self_smp(io_cap, sc, mitm, key_dist):
|
||||||
class Delegate(PairingDelegate):
|
class Delegate(PairingDelegate):
|
||||||
def __init__(self, name, io_capability):
|
def __init__(self, name, io_capability, local_initiator_key_distribution, local_responder_key_distribution):
|
||||||
super().__init__(io_capability)
|
super().__init__(io_capability, local_initiator_key_distribution,
|
||||||
|
local_responder_key_distribution)
|
||||||
self.name = name
|
self.name = name
|
||||||
self.reset()
|
self.reset()
|
||||||
|
|
||||||
@@ -240,17 +297,8 @@ async def test_self_smp():
|
|||||||
|
|
||||||
pairing_config_sets = [('Initiator', [None]), ('Responder', [None])]
|
pairing_config_sets = [('Initiator', [None]), ('Responder', [None])]
|
||||||
for pairing_config_set in pairing_config_sets:
|
for pairing_config_set in pairing_config_sets:
|
||||||
for io_capability in [
|
delegate = Delegate(pairing_config_set[0], io_cap, key_dist, key_dist)
|
||||||
PairingDelegate.NO_OUTPUT_NO_INPUT,
|
pairing_config_set[1].append(PairingConfig(sc, mitm, True, delegate))
|
||||||
PairingDelegate.KEYBOARD_INPUT_ONLY,
|
|
||||||
PairingDelegate.DISPLAY_OUTPUT_ONLY,
|
|
||||||
PairingDelegate.DISPLAY_OUTPUT_AND_YES_NO_INPUT,
|
|
||||||
PairingDelegate.DISPLAY_OUTPUT_AND_KEYBOARD_INPUT
|
|
||||||
]:
|
|
||||||
for sc in [False, True]:
|
|
||||||
for mitm in [False, True]:
|
|
||||||
delegate = Delegate(pairing_config_set[0], io_capability)
|
|
||||||
pairing_config_set[1].append(PairingConfig(sc, mitm, True, delegate))
|
|
||||||
|
|
||||||
for pairing_config1 in pairing_config_sets[0][1]:
|
for pairing_config1 in pairing_config_sets[0][1]:
|
||||||
for pairing_config2 in pairing_config_sets[1][1]:
|
for pairing_config2 in pairing_config_sets[1][1]:
|
||||||
@@ -262,9 +310,11 @@ async def test_self_smp():
|
|||||||
if pairing_config1 and pairing_config2:
|
if pairing_config1 and pairing_config2:
|
||||||
pairing_config1.delegate.peer_delegate = pairing_config2.delegate
|
pairing_config1.delegate.peer_delegate = pairing_config2.delegate
|
||||||
pairing_config2.delegate.peer_delegate = pairing_config1.delegate
|
pairing_config2.delegate.peer_delegate = pairing_config1.delegate
|
||||||
|
|
||||||
await _test_self_smp_with_configs(pairing_config1, pairing_config2)
|
await _test_self_smp_with_configs(pairing_config1, pairing_config2)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_self_smp_reject():
|
async def test_self_smp_reject():
|
||||||
@@ -311,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()
|
||||||
|
|||||||
@@ -176,6 +176,20 @@ def test_g2():
|
|||||||
assert(value == 0x2f9ed5ba)
|
assert(value == 0x2f9ed5ba)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def test_h6():
|
||||||
|
KEY = bytes.fromhex('ec0234a3 57c8ad05 341010a6 0a397d9b')
|
||||||
|
KEY_ID = bytes.fromhex('6c656272')
|
||||||
|
assert(h6(KEY, KEY_ID) == bytes.fromhex('2d9ae102 e76dc91c e8d3a9e2 80b16399'))
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def test_h7():
|
||||||
|
KEY = bytes.fromhex('ec0234a3 57c8ad05 341010a6 0a397d9b')
|
||||||
|
SALT = bytes.fromhex('00000000 00000000 00000000 746D7031')
|
||||||
|
assert(h7(SALT, KEY) == bytes.fromhex('fb173597 c6a3c0ec d2998c2a 75a57011'))
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
def test_ah():
|
def test_ah():
|
||||||
irk = bytes(reversed(bytes.fromhex('ec0234a3 57c8ad05 341010a6 0a397d9b')))
|
irk = bytes(reversed(bytes.fromhex('ec0234a3 57c8ad05 341010a6 0a397d9b')))
|
||||||
@@ -195,4 +209,6 @@ if __name__ == '__main__':
|
|||||||
test_f5()
|
test_f5()
|
||||||
test_f6()
|
test_f6()
|
||||||
test_g2()
|
test_g2()
|
||||||
|
test_h6()
|
||||||
|
test_h7()
|
||||||
test_ah()
|
test_ah()
|
||||||
|
|||||||
Reference in New Issue
Block a user