forked from auracaster/bumble_mirror
Compare commits
266 Commits
gbg/cli-sc
...
v0.0.150
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dad7957d92 | ||
|
|
4ffc14482f | ||
|
|
63794981b7 | ||
|
|
5f86cddc85 | ||
|
|
51d3a869a4 | ||
|
|
dd930e3bde | ||
|
|
9af426db45 | ||
|
|
4286b2ab59 | ||
|
|
3442358dea | ||
|
|
bf3e05ef91 | ||
|
|
5351ab8a42 | ||
|
|
49b2c13e69 | ||
|
|
2c2f512180 | ||
|
|
859aea5a99 | ||
|
|
962737a97b | ||
|
|
85496aaff5 | ||
|
|
a95e601a5c | ||
|
|
df218b5370 | ||
|
|
0f737244b5 | ||
|
|
a258ba383a | ||
|
|
c53e1d2480 | ||
|
|
620c135ac4 | ||
|
|
fca73a49a3 | ||
|
|
cf70db84a1 | ||
|
|
7731c41f80 | ||
|
|
278341cbc0 | ||
|
|
fb49a87494 | ||
|
|
eba82b9d9a | ||
|
|
677fc77d3c | ||
|
|
e026de295f | ||
|
|
52c15705e9 | ||
|
|
45ca0ef071 | ||
|
|
e0af954baa | ||
|
|
044597de66 | ||
|
|
fb68fa6a33 | ||
|
|
b6fe7460ac | ||
|
|
5c59b6ca6d | ||
|
|
dcd66743f6 | ||
|
|
423a5a95d8 | ||
|
|
6f1f185642 | ||
|
|
8e881fdb18 | ||
|
|
4907022398 | ||
|
|
e93f71c035 | ||
|
|
94ff80563b | ||
|
|
552deab8a7 | ||
|
|
a72beb1b06 | ||
|
|
7e62d4a81a | ||
|
|
a50181e6b8 | ||
|
|
9e1358536b | ||
|
|
21d8a0d577 | ||
|
|
a8e61673d0 | ||
|
|
bd25cf27df | ||
|
|
fdf2da7023 | ||
|
|
dfb6734324 | ||
|
|
51ae6a5969 | ||
|
|
4fc13585cc | ||
|
|
c5e5397ed8 | ||
|
|
4c6320f98a | ||
|
|
cc0d56ad14 | ||
|
|
0019fa8e79 | ||
|
|
7ae1bf8959 | ||
|
|
9541cb6db0 | ||
|
|
1cd13dfc19 | ||
|
|
d4346c3c9b | ||
|
|
afe8765508 | ||
|
|
41d1772cb5 | ||
|
|
6e9078d60e | ||
|
|
d5c7d0db57 | ||
|
|
b70ebdef73 | ||
|
|
3af027e234 | ||
|
|
6e719ca9fd | ||
|
|
1a580d1c1e | ||
|
|
aee7348687 | ||
|
|
864889ccab | ||
|
|
fda00dcb28 | ||
|
|
77e5618ce7 | ||
|
|
6fa857ad13 | ||
|
|
bc29f327ef | ||
|
|
1894b96de4 | ||
|
|
c4fb63d35c | ||
|
|
33ae047765 | ||
|
|
1efa2e9d44 | ||
|
|
aa9af61cbe | ||
|
|
dc3ac3060e | ||
|
|
c34c5fdf17 | ||
|
|
e77723a5f9 | ||
|
|
fe8cf51432 | ||
|
|
97a0e115ae | ||
|
|
46e7aac77c | ||
|
|
08a6f4fa49 | ||
|
|
ca063eda0b | ||
|
|
c97ba4319f | ||
|
|
a5275ade29 | ||
|
|
e7b39c4188 | ||
|
|
0594eaef09 | ||
|
|
05200284d2 | ||
|
|
d21da78aa3 | ||
|
|
fbc7cf02a3 | ||
|
|
a8beb6b1ff | ||
|
|
2d44de611f | ||
|
|
9874bb3b37 | ||
|
|
6645ad47ee | ||
|
|
ad27de7717 | ||
|
|
e6fc63b2d8 | ||
|
|
1321c7da81 | ||
|
|
5a1b03fd91 | ||
|
|
de47721753 | ||
|
|
83a76a75d3 | ||
|
|
d5b5ef8313 | ||
|
|
856a8d53cd | ||
|
|
177c273a57 | ||
|
|
24a863983d | ||
|
|
b7ef09d4a3 | ||
|
|
b5b6cd13b8 | ||
|
|
ef781bc374 | ||
|
|
00978c1d63 | ||
|
|
b731f6f556 | ||
|
|
ed261886e1 | ||
|
|
5e18094c31 | ||
|
|
9a9b4e5bf1 | ||
|
|
895f1618d8 | ||
|
|
52746e0c68 | ||
|
|
f9b7072423 | ||
|
|
fa4be1958f | ||
|
|
f1686d8a9a | ||
|
|
5c6a7f2036 | ||
|
|
99758e4b7d | ||
|
|
7385de6a69 | ||
|
|
bb297e7516 | ||
|
|
8a91c614c7 | ||
|
|
70a50a74b7 | ||
|
|
6a16c61c5f | ||
|
|
0a22f2f7c7 | ||
|
|
422b05ad51 | ||
|
|
16e926a216 | ||
|
|
e94dc66d0c | ||
|
|
e37c77532b | ||
|
|
8b9ce03e86 | ||
|
|
7e854efbbb | ||
|
|
64b75be29b | ||
|
|
06018211fe | ||
|
|
e640991608 | ||
|
|
1068a6858d | ||
|
|
17db5dd4ff | ||
|
|
ea0a7e2347 | ||
|
|
6febd1ba35 | ||
|
|
ea6a8d4339 | ||
|
|
ce049865a4 | ||
|
|
6e0129b71d | ||
|
|
7ae3a1d973 | ||
|
|
c2959dadb4 | ||
|
|
80fe2ea422 | ||
|
|
08e6590a76 | ||
|
|
f580ffcbc3 | ||
|
|
5178c866ac | ||
|
|
441933bd64 | ||
|
|
287df94090 | ||
|
|
86f9496575 | ||
|
|
f5fe3d87f2 | ||
|
|
f65bed2ec4 | ||
|
|
3efe35065d | ||
|
|
83b42488ea | ||
|
|
135df0dcc0 | ||
|
|
8bef344879 | ||
|
|
55e2f23e29 | ||
|
|
297246fa4c | ||
|
|
52db1cfcc1 | ||
|
|
29f9a79502 | ||
|
|
c86125de4f | ||
|
|
697d5df3f8 | ||
|
|
87aa4f617e | ||
|
|
a8eff737e6 | ||
|
|
4417eb636c | ||
|
|
f4e5e61bbb | ||
|
|
ba7a60025f | ||
|
|
d92b7e9b74 | ||
|
|
b0336adf1c | ||
|
|
691450c7de | ||
|
|
99a0eb21c1 | ||
|
|
ab4859bd94 | ||
|
|
0d70cbde64 | ||
|
|
f41d0682b2 | ||
|
|
062dc1e53d | ||
|
|
662704e551 | ||
|
|
02a474c44e | ||
|
|
a1c7aec492 | ||
|
|
6112f00049 | ||
|
|
f56ac14f2c | ||
|
|
a739fc71ce | ||
|
|
b89f9030a0 | ||
|
|
9e5a85bd10 | ||
|
|
b437bd8619 | ||
|
|
a3e4674819 | ||
|
|
5f1d57fcb0 | ||
|
|
ae0b739e4a | ||
|
|
0570b59796 | ||
|
|
22218627f6 | ||
|
|
1c72242264 | ||
|
|
9c133706e6 | ||
|
|
4988a31487 | ||
|
|
e6c062117f | ||
|
|
f2133235d5 | ||
|
|
867e8c13dc | ||
|
|
25ce38c3f5 | ||
|
|
c0810230a6 | ||
|
|
27c46eef9d | ||
|
|
c140876157 | ||
|
|
d743656f09 | ||
|
|
b91d0e24c1 | ||
|
|
eb46f60c87 | ||
|
|
8bbba7c84c | ||
|
|
ee54df2d08 | ||
|
|
6549e53398 | ||
|
|
0f219eff12 | ||
|
|
4a1345cf95 | ||
|
|
8a1cdef152 | ||
|
|
6e1baf0344 | ||
|
|
cea1905ffb | ||
|
|
af8e0d4dc7 | ||
|
|
875195aebb | ||
|
|
5aee37aeab | ||
|
|
edcb7d05d6 | ||
|
|
ce9004f0ac | ||
|
|
d4228e3b5b | ||
|
|
be8f8ac68f | ||
|
|
ca16410a6d | ||
|
|
b95888eb39 | ||
|
|
56ed46adfa | ||
|
|
7044102e05 | ||
|
|
ca8f284888 | ||
|
|
e9e14f5183 | ||
|
|
b961affd3d | ||
|
|
51ddb36c91 | ||
|
|
78534b659a | ||
|
|
ce9472bf42 | ||
|
|
fc331b7aea | ||
|
|
8119bc210c | ||
|
|
65deefdc64 | ||
|
|
2920c3b0d1 | ||
|
|
f5cd825dbc | ||
|
|
cf4c43c4ff | ||
|
|
da2f596e52 | ||
|
|
c8aa0b4ef6 | ||
|
|
75ac276c8b | ||
|
|
dd4023ff56 | ||
|
|
dde8c5e1c2 | ||
|
|
8ed1f4b50d | ||
|
|
92de7dff4f | ||
|
|
16b4f18c92 | ||
|
|
46f4b82d29 | ||
|
|
4e2f66f709 | ||
|
|
3d79d7def5 | ||
|
|
915405a9bd | ||
|
|
45dd849d9f | ||
|
|
7208fd6642 | ||
|
|
eb8556ccf6 | ||
|
|
4d96b821bc | ||
|
|
78b36d2049 | ||
|
|
3e0cad1456 | ||
|
|
b4de38cdc3 | ||
|
|
68d9fbc159 | ||
|
|
a916b7a21a | ||
|
|
6ff52df8bd | ||
|
|
7fa2eb7658 | ||
|
|
86618e52ef | ||
|
|
fbb46dd736 |
2
.git-blame-ignore-revs
Normal file
2
.git-blame-ignore-revs
Normal file
@@ -0,0 +1,2 @@
|
||||
# Migrate code style to Black
|
||||
135df0dcc01ab765f432e19b1a5202d29bd55545
|
||||
35
.github/workflows/code-check.yml
vendored
Normal file
35
.github/workflows/code-check.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
# Check the code against the formatter and linter
|
||||
name: Code format and lint check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Check Code
|
||||
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,test,development]"
|
||||
- name: Check
|
||||
run: |
|
||||
invoke project.pre-commit
|
||||
16
.github/workflows/python-build-test.yml
vendored
16
.github/workflows/python-build-test.yml
vendored
@@ -14,6 +14,10 @@ jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.8", "3.9", "3.10"]
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- name: Check out from Git
|
||||
@@ -21,18 +25,18 @@ jobs:
|
||||
- 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
|
||||
uses: actions/setup-python@v3
|
||||
git fetch --depth=1 origin +refs/tags/*:refs/tags/*
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.10"
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install ".[build,test,development,documentation]"
|
||||
- name: Test with pytest
|
||||
- name: Test
|
||||
run: |
|
||||
pytest
|
||||
invoke test
|
||||
- name: Build
|
||||
run: |
|
||||
inv build
|
||||
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -3,9 +3,9 @@ build/
|
||||
dist/
|
||||
*.egg-info/
|
||||
*~
|
||||
bumble/__pycache__
|
||||
docs/mkdocs/site
|
||||
tests/__pycache__
|
||||
test-results.xml
|
||||
bumble/transport/__pycache__
|
||||
bumble/profiles/__pycache__
|
||||
__pycache__
|
||||
# generated by setuptools_scm
|
||||
bumble/_version.py
|
||||
.vscode/launch.json
|
||||
|
||||
80
.vscode/settings.json
vendored
Normal file
80
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"Abortable",
|
||||
"altsetting",
|
||||
"ansiblue",
|
||||
"ansicyan",
|
||||
"ansigreen",
|
||||
"ansimagenta",
|
||||
"ansired",
|
||||
"ansiyellow",
|
||||
"appendleft",
|
||||
"ASHA",
|
||||
"asyncio",
|
||||
"ATRAC",
|
||||
"avdtp",
|
||||
"bitpool",
|
||||
"bitstruct",
|
||||
"BSCP",
|
||||
"BTPROTO",
|
||||
"CCCD",
|
||||
"cccds",
|
||||
"cmac",
|
||||
"CONNECTIONLESS",
|
||||
"csrcs",
|
||||
"datagram",
|
||||
"DATALINK",
|
||||
"delayreport",
|
||||
"deregisters",
|
||||
"deregistration",
|
||||
"dhkey",
|
||||
"diversifier",
|
||||
"Fitbit",
|
||||
"GATTLINK",
|
||||
"HANDSFREE",
|
||||
"keydown",
|
||||
"keyup",
|
||||
"levelname",
|
||||
"libc",
|
||||
"libusb",
|
||||
"MITM",
|
||||
"NDIS",
|
||||
"NONBLOCK",
|
||||
"NONCONN",
|
||||
"OXIMETER",
|
||||
"popleft",
|
||||
"psms",
|
||||
"pyee",
|
||||
"pyusb",
|
||||
"rfcomm",
|
||||
"ROHC",
|
||||
"rssi",
|
||||
"SEID",
|
||||
"seids",
|
||||
"SERV",
|
||||
"ssrc",
|
||||
"strerror",
|
||||
"subband",
|
||||
"subbands",
|
||||
"subevent",
|
||||
"Subrating",
|
||||
"substates",
|
||||
"tobytes",
|
||||
"tsep",
|
||||
"usbmodem",
|
||||
"vhci",
|
||||
"websockets",
|
||||
"xcursor",
|
||||
"ycursor"
|
||||
],
|
||||
"[python]": {
|
||||
"editor.rulers": [88]
|
||||
},
|
||||
"python.formatting.provider": "black",
|
||||
"pylint.importStrategy": "useBundled",
|
||||
"python.testing.pytestArgs": [
|
||||
"."
|
||||
],
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.pytestEnabled": true
|
||||
}
|
||||
21
LICENSE
21
LICENSE
@@ -199,4 +199,23 @@
|
||||
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.
|
||||
limitations under the License.
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
Files: bumble/colors.py
|
||||
Copyright (c) 2012 Giorgos Verigakis <verigak@gmail.com>
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
@@ -15,10 +15,10 @@ Bumble is a full-featured Bluetooth stack written entirely in Python. It support
|
||||
|
||||
## Documentation
|
||||
|
||||
Browse the pre-built [Online Documentation](https://google.github.io/bumble/),
|
||||
Browse the pre-built [Online Documentation](https://google.github.io/bumble/),
|
||||
or see the documentation source under `docs/mkdocs/src`, or build the static HTML site from the markdown text with:
|
||||
```
|
||||
mkdocs build -f docs/mkdocs/mkdocs.yml
|
||||
mkdocs build -f docs/mkdocs/mkdocs.yml
|
||||
```
|
||||
|
||||
## Usage
|
||||
@@ -29,7 +29,7 @@ For a quick start to using Bumble, see the [Getting Started](docs/mkdocs/src/get
|
||||
|
||||
### Dependencies
|
||||
|
||||
To install package dependencies needed to run the bumble examples execute the following commands:
|
||||
To install package dependencies needed to run the bumble examples, execute the following commands:
|
||||
|
||||
```
|
||||
python -m pip install --upgrade pip
|
||||
@@ -50,7 +50,7 @@ Bumble is easiest to use with a dedicated USB dongle.
|
||||
This is because internal Bluetooth interfaces tend to be locked down by the operating system.
|
||||
You can use the [usb_probe](/docs/mkdocs/src/apps_and_tools/usb_probe.md) tool (all platforms) or `lsusb` (Linux or macOS) to list the available USB devices on your system.
|
||||
|
||||
See the [USB Transport](/docs/mkdocs/src/transports/usb.md) page for details on how to refer to USB devices.
|
||||
See the [USB Transport](/docs/mkdocs/src/transports/usb.md) page for details on how to refer to USB devices. Also, if your are on a mac, see [these instructions](docs/mkdocs/src/platforms/macos.md).
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -47,5 +47,3 @@ NOTE: this assumes you're running a Link Relay on port `10723`.
|
||||
|
||||
## `console.py`
|
||||
A simple text-based-ui interactive Bluetooth device with GATT client capabilities.
|
||||
|
||||
|
||||
|
||||
1209
apps/bench.py
Normal file
1209
apps/bench.py
Normal file
File diff suppressed because it is too large
Load Diff
913
apps/console.py
913
apps/console.py
File diff suppressed because it is too large
Load Diff
@@ -19,44 +19,103 @@ import asyncio
|
||||
import os
|
||||
import logging
|
||||
import click
|
||||
from colors import color
|
||||
from bumble.company_ids import COMPANY_IDENTIFIERS
|
||||
|
||||
from bumble.colors import color
|
||||
from bumble.core import name_or_number
|
||||
from bumble.hci import (
|
||||
map_null_terminated_utf8_string,
|
||||
HCI_LE_SUPPORTED_FEATURES_NAMES,
|
||||
HCI_SUCCESS,
|
||||
HCI_LE_SUPPORTED_FEATURES_NAMES,
|
||||
HCI_VERSION_NAMES,
|
||||
LMP_VERSION_NAMES,
|
||||
HCI_Command,
|
||||
HCI_Read_BD_ADDR_Command,
|
||||
HCI_Command_Complete_Event,
|
||||
HCI_Command_Status_Event,
|
||||
HCI_READ_BD_ADDR_COMMAND,
|
||||
HCI_Read_BD_ADDR_Command,
|
||||
HCI_READ_LOCAL_NAME_COMMAND,
|
||||
HCI_Read_Local_Name_Command,
|
||||
HCI_READ_LOCAL_NAME_COMMAND
|
||||
HCI_LE_READ_MAXIMUM_DATA_LENGTH_COMMAND,
|
||||
HCI_LE_Read_Maximum_Data_Length_Command,
|
||||
HCI_LE_READ_NUMBER_OF_SUPPORTED_ADVERTISING_SETS_COMMAND,
|
||||
HCI_LE_Read_Number_Of_Supported_Advertising_Sets_Command,
|
||||
HCI_LE_READ_MAXIMUM_ADVERTISING_DATA_LENGTH_COMMAND,
|
||||
HCI_LE_Read_Maximum_Advertising_Data_Length_Command,
|
||||
)
|
||||
from bumble.host import Host
|
||||
from bumble.transport import open_transport_or_link
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def command_succeeded(response):
|
||||
if isinstance(response, HCI_Command_Status_Event):
|
||||
return response.status == HCI_SUCCESS
|
||||
if isinstance(response, HCI_Command_Complete_Event):
|
||||
return response.return_parameters.status == HCI_SUCCESS
|
||||
return False
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def get_classic_info(host):
|
||||
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:
|
||||
if command_succeeded(response):
|
||||
print()
|
||||
print(color('Classic Address:', 'yellow'), response.return_parameters.bd_addr)
|
||||
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:
|
||||
if command_succeeded(response):
|
||||
print()
|
||||
print(color('Local Name:', 'yellow'), map_null_terminated_utf8_string(response.return_parameters.local_name))
|
||||
print(
|
||||
color('Local Name:', 'yellow'),
|
||||
map_null_terminated_utf8_string(response.return_parameters.local_name),
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def get_le_info(host):
|
||||
print()
|
||||
|
||||
if host.supports_command(HCI_LE_READ_NUMBER_OF_SUPPORTED_ADVERTISING_SETS_COMMAND):
|
||||
response = await host.send_command(
|
||||
HCI_LE_Read_Number_Of_Supported_Advertising_Sets_Command()
|
||||
)
|
||||
if command_succeeded(response):
|
||||
print(
|
||||
color('LE Number Of Supported Advertising Sets:', 'yellow'),
|
||||
response.return_parameters.num_supported_advertising_sets,
|
||||
'\n',
|
||||
)
|
||||
|
||||
if host.supports_command(HCI_LE_READ_MAXIMUM_ADVERTISING_DATA_LENGTH_COMMAND):
|
||||
response = await host.send_command(
|
||||
HCI_LE_Read_Maximum_Advertising_Data_Length_Command()
|
||||
)
|
||||
if command_succeeded(response):
|
||||
print(
|
||||
color('LE Maximum Advertising Data Length:', 'yellow'),
|
||||
response.return_parameters.max_advertising_data_length,
|
||||
'\n',
|
||||
)
|
||||
|
||||
if host.supports_command(HCI_LE_READ_MAXIMUM_DATA_LENGTH_COMMAND):
|
||||
response = await host.send_command(HCI_LE_Read_Maximum_Data_Length_Command())
|
||||
if command_succeeded(response):
|
||||
print(
|
||||
color('Maximum Data Length:', 'yellow'),
|
||||
(
|
||||
f'tx:{response.return_parameters.supported_max_tx_octets}/'
|
||||
f'{response.return_parameters.supported_max_tx_time}, '
|
||||
f'rx:{response.return_parameters.supported_max_rx_octets}/'
|
||||
f'{response.return_parameters.supported_max_rx_time}'
|
||||
),
|
||||
'\n',
|
||||
)
|
||||
|
||||
print(color('LE Features:', 'yellow'))
|
||||
for feature in host.supported_le_features:
|
||||
print(' ', name_or_number(HCI_LE_SUPPORTED_FEATURES_NAMES, feature))
|
||||
@@ -73,10 +132,19 @@ async def async_main(transport):
|
||||
|
||||
# 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(' 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 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
|
||||
@@ -96,7 +164,7 @@ async def async_main(transport):
|
||||
@click.command()
|
||||
@click.argument('transport')
|
||||
def main(transport):
|
||||
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
|
||||
asyncio.run(async_main(transport))
|
||||
|
||||
|
||||
|
||||
@@ -28,11 +28,14 @@ from bumble.transport import open_transport_or_link
|
||||
# -----------------------------------------------------------------------------
|
||||
async def async_main():
|
||||
if len(sys.argv) != 3:
|
||||
print('Usage: controllers.py <hci-transport-1> <hci-transport-2> [<hci-transport-3> ...]')
|
||||
print(
|
||||
'Usage: controllers.py <hci-transport-1> <hci-transport-2> '
|
||||
'[<hci-transport-3> ...]'
|
||||
)
|
||||
print('example: python controllers.py pty:ble1 pty:ble2')
|
||||
return
|
||||
|
||||
# Create a loccal link to attach the controllers to
|
||||
# Create a local link to attach the controllers to
|
||||
link = LocalLink()
|
||||
|
||||
# Create a transport and controller for all requested names
|
||||
@@ -41,7 +44,12 @@ async def async_main():
|
||||
for index, transport_name in enumerate(sys.argv[1:]):
|
||||
transport = await open_transport_or_link(transport_name)
|
||||
transports.append(transport)
|
||||
controller = Controller(f'C{index}', host_source = transport.source, host_sink = transport.sink, link = link)
|
||||
controller = Controller(
|
||||
f'C{index}',
|
||||
host_source=transport.source,
|
||||
host_sink=transport.sink,
|
||||
link=link,
|
||||
)
|
||||
controllers.append(controller)
|
||||
|
||||
# Wait until the user interrupts
|
||||
@@ -54,7 +62,7 @@ async def async_main():
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def main():
|
||||
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||
asyncio.run(async_main())
|
||||
|
||||
|
||||
|
||||
@@ -19,9 +19,9 @@ import asyncio
|
||||
import os
|
||||
import logging
|
||||
import click
|
||||
from colors import color
|
||||
|
||||
from bumble.core import ProtocolError, TimeoutError
|
||||
import bumble.core
|
||||
from bumble.colors import color
|
||||
from bumble.device import Device, Peer
|
||||
from bumble.gatt import show_services
|
||||
from bumble.transport import open_transport_or_link
|
||||
@@ -49,9 +49,9 @@ async def dump_gatt_db(peer, done):
|
||||
try:
|
||||
value = await attribute.read_value()
|
||||
print(color(f'{value.hex()}', 'green'))
|
||||
except ProtocolError as error:
|
||||
except bumble.core.ProtocolError as error:
|
||||
print(color(error, 'red'))
|
||||
except TimeoutError:
|
||||
except bumble.core.TimeoutError:
|
||||
print(color('read timeout', 'red'))
|
||||
|
||||
if done is not None:
|
||||
@@ -64,9 +64,13 @@ async def async_main(device_config, encrypt, transport, address_or_name):
|
||||
|
||||
# Create a device
|
||||
if device_config:
|
||||
device = Device.from_config_file_with_hci(device_config, hci_source, hci_sink)
|
||||
device = Device.from_config_file_with_hci(
|
||||
device_config, hci_source, hci_sink
|
||||
)
|
||||
else:
|
||||
device = Device.with_hci('Bumble', 'F0:F1:F2:F3:F4:F5', hci_source, hci_sink)
|
||||
device = Device.with_hci(
|
||||
'Bumble', 'F0:F1:F2:F3:F4:F5', hci_source, hci_sink
|
||||
)
|
||||
await device.power_on()
|
||||
|
||||
if address_or_name:
|
||||
@@ -81,7 +85,12 @@ async def async_main(device_config, encrypt, transport, address_or_name):
|
||||
else:
|
||||
# Wait for a connection
|
||||
done = asyncio.get_running_loop().create_future()
|
||||
device.on('connection', lambda connection: asyncio.create_task(dump_gatt_db(Peer(connection), done)))
|
||||
device.on(
|
||||
'connection',
|
||||
lambda connection: asyncio.create_task(
|
||||
dump_gatt_db(Peer(connection), done)
|
||||
),
|
||||
)
|
||||
await device.start_advertising(auto_restart=True)
|
||||
|
||||
print(color('### Waiting for connection...', 'blue'))
|
||||
@@ -99,7 +108,7 @@ def main(device_config, encrypt, transport, address_or_name):
|
||||
Dump the GATT database on a remote device. If ADDRESS_OR_NAME is not specified,
|
||||
wait for an incoming connection.
|
||||
"""
|
||||
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||
asyncio.run(async_main(device_config, encrypt, transport, address_or_name))
|
||||
|
||||
|
||||
|
||||
@@ -17,13 +17,14 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import os
|
||||
import struct
|
||||
import logging
|
||||
import click
|
||||
from colors import color
|
||||
|
||||
from bumble.colors import color
|
||||
from bumble.device import Device, Peer
|
||||
from bumble.core import AdvertisingData
|
||||
from bumble.gatt import Service, Characteristic
|
||||
from bumble.gatt import Service, Characteristic, CharacteristicValue
|
||||
from bumble.utils import AsyncRunner
|
||||
from bumble.transport import open_transport_or_link
|
||||
from bumble.hci import HCI_Constant
|
||||
@@ -32,24 +33,73 @@ from bumble.hci import HCI_Constant
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
GG_GATTLINK_SERVICE_UUID = 'ABBAFF00-E56A-484C-B832-8B17CF6CBFE8'
|
||||
GG_GATTLINK_RX_CHARACTERISTIC_UUID = 'ABBAFF01-E56A-484C-B832-8B17CF6CBFE8'
|
||||
GG_GATTLINK_TX_CHARACTERISTIC_UUID = 'ABBAFF02-E56A-484C-B832-8B17CF6CBFE8'
|
||||
GG_GATTLINK_L2CAP_CHANNEL_PSM_CHARACTERISTIC_UUID = 'ABBAFF03-E56A-484C-B832-8B17CF6CBFE8'
|
||||
GG_GATTLINK_SERVICE_UUID = 'ABBAFF00-E56A-484C-B832-8B17CF6CBFE8'
|
||||
GG_GATTLINK_RX_CHARACTERISTIC_UUID = 'ABBAFF01-E56A-484C-B832-8B17CF6CBFE8'
|
||||
GG_GATTLINK_TX_CHARACTERISTIC_UUID = 'ABBAFF02-E56A-484C-B832-8B17CF6CBFE8'
|
||||
GG_GATTLINK_L2CAP_CHANNEL_PSM_CHARACTERISTIC_UUID = (
|
||||
'ABBAFF03-E56A-484C-B832-8B17CF6CBFE8'
|
||||
)
|
||||
|
||||
GG_PREFERRED_MTU = 256
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class GattlinkHubBridge(Device.Listener):
|
||||
class GattlinkL2capEndpoint:
|
||||
def __init__(self):
|
||||
self.peer = None
|
||||
self.rx_socket = None
|
||||
self.tx_socket = None
|
||||
self.l2cap_channel = None
|
||||
self.l2cap_packet = b''
|
||||
self.l2cap_packet_size = 0
|
||||
|
||||
# Called when an L2CAP SDU has been received
|
||||
def on_coc_sdu(self, sdu):
|
||||
print(color(f'<<< [L2CAP SDU]: {len(sdu)} bytes', 'cyan'))
|
||||
while len(sdu):
|
||||
if self.l2cap_packet_size == 0:
|
||||
# Expect a new packet
|
||||
self.l2cap_packet_size = sdu[0] + 1
|
||||
sdu = sdu[1:]
|
||||
else:
|
||||
bytes_needed = self.l2cap_packet_size - len(self.l2cap_packet)
|
||||
chunk = min(bytes_needed, len(sdu))
|
||||
self.l2cap_packet += sdu[:chunk]
|
||||
sdu = sdu[chunk:]
|
||||
if len(self.l2cap_packet) == self.l2cap_packet_size:
|
||||
self.on_l2cap_packet(self.l2cap_packet)
|
||||
self.l2cap_packet = b''
|
||||
self.l2cap_packet_size = 0
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class GattlinkHubBridge(GattlinkL2capEndpoint, Device.Listener):
|
||||
def __init__(self, device, peer_address):
|
||||
super().__init__()
|
||||
self.device = device
|
||||
self.peer_address = peer_address
|
||||
self.peer = None
|
||||
self.tx_socket = None
|
||||
self.rx_characteristic = None
|
||||
self.tx_characteristic = None
|
||||
self.l2cap_psm_characteristic = None
|
||||
|
||||
device.listener = self
|
||||
|
||||
async def start(self):
|
||||
# Connect to the peer
|
||||
print(f'=== Connecting to {self.peer_address}...')
|
||||
await self.device.connect(self.peer_address)
|
||||
|
||||
async def connect_l2cap(self, psm):
|
||||
print(color(f'### Connecting with L2CAP on PSM = {psm}', 'yellow'))
|
||||
try:
|
||||
self.l2cap_channel = await self.peer.connection.open_l2cap_channel(psm)
|
||||
print(color('*** Connected', 'yellow'), self.l2cap_channel)
|
||||
self.l2cap_channel.sink = self.on_coc_sdu
|
||||
|
||||
except Exception as error:
|
||||
print(color(f'!!! Connection failed: {error}', 'red'))
|
||||
|
||||
@AsyncRunner.run_in_task()
|
||||
# pylint: disable=invalid-overridden-method
|
||||
async def on_connection(self, connection):
|
||||
print(f'=== Connected to {connection}')
|
||||
self.peer = Peer(connection)
|
||||
@@ -80,115 +130,221 @@ class GattlinkHubBridge(Device.Listener):
|
||||
self.rx_characteristic = characteristic
|
||||
elif characteristic.uuid == GG_GATTLINK_TX_CHARACTERISTIC_UUID:
|
||||
self.tx_characteristic = characteristic
|
||||
elif (
|
||||
characteristic.uuid == GG_GATTLINK_L2CAP_CHANNEL_PSM_CHARACTERISTIC_UUID
|
||||
):
|
||||
self.l2cap_psm_characteristic = characteristic
|
||||
print('RX:', self.rx_characteristic)
|
||||
print('TX:', self.tx_characteristic)
|
||||
print('PSM:', self.l2cap_psm_characteristic)
|
||||
|
||||
# Subscribe to TX
|
||||
if self.tx_characteristic:
|
||||
if self.l2cap_psm_characteristic:
|
||||
# Subscribe to and then read the PSM value
|
||||
await self.peer.subscribe(
|
||||
self.l2cap_psm_characteristic, self.on_l2cap_psm_received
|
||||
)
|
||||
psm_bytes = await self.peer.read_value(self.l2cap_psm_characteristic)
|
||||
psm = struct.unpack('<H', psm_bytes)[0]
|
||||
await self.connect_l2cap(psm)
|
||||
elif self.tx_characteristic:
|
||||
# Subscribe to TX
|
||||
await self.peer.subscribe(self.tx_characteristic, self.on_tx_received)
|
||||
print(color('=== Subscribed to Gattlink TX', 'yellow'))
|
||||
else:
|
||||
print(color('!!! Gattlink TX not found', 'red'))
|
||||
print(color('!!! No Gattlink TX or PSM found', 'red'))
|
||||
|
||||
def on_connection_failure(self, error):
|
||||
print(color(f'!!! Connection failed: {error}'))
|
||||
|
||||
def on_disconnection(self, reason):
|
||||
print(color(f'!!! Disconnected from {self.peer}, reason={HCI_Constant.error_name(reason)}', 'red'))
|
||||
print(
|
||||
color(
|
||||
f'!!! Disconnected from {self.peer}, '
|
||||
f'reason={HCI_Constant.error_name(reason)}',
|
||||
'red',
|
||||
)
|
||||
)
|
||||
self.tx_characteristic = None
|
||||
self.rx_characteristic = None
|
||||
self.peer = None
|
||||
|
||||
# Called when an L2CAP packet has been received
|
||||
def on_l2cap_packet(self, packet):
|
||||
print(color(f'<<< [L2CAP PACKET]: {len(packet)} bytes', 'cyan'))
|
||||
print(color('>>> [UDP]', 'magenta'))
|
||||
self.tx_socket.sendto(packet)
|
||||
|
||||
# Called by the GATT client when a notification is received
|
||||
def on_tx_received(self, value):
|
||||
print(color('>>> TX:', 'magenta'), value.hex())
|
||||
print(color(f'<<< [GATT TX]: {len(value)} bytes', 'cyan'))
|
||||
if self.tx_socket:
|
||||
print(color('>>> [UDP]', 'magenta'))
|
||||
self.tx_socket.sendto(value)
|
||||
|
||||
# Called by asyncio when the UDP socket is created
|
||||
def connection_made(self, transport):
|
||||
pass
|
||||
|
||||
# Called by asyncio when a UDP datagram is received
|
||||
def datagram_received(self, data, address):
|
||||
print(color('<<< RX:', 'magenta'), data.hex())
|
||||
|
||||
# TODO: use a queue instead of creating a task everytime
|
||||
if self.peer and self.rx_characteristic:
|
||||
asyncio.create_task(self.peer.write_value(self.rx_characteristic, data))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class GattlinkNodeBridge(Device.Listener):
|
||||
def __init__(self):
|
||||
self.peer = None
|
||||
self.rx_socket = None
|
||||
self.tx_socket = None
|
||||
def on_l2cap_psm_received(self, value):
|
||||
psm = struct.unpack('<H', value)[0]
|
||||
asyncio.create_task(self.connect_l2cap(psm))
|
||||
|
||||
# Called by asyncio when the UDP socket is created
|
||||
def connection_made(self, transport):
|
||||
pass
|
||||
|
||||
# Called by asyncio when a UDP datagram is received
|
||||
def datagram_received(self, data, address):
|
||||
print(color('<<< RX:', 'magenta'), data.hex())
|
||||
def datagram_received(self, data, _address):
|
||||
print(color(f'<<< [UDP]: {len(data)} bytes', 'green'))
|
||||
|
||||
# TODO: use a queue instead of creating a task everytime
|
||||
if self.peer and self.rx_characteristic:
|
||||
if self.l2cap_channel:
|
||||
print(color('>>> [L2CAP]', 'yellow'))
|
||||
self.l2cap_channel.write(bytes([len(data) - 1]) + data)
|
||||
elif self.peer and self.rx_characteristic:
|
||||
print(color('>>> [GATT RX]', 'yellow'))
|
||||
asyncio.create_task(self.peer.write_value(self.rx_characteristic, data))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def run(hci_transport, device_address, send_host, send_port, receive_host, receive_port):
|
||||
class GattlinkNodeBridge(GattlinkL2capEndpoint, Device.Listener):
|
||||
def __init__(self, device):
|
||||
super().__init__()
|
||||
self.device = device
|
||||
self.peer = None
|
||||
self.tx_socket = None
|
||||
self.tx_subscriber = None
|
||||
self.rx_characteristic = None
|
||||
self.transport = None
|
||||
|
||||
# Register as a listener
|
||||
device.listener = self
|
||||
|
||||
# Listen for incoming L2CAP CoC connections
|
||||
psm = 0xFB
|
||||
device.register_l2cap_channel_server(0xFB, self.on_coc)
|
||||
print(f'### Listening for CoC connection on PSM {psm}')
|
||||
|
||||
# Setup the Gattlink service
|
||||
self.rx_characteristic = Characteristic(
|
||||
GG_GATTLINK_RX_CHARACTERISTIC_UUID,
|
||||
Characteristic.WRITE_WITHOUT_RESPONSE,
|
||||
Characteristic.WRITEABLE,
|
||||
CharacteristicValue(write=self.on_rx_write),
|
||||
)
|
||||
self.tx_characteristic = Characteristic(
|
||||
GG_GATTLINK_TX_CHARACTERISTIC_UUID,
|
||||
Characteristic.Properties.NOTIFY,
|
||||
Characteristic.READABLE,
|
||||
)
|
||||
self.tx_characteristic.on('subscription', self.on_tx_subscription)
|
||||
self.psm_characteristic = Characteristic(
|
||||
GG_GATTLINK_L2CAP_CHANNEL_PSM_CHARACTERISTIC_UUID,
|
||||
Characteristic.Properties.READ | Characteristic.Properties.NOTIFY,
|
||||
Characteristic.READABLE,
|
||||
bytes([psm, 0]),
|
||||
)
|
||||
gattlink_service = Service(
|
||||
GG_GATTLINK_SERVICE_UUID,
|
||||
[self.rx_characteristic, self.tx_characteristic, self.psm_characteristic],
|
||||
)
|
||||
device.add_services([gattlink_service])
|
||||
device.advertising_data = bytes(
|
||||
AdvertisingData(
|
||||
[
|
||||
(AdvertisingData.COMPLETE_LOCAL_NAME, bytes('Bumble GG', 'utf-8')),
|
||||
(
|
||||
AdvertisingData.INCOMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS,
|
||||
bytes(
|
||||
reversed(bytes.fromhex('ABBAFF00E56A484CB8328B17CF6CBFE8'))
|
||||
),
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
async def start(self):
|
||||
await self.device.start_advertising()
|
||||
|
||||
# Called by asyncio when the UDP socket is created
|
||||
def connection_made(self, transport):
|
||||
self.transport = transport
|
||||
|
||||
# Called by asyncio when a UDP datagram is received
|
||||
def datagram_received(self, data, _address):
|
||||
print(color(f'<<< [UDP]: {len(data)} bytes', 'green'))
|
||||
|
||||
if self.l2cap_channel:
|
||||
print(color('>>> [L2CAP]', 'yellow'))
|
||||
self.l2cap_channel.write(bytes([len(data) - 1]) + data)
|
||||
elif self.tx_subscriber:
|
||||
print(color('>>> [GATT TX]', 'yellow'))
|
||||
self.tx_characteristic.value = data
|
||||
asyncio.create_task(self.device.notify_subscribers(self.tx_characteristic))
|
||||
|
||||
# Called when a write to the RX characteristic has been received
|
||||
def on_rx_write(self, _connection, data):
|
||||
print(color(f'<<< [GATT RX]: {len(data)} bytes', 'cyan'))
|
||||
print(color('>>> [UDP]', 'magenta'))
|
||||
self.tx_socket.sendto(data)
|
||||
|
||||
# Called when the subscription to the TX characteristic has changed
|
||||
def on_tx_subscription(self, peer, enabled):
|
||||
print(
|
||||
f'### [GATT TX] subscription from {peer}: '
|
||||
f'{"enabled" if enabled else "disabled"}'
|
||||
)
|
||||
if enabled:
|
||||
self.tx_subscriber = peer
|
||||
else:
|
||||
self.tx_subscriber = None
|
||||
|
||||
# Called when an L2CAP packet is received
|
||||
def on_l2cap_packet(self, packet):
|
||||
print(color(f'<<< [L2CAP PACKET]: {len(packet)} bytes', 'cyan'))
|
||||
print(color('>>> [UDP]', 'magenta'))
|
||||
self.tx_socket.sendto(packet)
|
||||
|
||||
# Called when a new connection is established
|
||||
def on_coc(self, channel):
|
||||
print('*** CoC Connection', channel)
|
||||
self.l2cap_channel = channel
|
||||
channel.sink = self.on_coc_sdu
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def run(
|
||||
hci_transport,
|
||||
device_address,
|
||||
role_or_peer_address,
|
||||
send_host,
|
||||
send_port,
|
||||
receive_host,
|
||||
receive_port,
|
||||
):
|
||||
print('<<< connecting to HCI...')
|
||||
async with await open_transport_or_link(hci_transport) as (hci_source, hci_sink):
|
||||
print('<<< connected')
|
||||
|
||||
# Instantiate a bridge object
|
||||
bridge = GattlinkNodeBridge()
|
||||
device = Device.with_hci('Bumble GG', device_address, hci_source, hci_sink)
|
||||
|
||||
# Instantiate a bridge object
|
||||
if role_or_peer_address == 'node':
|
||||
bridge = GattlinkNodeBridge(device)
|
||||
else:
|
||||
bridge = GattlinkHubBridge(device, role_or_peer_address)
|
||||
|
||||
# Create a UDP to RX bridge (receive from UDP, send to RX)
|
||||
loop = asyncio.get_running_loop()
|
||||
await loop.create_datagram_endpoint(
|
||||
lambda: bridge,
|
||||
local_addr=(receive_host, receive_port)
|
||||
lambda: bridge, local_addr=(receive_host, receive_port)
|
||||
)
|
||||
|
||||
# Create a UDP to TX bridge (receive from TX, send to UDP)
|
||||
bridge.tx_socket, _ = await loop.create_datagram_endpoint(
|
||||
lambda: asyncio.DatagramProtocol(),
|
||||
remote_addr=(send_host, send_port)
|
||||
asyncio.DatagramProtocol,
|
||||
remote_addr=(send_host, send_port),
|
||||
)
|
||||
|
||||
# Create a device to manage the host, with a custom listener
|
||||
device = Device.with_hci('Bumble', 'F0:F1:F2:F3:F4:F5', hci_source, hci_sink)
|
||||
device.listener = bridge
|
||||
await device.power_on()
|
||||
|
||||
# Connect to the peer
|
||||
# print(f'=== Connecting to {device_address}...')
|
||||
# await device.connect(device_address)
|
||||
|
||||
# TODO move to class
|
||||
gattlink_service = Service(
|
||||
GG_GATTLINK_SERVICE_UUID,
|
||||
[
|
||||
Characteristic(
|
||||
GG_GATTLINK_L2CAP_CHANNEL_PSM_CHARACTERISTIC_UUID,
|
||||
Characteristic.READ,
|
||||
Characteristic.READABLE,
|
||||
bytes([193, 0])
|
||||
)
|
||||
]
|
||||
)
|
||||
device.add_services([gattlink_service])
|
||||
device.advertising_data = bytes(
|
||||
AdvertisingData([
|
||||
(AdvertisingData.COMPLETE_LOCAL_NAME, bytes('Bumble GG', 'utf-8')),
|
||||
(AdvertisingData.INCOMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS, bytes(reversed(bytes.fromhex('ABBAFF00E56A484CB8328B17CF6CBFE8'))))
|
||||
])
|
||||
)
|
||||
await device.start_advertising()
|
||||
await bridge.start()
|
||||
|
||||
# Wait until the source terminates
|
||||
await hci_source.wait_for_termination()
|
||||
@@ -197,15 +353,44 @@ async def run(hci_transport, device_address, send_host, send_port, receive_host,
|
||||
@click.command()
|
||||
@click.argument('hci_transport')
|
||||
@click.argument('device_address')
|
||||
@click.option('-sh', '--send-host', type=str, default='127.0.0.1', help='UDP host to send to')
|
||||
@click.argument('role_or_peer_address')
|
||||
@click.option(
|
||||
'-sh', '--send-host', type=str, default='127.0.0.1', help='UDP host to send to'
|
||||
)
|
||||
@click.option('-sp', '--send-port', type=int, default=9001, help='UDP port to send to')
|
||||
@click.option('-rh', '--receive-host', type=str, default='127.0.0.1', help='UDP host to receive on')
|
||||
@click.option('-rp', '--receive-port', type=int, default=9000, help='UDP port to receive on')
|
||||
def main(hci_transport, device_address, send_host, send_port, receive_host, receive_port):
|
||||
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||
asyncio.run(run(hci_transport, device_address, send_host, send_port, receive_host, receive_port))
|
||||
@click.option(
|
||||
'-rh',
|
||||
'--receive-host',
|
||||
type=str,
|
||||
default='127.0.0.1',
|
||||
help='UDP host to receive on',
|
||||
)
|
||||
@click.option(
|
||||
'-rp', '--receive-port', type=int, default=9000, help='UDP port to receive on'
|
||||
)
|
||||
def main(
|
||||
hci_transport,
|
||||
device_address,
|
||||
role_or_peer_address,
|
||||
send_host,
|
||||
send_port,
|
||||
receive_host,
|
||||
receive_port,
|
||||
):
|
||||
asyncio.run(
|
||||
run(
|
||||
hci_transport,
|
||||
device_address,
|
||||
role_or_peer_address,
|
||||
send_host,
|
||||
send_port,
|
||||
receive_host,
|
||||
receive_port,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
@@ -34,16 +34,29 @@ logger = logging.getLogger(__name__)
|
||||
# -----------------------------------------------------------------------------
|
||||
async def async_main():
|
||||
if len(sys.argv) < 3:
|
||||
print('Usage: hci_bridge.py <host-transport-spec> <controller-transport-spec> [command-short-circuit-list]')
|
||||
print('example: python hci_bridge.py udp:0.0.0.0:9000,127.0.0.1:9001 serial:/dev/tty.usbmodem0006839912171,1000000 0x3f:0x0070,0x3f:0x0074,0x3f:0x0077,0x3f:0x0078')
|
||||
print(
|
||||
'Usage: hci_bridge.py <host-transport-spec> <controller-transport-spec> '
|
||||
'[command-short-circuit-list]'
|
||||
)
|
||||
print(
|
||||
'example: python hci_bridge.py udp:0.0.0.0:9000,127.0.0.1:9001 '
|
||||
'serial:/dev/tty.usbmodem0006839912171,1000000 '
|
||||
'0x3f:0x0070,0x3f:0x0074,0x3f:0x0077,0x3f:0x0078'
|
||||
)
|
||||
return
|
||||
|
||||
print('>>> connecting to HCI...')
|
||||
async with await transport.open_transport_or_link(sys.argv[1]) as (hci_host_source, hci_host_sink):
|
||||
async with await transport.open_transport_or_link(sys.argv[1]) as (
|
||||
hci_host_source,
|
||||
hci_host_sink,
|
||||
):
|
||||
print('>>> connected')
|
||||
|
||||
print('>>> connecting to HCI...')
|
||||
async with await transport.open_transport_or_link(sys.argv[2]) as (hci_controller_source, hci_controller_sink):
|
||||
async with await transport.open_transport_or_link(sys.argv[2]) as (
|
||||
hci_controller_source,
|
||||
hci_controller_sink,
|
||||
):
|
||||
print('>>> connected')
|
||||
|
||||
command_short_circuits = []
|
||||
@@ -51,36 +64,43 @@ async def async_main():
|
||||
for op_code_str in sys.argv[3].split(','):
|
||||
if ':' in op_code_str:
|
||||
ogf, ocf = op_code_str.split(':')
|
||||
command_short_circuits.append(hci.hci_command_op_code(int(ogf, 16), int(ocf, 16)))
|
||||
command_short_circuits.append(
|
||||
hci.hci_command_op_code(int(ogf, 16), int(ocf, 16))
|
||||
)
|
||||
else:
|
||||
command_short_circuits.append(int(op_code_str, 16))
|
||||
|
||||
def host_to_controller_filter(hci_packet):
|
||||
if hci_packet.hci_packet_type == hci.HCI_COMMAND_PACKET and hci_packet.op_code in command_short_circuits:
|
||||
if (
|
||||
hci_packet.hci_packet_type == hci.HCI_COMMAND_PACKET
|
||||
and hci_packet.op_code in command_short_circuits
|
||||
):
|
||||
# Respond with a success response
|
||||
logger.debug('short-circuiting packet')
|
||||
response = hci.HCI_Command_Complete_Event(
|
||||
num_hci_command_packets = 1,
|
||||
command_opcode = hci_packet.op_code,
|
||||
return_parameters = bytes([hci.HCI_SUCCESS])
|
||||
num_hci_command_packets=1,
|
||||
command_opcode=hci_packet.op_code,
|
||||
return_parameters=bytes([hci.HCI_SUCCESS]),
|
||||
)
|
||||
# Return a packet with 'respond to sender' set to True
|
||||
return (response.to_bytes(), True)
|
||||
|
||||
return None
|
||||
|
||||
_ = HCI_Bridge(
|
||||
hci_host_source,
|
||||
hci_host_sink,
|
||||
hci_controller_source,
|
||||
hci_controller_sink,
|
||||
host_to_controller_filter,
|
||||
None
|
||||
None,
|
||||
)
|
||||
await asyncio.get_running_loop().create_future()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def main():
|
||||
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||
asyncio.run(async_main())
|
||||
|
||||
|
||||
|
||||
350
apps/l2cap_bridge.py
Normal file
350
apps/l2cap_bridge.py
Normal file
@@ -0,0 +1,350 @@
|
||||
# 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 logging
|
||||
import os
|
||||
import click
|
||||
|
||||
from bumble.colors import color
|
||||
from bumble.transport import open_transport_or_link
|
||||
from bumble.device import Device
|
||||
from bumble.utils import FlowControlAsyncPipe
|
||||
from bumble.hci import HCI_Constant
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class ServerBridge:
|
||||
"""
|
||||
L2CAP CoC server bridge: waits for a peer to connect an L2CAP CoC channel
|
||||
on a specified PSM. When the connection is made, the bridge connects a TCP
|
||||
socket to a remote host and bridges the data in both directions, with flow
|
||||
control.
|
||||
When the L2CAP CoC channel is closed, the bridge disconnects the TCP socket
|
||||
and waits for a new L2CAP CoC channel to be connected.
|
||||
When the TCP connection is closed by the TCP server, XXXX
|
||||
"""
|
||||
|
||||
def __init__(self, psm, max_credits, mtu, mps, tcp_host, tcp_port):
|
||||
self.psm = psm
|
||||
self.max_credits = max_credits
|
||||
self.mtu = mtu
|
||||
self.mps = mps
|
||||
self.tcp_host = tcp_host
|
||||
self.tcp_port = tcp_port
|
||||
|
||||
async def start(self, device):
|
||||
# Listen for incoming L2CAP CoC connections
|
||||
device.register_l2cap_channel_server(
|
||||
psm=self.psm,
|
||||
server=self.on_coc,
|
||||
max_credits=self.max_credits,
|
||||
mtu=self.mtu,
|
||||
mps=self.mps,
|
||||
)
|
||||
print(color(f'### Listening for CoC connection on PSM {self.psm}', 'yellow'))
|
||||
|
||||
def on_ble_connection(connection):
|
||||
def on_ble_disconnection(reason):
|
||||
print(
|
||||
color('@@@ Bluetooth disconnection:', 'red'),
|
||||
HCI_Constant.error_name(reason),
|
||||
)
|
||||
|
||||
print(color('@@@ Bluetooth connection:', 'green'), connection)
|
||||
connection.on('disconnection', on_ble_disconnection)
|
||||
|
||||
device.on('connection', on_ble_connection)
|
||||
|
||||
await device.start_advertising(auto_restart=True)
|
||||
|
||||
# Called when a new L2CAP connection is established
|
||||
def on_coc(self, l2cap_channel):
|
||||
print(color('*** L2CAP channel:', 'cyan'), l2cap_channel)
|
||||
|
||||
class Pipe:
|
||||
def __init__(self, bridge, l2cap_channel):
|
||||
self.bridge = bridge
|
||||
self.tcp_transport = None
|
||||
self.l2cap_channel = l2cap_channel
|
||||
|
||||
l2cap_channel.on('close', self.on_l2cap_close)
|
||||
l2cap_channel.sink = self.on_coc_sdu
|
||||
|
||||
async def connect_to_tcp(self):
|
||||
# Connect to the TCP server
|
||||
print(
|
||||
color(
|
||||
f'### Connecting to TCP {self.bridge.tcp_host}:'
|
||||
f'{self.bridge.tcp_port}...',
|
||||
'yellow',
|
||||
)
|
||||
)
|
||||
|
||||
class TcpClientProtocol(asyncio.Protocol):
|
||||
def __init__(self, pipe):
|
||||
self.pipe = pipe
|
||||
|
||||
def connection_lost(self, exc):
|
||||
print(color(f'!!! TCP connection lost: {exc}', 'red'))
|
||||
if self.pipe.l2cap_channel is not None:
|
||||
asyncio.create_task(self.pipe.l2cap_channel.disconnect())
|
||||
|
||||
def data_received(self, data):
|
||||
print(f'<<< Received on TCP: {len(data)}')
|
||||
self.pipe.l2cap_channel.write(data)
|
||||
|
||||
try:
|
||||
(
|
||||
self.tcp_transport,
|
||||
_,
|
||||
) = await asyncio.get_running_loop().create_connection(
|
||||
lambda: TcpClientProtocol(self),
|
||||
host=self.bridge.tcp_host,
|
||||
port=self.bridge.tcp_port,
|
||||
)
|
||||
print(color('### Connected', 'green'))
|
||||
except Exception as error:
|
||||
print(color(f'!!! Connection failed: {error}', 'red'))
|
||||
await self.l2cap_channel.disconnect()
|
||||
|
||||
def on_l2cap_close(self):
|
||||
self.l2cap_channel = None
|
||||
if self.tcp_transport is not None:
|
||||
self.tcp_transport.close()
|
||||
|
||||
def on_coc_sdu(self, sdu):
|
||||
print(color(f'<<< [L2CAP SDU]: {len(sdu)} bytes', 'cyan'))
|
||||
if self.tcp_transport is None:
|
||||
print(color('!!! TCP socket not open, dropping', 'red'))
|
||||
return
|
||||
self.tcp_transport.write(sdu)
|
||||
|
||||
pipe = Pipe(self, l2cap_channel)
|
||||
|
||||
asyncio.create_task(pipe.connect_to_tcp())
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class ClientBridge:
|
||||
"""
|
||||
L2CAP CoC client bridge: connects to a BLE device, then waits for an inbound
|
||||
TCP connection on a specified port number. When a TCP client connects, an
|
||||
L2CAP CoC channel connection to the BLE device is established, and the data
|
||||
is bridged in both directions, with flow control.
|
||||
When the TCP connection is closed by the client, the L2CAP CoC channel is
|
||||
disconnected, but the connection to the BLE device remains, ready for a new
|
||||
TCP client to connect.
|
||||
When the L2CAP CoC channel is closed, XXXX
|
||||
"""
|
||||
|
||||
READ_CHUNK_SIZE = 4096
|
||||
|
||||
def __init__(self, psm, max_credits, mtu, mps, address, tcp_host, tcp_port):
|
||||
self.psm = psm
|
||||
self.max_credits = max_credits
|
||||
self.mtu = mtu
|
||||
self.mps = mps
|
||||
self.address = address
|
||||
self.tcp_host = tcp_host
|
||||
self.tcp_port = tcp_port
|
||||
|
||||
async def start(self, device):
|
||||
print(color(f'### Connecting to {self.address}...', 'yellow'))
|
||||
connection = await device.connect(self.address)
|
||||
print(color('### Connected', 'green'))
|
||||
|
||||
# Called when the BLE connection is disconnected
|
||||
def on_ble_disconnection(reason):
|
||||
print(
|
||||
color('@@@ Bluetooth disconnection:', 'red'),
|
||||
HCI_Constant.error_name(reason),
|
||||
)
|
||||
|
||||
connection.on('disconnection', on_ble_disconnection)
|
||||
|
||||
# Called when a TCP connection is established
|
||||
async def on_tcp_connection(reader, writer):
|
||||
peer_name = writer.get_extra_info('peer_name')
|
||||
print(color(f'<<< TCP connection from {peer_name}', 'magenta'))
|
||||
|
||||
def on_coc_sdu(sdu):
|
||||
print(color(f'<<< [L2CAP SDU]: {len(sdu)} bytes', 'cyan'))
|
||||
l2cap_to_tcp_pipe.write(sdu)
|
||||
|
||||
def on_l2cap_close():
|
||||
print(color('*** L2CAP channel closed', 'red'))
|
||||
l2cap_to_tcp_pipe.stop()
|
||||
writer.close()
|
||||
|
||||
# Connect a new L2CAP channel
|
||||
print(color(f'>>> Opening L2CAP channel on PSM = {self.psm}', 'yellow'))
|
||||
try:
|
||||
l2cap_channel = await connection.open_l2cap_channel(
|
||||
psm=self.psm,
|
||||
max_credits=self.max_credits,
|
||||
mtu=self.mtu,
|
||||
mps=self.mps,
|
||||
)
|
||||
print(color('*** L2CAP channel:', 'cyan'), l2cap_channel)
|
||||
except Exception as error:
|
||||
print(color(f'!!! Connection failed: {error}', 'red'))
|
||||
writer.close()
|
||||
return
|
||||
|
||||
l2cap_channel.sink = on_coc_sdu
|
||||
l2cap_channel.on('close', on_l2cap_close)
|
||||
|
||||
# Start a flow control pipe from L2CAP to TCP
|
||||
l2cap_to_tcp_pipe = FlowControlAsyncPipe(
|
||||
l2cap_channel.pause_reading,
|
||||
l2cap_channel.resume_reading,
|
||||
writer.write,
|
||||
writer.drain,
|
||||
)
|
||||
l2cap_to_tcp_pipe.start()
|
||||
|
||||
# Pipe data from TCP to L2CAP
|
||||
while True:
|
||||
try:
|
||||
data = await reader.read(self.READ_CHUNK_SIZE)
|
||||
|
||||
if len(data) == 0:
|
||||
print(color('!!! End of stream', 'red'))
|
||||
await l2cap_channel.disconnect()
|
||||
return
|
||||
|
||||
print(color(f'<<< [TCP DATA]: {len(data)} bytes', 'blue'))
|
||||
l2cap_channel.write(data)
|
||||
await l2cap_channel.drain()
|
||||
except Exception as error:
|
||||
print(f'!!! Exception: {error}')
|
||||
break
|
||||
|
||||
writer.close()
|
||||
print(color('~~~ Bye bye', 'magenta'))
|
||||
|
||||
await asyncio.start_server(
|
||||
on_tcp_connection,
|
||||
host=self.tcp_host if self.tcp_host != '_' else None,
|
||||
port=self.tcp_port,
|
||||
)
|
||||
print(
|
||||
color(
|
||||
f'### Listening for TCP connections on port {self.tcp_port}', 'magenta'
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def run(device_config, hci_transport, bridge):
|
||||
print('<<< connecting to HCI...')
|
||||
async with await open_transport_or_link(hci_transport) as (hci_source, hci_sink):
|
||||
print('<<< connected')
|
||||
|
||||
device = Device.from_config_file_with_hci(device_config, hci_source, hci_sink)
|
||||
|
||||
# Let's go
|
||||
await device.power_on()
|
||||
await bridge.start(device)
|
||||
|
||||
# Wait until the transport terminates
|
||||
await hci_source.wait_for_termination()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@click.group()
|
||||
@click.pass_context
|
||||
@click.option('--device-config', help='Device configuration file', required=True)
|
||||
@click.option('--hci-transport', help='HCI transport', required=True)
|
||||
@click.option('--psm', help='PSM for L2CAP CoC', type=int, default=1234)
|
||||
@click.option(
|
||||
'--l2cap-coc-max-credits',
|
||||
help='Maximum L2CAP CoC Credits',
|
||||
type=click.IntRange(1, 65535),
|
||||
default=128,
|
||||
)
|
||||
@click.option(
|
||||
'--l2cap-coc-mtu',
|
||||
help='L2CAP CoC MTU',
|
||||
type=click.IntRange(23, 65535),
|
||||
default=1022,
|
||||
)
|
||||
@click.option(
|
||||
'--l2cap-coc-mps',
|
||||
help='L2CAP CoC MPS',
|
||||
type=click.IntRange(23, 65533),
|
||||
default=1024,
|
||||
)
|
||||
def cli(
|
||||
context,
|
||||
device_config,
|
||||
hci_transport,
|
||||
psm,
|
||||
l2cap_coc_max_credits,
|
||||
l2cap_coc_mtu,
|
||||
l2cap_coc_mps,
|
||||
):
|
||||
context.ensure_object(dict)
|
||||
context.obj['device_config'] = device_config
|
||||
context.obj['hci_transport'] = hci_transport
|
||||
context.obj['psm'] = psm
|
||||
context.obj['max_credits'] = l2cap_coc_max_credits
|
||||
context.obj['mtu'] = l2cap_coc_mtu
|
||||
context.obj['mps'] = l2cap_coc_mps
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@cli.command()
|
||||
@click.pass_context
|
||||
@click.option('--tcp-host', help='TCP host', default='localhost')
|
||||
@click.option('--tcp-port', help='TCP port', default=9544)
|
||||
def server(context, tcp_host, tcp_port):
|
||||
bridge = ServerBridge(
|
||||
context.obj['psm'],
|
||||
context.obj['max_credits'],
|
||||
context.obj['mtu'],
|
||||
context.obj['mps'],
|
||||
tcp_host,
|
||||
tcp_port,
|
||||
)
|
||||
asyncio.run(run(context.obj['device_config'], context.obj['hci_transport'], bridge))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@cli.command()
|
||||
@click.pass_context
|
||||
@click.argument('bluetooth-address')
|
||||
@click.option('--tcp-host', help='TCP host', default='_')
|
||||
@click.option('--tcp-port', help='TCP port', default=9543)
|
||||
def client(context, bluetooth_address, tcp_host, tcp_port):
|
||||
bridge = ClientBridge(
|
||||
context.obj['psm'],
|
||||
context.obj['max_credits'],
|
||||
context.obj['mtu'],
|
||||
context.obj['mps'],
|
||||
bluetooth_address,
|
||||
tcp_host,
|
||||
tcp_port,
|
||||
)
|
||||
asyncio.run(run(context.obj['device_config'], context.obj['hci_transport'], bridge))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
|
||||
if __name__ == '__main__':
|
||||
cli(obj={}) # pylint: disable=no-value-for-parameter
|
||||
@@ -16,7 +16,6 @@
|
||||
# Imports
|
||||
# ----------------------------------------------------------------------------
|
||||
import sys
|
||||
import websockets
|
||||
import logging
|
||||
import json
|
||||
import asyncio
|
||||
@@ -24,7 +23,9 @@ import argparse
|
||||
import uuid
|
||||
import os
|
||||
from urllib.parse import urlparse
|
||||
from colors import color
|
||||
import websockets
|
||||
|
||||
from bumble.colors import color
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -65,9 +66,9 @@ class Connection:
|
||||
"""
|
||||
|
||||
def __init__(self, room, websocket):
|
||||
self.room = room
|
||||
self.room = room
|
||||
self.websocket = websocket
|
||||
self.address = str(uuid.uuid4())
|
||||
self.address = str(uuid.uuid4())
|
||||
|
||||
async def send_message(self, message):
|
||||
try:
|
||||
@@ -98,7 +99,11 @@ class Connection:
|
||||
self.address = address
|
||||
|
||||
def __str__(self):
|
||||
return f'Connection(address="{self.address}", client={self.websocket.remote_address[0]}:{self.websocket.remote_address[1]})'
|
||||
return (
|
||||
f'Connection(address="{self.address}", '
|
||||
f'client={self.websocket.remote_address[0]}:'
|
||||
f'{self.websocket.remote_address[1]})'
|
||||
)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
@@ -110,9 +115,9 @@ class Room:
|
||||
"""
|
||||
|
||||
def __init__(self, relay, name):
|
||||
self.relay = relay
|
||||
self.name = name
|
||||
self.observers = []
|
||||
self.relay = relay
|
||||
self.name = name
|
||||
self.observers = []
|
||||
self.connections = []
|
||||
|
||||
async def add_connection(self, connection):
|
||||
@@ -139,13 +144,15 @@ class Room:
|
||||
|
||||
# Parse the message to decide how to handle it
|
||||
if message.startswith('@'):
|
||||
# This is a targetted message
|
||||
await self.on_targetted_message(connection, message)
|
||||
# This is a targeted message
|
||||
await self.on_targeted_message(connection, message)
|
||||
elif message.startswith('/'):
|
||||
# This is an RPC request
|
||||
await self.on_rpc_request(connection, message)
|
||||
else:
|
||||
await connection.send_message(f'result:{error_to_json("error: invalid message")}')
|
||||
await connection.send_message(
|
||||
f'result:{error_to_json("error: invalid message")}'
|
||||
)
|
||||
|
||||
async def broadcast_message(self, sender, message):
|
||||
'''
|
||||
@@ -155,7 +162,9 @@ class Room:
|
||||
|
||||
async def on_rpc_request(self, connection, message):
|
||||
command, *params = message.split(' ', 1)
|
||||
if handler := getattr(self, f'on_{command[1:].lower().replace("-","_")}_command', None):
|
||||
if handler := getattr(
|
||||
self, f'on_{command[1:].lower().replace("-","_")}_command', None
|
||||
):
|
||||
try:
|
||||
result = await handler(connection, params)
|
||||
except Exception as error:
|
||||
@@ -165,7 +174,7 @@ class Room:
|
||||
|
||||
await connection.send_message(result or 'result:{}')
|
||||
|
||||
async def on_targetted_message(self, connection, message):
|
||||
async def on_targeted_message(self, connection, message):
|
||||
target, *payload = message.split(' ', 1)
|
||||
if not payload:
|
||||
return error_to_json('missing arguments')
|
||||
@@ -174,7 +183,8 @@ class Room:
|
||||
|
||||
# Determine what targets to send to
|
||||
if target == '*':
|
||||
# Send to all connections in the room except the connection from which the message was received
|
||||
# Send to all connections in the room except the connection from which the
|
||||
# message was received
|
||||
connections = [c for c in self.connections if c != connection]
|
||||
else:
|
||||
connections = self.find_connections_by_address(target)
|
||||
@@ -192,7 +202,9 @@ class Room:
|
||||
current_address = connection.address
|
||||
new_address = params[0]
|
||||
connection.set_address(new_address)
|
||||
await self.broadcast_message(connection, f'address-changed:from={current_address},to={new_address}')
|
||||
await self.broadcast_message(
|
||||
connection, f'address-changed:from={current_address},to={new_address}'
|
||||
)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
@@ -210,9 +222,10 @@ class Relay:
|
||||
def start(self):
|
||||
logger.info(f'Starting Relay on port {self.port}')
|
||||
|
||||
# pylint: disable-next=no-member
|
||||
return websockets.serve(self.serve, '0.0.0.0', self.port, ping_interval=None)
|
||||
|
||||
async def serve_as_controller(connection):
|
||||
async def serve_as_controller(self, connection):
|
||||
pass
|
||||
|
||||
async def serve(self, websocket, path):
|
||||
@@ -246,24 +259,24 @@ def main():
|
||||
print('ERROR: Python 3.6.1 or higher is required')
|
||||
sys.exit(1)
|
||||
|
||||
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||
|
||||
# Parse arguments
|
||||
arg_parser = argparse.ArgumentParser(description='Bumble Link Relay')
|
||||
arg_parser.add_argument('--log-level', default='INFO', help='logger level')
|
||||
arg_parser.add_argument('--log-config', help='logger config file (YAML)')
|
||||
arg_parser.add_argument('--port',
|
||||
type = int,
|
||||
default = DEFAULT_RELAY_PORT,
|
||||
help = 'Port to listen on')
|
||||
arg_parser.add_argument(
|
||||
'--port', type=int, default=DEFAULT_RELAY_PORT, help='Port to listen on'
|
||||
)
|
||||
args = arg_parser.parse_args()
|
||||
|
||||
# Setup logger
|
||||
if args.log_config:
|
||||
from logging import config
|
||||
from logging import config # pylint: disable=import-outside-toplevel
|
||||
|
||||
config.fileConfig(args.log_config)
|
||||
else:
|
||||
logging.basicConfig(level = getattr(logging, args.log_level.upper()))
|
||||
logging.basicConfig(level=getattr(logging, args.log_level.upper()))
|
||||
|
||||
# Start a relay
|
||||
relay = Relay(args.port)
|
||||
|
||||
299
apps/pair.py
299
apps/pair.py
@@ -19,12 +19,12 @@ import asyncio
|
||||
import os
|
||||
import logging
|
||||
import click
|
||||
import aioconsole
|
||||
from colors import color
|
||||
from prompt_toolkit.shortcuts import PromptSession
|
||||
|
||||
from bumble.colors import color
|
||||
from bumble.device import Device, Peer
|
||||
from bumble.transport import open_transport_or_link
|
||||
from bumble.smp import PairingDelegate, PairingConfig
|
||||
from bumble.pairing import PairingDelegate, PairingConfig
|
||||
from bumble.smp import error_name as smp_error_name
|
||||
from bumble.keys import JsonKeyStore
|
||||
from bumble.core import ProtocolError
|
||||
@@ -33,30 +33,57 @@ from bumble.gatt import (
|
||||
GATT_GENERIC_ACCESS_SERVICE,
|
||||
Service,
|
||||
Characteristic,
|
||||
CharacteristicValue
|
||||
CharacteristicValue,
|
||||
)
|
||||
from bumble.att import (
|
||||
ATT_Error,
|
||||
ATT_INSUFFICIENT_AUTHENTICATION_ERROR,
|
||||
ATT_INSUFFICIENT_ENCRYPTION_ERROR
|
||||
ATT_INSUFFICIENT_ENCRYPTION_ERROR,
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Delegate(PairingDelegate):
|
||||
def __init__(self, mode, connection, capability_string, prompt):
|
||||
super().__init__({
|
||||
'keyboard': PairingDelegate.KEYBOARD_INPUT_ONLY,
|
||||
'display': PairingDelegate.DISPLAY_OUTPUT_ONLY,
|
||||
'display+keyboard': PairingDelegate.DISPLAY_OUTPUT_AND_KEYBOARD_INPUT,
|
||||
'display+yes/no': PairingDelegate.DISPLAY_OUTPUT_AND_YES_NO_INPUT,
|
||||
'none': PairingDelegate.NO_OUTPUT_NO_INPUT
|
||||
}[capability_string.lower()])
|
||||
class Waiter:
|
||||
instance = None
|
||||
|
||||
self.mode = mode
|
||||
self.peer = Peer(connection)
|
||||
def __init__(self):
|
||||
self.done = asyncio.get_running_loop().create_future()
|
||||
|
||||
def terminate(self):
|
||||
self.done.set_result(None)
|
||||
|
||||
async def wait_until_terminated(self):
|
||||
return await self.done
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Delegate(PairingDelegate):
|
||||
def __init__(self, mode, connection, capability_string, do_prompt):
|
||||
super().__init__(
|
||||
{
|
||||
'keyboard': PairingDelegate.KEYBOARD_INPUT_ONLY,
|
||||
'display': PairingDelegate.DISPLAY_OUTPUT_ONLY,
|
||||
'display+keyboard': PairingDelegate.DISPLAY_OUTPUT_AND_KEYBOARD_INPUT,
|
||||
'display+yes/no': PairingDelegate.DISPLAY_OUTPUT_AND_YES_NO_INPUT,
|
||||
'none': PairingDelegate.NO_OUTPUT_NO_INPUT,
|
||||
}[capability_string.lower()]
|
||||
)
|
||||
|
||||
self.mode = mode
|
||||
self.peer = Peer(connection)
|
||||
self.peer_name = None
|
||||
self.prompt = prompt
|
||||
self.do_prompt = do_prompt
|
||||
|
||||
def print(self, message):
|
||||
print(color(message, 'yellow'))
|
||||
|
||||
async def prompt(self, message):
|
||||
# Wait a bit to allow some of the log lines to print before we prompt
|
||||
await asyncio.sleep(1)
|
||||
|
||||
session = PromptSession(message)
|
||||
response = await session.prompt_async()
|
||||
return response.lower().strip()
|
||||
|
||||
async def update_peer_name(self):
|
||||
if self.peer_name is not None:
|
||||
@@ -71,87 +98,83 @@ class Delegate(PairingDelegate):
|
||||
self.peer_name = '[?]'
|
||||
|
||||
async def accept(self):
|
||||
if self.prompt:
|
||||
if self.do_prompt:
|
||||
await self.update_peer_name()
|
||||
|
||||
# Wait a bit to allow some of the log lines to print before we prompt
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# Prompt for acceptance
|
||||
print(color('###-----------------------------------', 'yellow'))
|
||||
print(color(f'### Pairing request from {self.peer_name}', 'yellow'))
|
||||
print(color('###-----------------------------------', 'yellow'))
|
||||
self.print('###-----------------------------------')
|
||||
self.print(f'### Pairing request from {self.peer_name}')
|
||||
self.print('###-----------------------------------')
|
||||
while True:
|
||||
response = await aioconsole.ainput(color('>>> Accept? ', 'yellow'))
|
||||
response = response.lower().strip()
|
||||
response = await self.prompt('>>> Accept? ')
|
||||
|
||||
if response == 'yes':
|
||||
return True
|
||||
elif response == 'no':
|
||||
|
||||
if response == 'no':
|
||||
return False
|
||||
else:
|
||||
# Accept silently
|
||||
return True
|
||||
|
||||
# Accept silently
|
||||
return True
|
||||
|
||||
async def compare_numbers(self, number, digits):
|
||||
await self.update_peer_name()
|
||||
|
||||
# Wait a bit to allow some of the log lines to print before we prompt
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# Prompt for a numeric comparison
|
||||
print(color('###-----------------------------------', 'yellow'))
|
||||
print(color(f'### Pairing with {self.peer_name}', 'yellow'))
|
||||
print(color('###-----------------------------------', 'yellow'))
|
||||
self.print('###-----------------------------------')
|
||||
self.print(f'### Pairing with {self.peer_name}')
|
||||
self.print('###-----------------------------------')
|
||||
while True:
|
||||
response = await aioconsole.ainput(color(f'>>> Does the other device display {number:0{digits}}? ', 'yellow'))
|
||||
response = response.lower().strip()
|
||||
response = await self.prompt(
|
||||
f'>>> Does the other device display {number:0{digits}}? '
|
||||
)
|
||||
|
||||
if response == 'yes':
|
||||
return True
|
||||
elif response == 'no':
|
||||
|
||||
if response == 'no':
|
||||
return False
|
||||
|
||||
async def get_number(self):
|
||||
await self.update_peer_name()
|
||||
|
||||
# Wait a bit to allow some of the log lines to print before we prompt
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# Prompt for a PIN
|
||||
while True:
|
||||
try:
|
||||
print(color('###-----------------------------------', 'yellow'))
|
||||
print(color(f'### Pairing with {self.peer_name}', 'yellow'))
|
||||
print(color('###-----------------------------------', 'yellow'))
|
||||
return int(await aioconsole.ainput(color('>>> Enter PIN: ', 'yellow')))
|
||||
self.print('###-----------------------------------')
|
||||
self.print(f'### Pairing with {self.peer_name}')
|
||||
self.print('###-----------------------------------')
|
||||
return int(await self.prompt('>>> Enter PIN: '))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
async def display_number(self, number, digits):
|
||||
await self.update_peer_name()
|
||||
|
||||
# Wait a bit to allow some of the log lines to print before we prompt
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# Display a PIN code
|
||||
print(color('###-----------------------------------', 'yellow'))
|
||||
print(color(f'### Pairing with {self.peer_name}', 'yellow'))
|
||||
print(color(f'### PIN: {number:0{digits}}', 'yellow'))
|
||||
print(color('###-----------------------------------', 'yellow'))
|
||||
self.print('###-----------------------------------')
|
||||
self.print(f'### Pairing with {self.peer_name}')
|
||||
self.print(f'### PIN: {number:0{digits}}')
|
||||
self.print('###-----------------------------------')
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def get_peer_name(peer, mode):
|
||||
if mode == 'classic':
|
||||
return await peer.request_name()
|
||||
else:
|
||||
# Try to get the peer name from GATT
|
||||
services = await peer.discover_service(GATT_GENERIC_ACCESS_SERVICE)
|
||||
if not services:
|
||||
return None
|
||||
|
||||
values = await peer.read_characteristics_by_uuid(GATT_DEVICE_NAME_CHARACTERISTIC, services[0])
|
||||
if values:
|
||||
return values[0].decode('utf-8')
|
||||
# Try to get the peer name from GATT
|
||||
services = await peer.discover_service(GATT_GENERIC_ACCESS_SERVICE)
|
||||
if not services:
|
||||
return None
|
||||
|
||||
values = await peer.read_characteristics_by_uuid(
|
||||
GATT_DEVICE_NAME_CHARACTERISTIC, services[0]
|
||||
)
|
||||
if values:
|
||||
return values[0].decode('utf-8')
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -164,12 +187,12 @@ def read_with_error(connection):
|
||||
|
||||
if AUTHENTICATION_ERROR_RETURNED[0]:
|
||||
return bytes([1])
|
||||
else:
|
||||
AUTHENTICATION_ERROR_RETURNED[0] = True
|
||||
raise ATT_Error(ATT_INSUFFICIENT_AUTHENTICATION_ERROR)
|
||||
|
||||
AUTHENTICATION_ERROR_RETURNED[0] = True
|
||||
raise ATT_Error(ATT_INSUFFICIENT_AUTHENTICATION_ERROR)
|
||||
|
||||
|
||||
def write_with_error(connection, value):
|
||||
def write_with_error(connection, _value):
|
||||
if not connection.is_encrypted:
|
||||
raise ATT_Error(ATT_INSUFFICIENT_ENCRYPTION_ERROR)
|
||||
|
||||
@@ -183,14 +206,14 @@ def on_connection(connection, request):
|
||||
print(color(f'<<< Connection: {connection}', 'green'))
|
||||
|
||||
# Listen for pairing events
|
||||
connection.on('pairing_start', on_pairing_start)
|
||||
connection.on('pairing', on_pairing)
|
||||
connection.on('pairing_start', on_pairing_start)
|
||||
connection.on('pairing', on_pairing)
|
||||
connection.on('pairing_failure', on_pairing_failure)
|
||||
|
||||
# Listen for encryption changes
|
||||
connection.on(
|
||||
'connection_encryption_change',
|
||||
lambda: on_connection_encryption_change(connection)
|
||||
lambda: on_connection_encryption_change(connection),
|
||||
)
|
||||
|
||||
# Request pairing if needed
|
||||
@@ -202,7 +225,12 @@ def on_connection(connection, request):
|
||||
# -----------------------------------------------------------------------------
|
||||
def on_connection_encryption_change(connection):
|
||||
print(color('@@@-----------------------------------', 'blue'))
|
||||
print(color(f'@@@ Connection is {"" if connection.is_encrypted else "not"}encrypted', 'blue'))
|
||||
print(
|
||||
color(
|
||||
f'@@@ Connection is {"" if connection.is_encrypted else "not"}encrypted',
|
||||
'blue',
|
||||
)
|
||||
)
|
||||
print(color('@@@-----------------------------------', 'blue'))
|
||||
|
||||
|
||||
@@ -219,6 +247,7 @@ def on_pairing(keys):
|
||||
print(color('*** Paired!', 'cyan'))
|
||||
keys.print(prefix=color('*** ', 'cyan'))
|
||||
print(color('***-----------------------------------', 'cyan'))
|
||||
Waiter.instance.terminate()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -226,6 +255,7 @@ def on_pairing_failure(reason):
|
||||
print(color('***-----------------------------------', 'red'))
|
||||
print(color(f'*** Pairing failed: {smp_error_name(reason)}', 'red'))
|
||||
print(color('***-----------------------------------', 'red'))
|
||||
Waiter.instance.terminate()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -234,6 +264,7 @@ async def pair(
|
||||
sc,
|
||||
mitm,
|
||||
bond,
|
||||
ctkd,
|
||||
io,
|
||||
prompt,
|
||||
request,
|
||||
@@ -241,8 +272,10 @@ async def pair(
|
||||
keystore_file,
|
||||
device_config,
|
||||
hci_transport,
|
||||
address_or_name
|
||||
address_or_name,
|
||||
):
|
||||
Waiter.instance = Waiter()
|
||||
|
||||
print('<<< connecting to HCI...')
|
||||
async with await open_transport_or_link(hci_transport) as (hci_source, hci_sink):
|
||||
print('<<< connected')
|
||||
@@ -270,11 +303,14 @@ async def pair(
|
||||
[
|
||||
Characteristic(
|
||||
'552957FB-CF1F-4A31-9535-E78847E1A714',
|
||||
Characteristic.READ | Characteristic.WRITE,
|
||||
Characteristic.Properties.READ
|
||||
| Characteristic.Properties.WRITE,
|
||||
Characteristic.READABLE | Characteristic.WRITEABLE,
|
||||
CharacteristicValue(read=read_with_error, write=write_with_error)
|
||||
CharacteristicValue(
|
||||
read=read_with_error, write=write_with_error
|
||||
),
|
||||
)
|
||||
]
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
@@ -282,16 +318,14 @@ async def pair(
|
||||
if mode == 'classic':
|
||||
device.classic_enabled = True
|
||||
device.le_enabled = False
|
||||
device.classic_smp_enabled = ctkd
|
||||
|
||||
# Get things going
|
||||
await device.power_on()
|
||||
|
||||
# Set up a pairing config factory
|
||||
device.pairing_config_factory = lambda connection: PairingConfig(
|
||||
sc,
|
||||
mitm,
|
||||
bond,
|
||||
Delegate(mode, connection, io, prompt)
|
||||
sc, mitm, bond, Delegate(mode, connection, io, prompt)
|
||||
)
|
||||
|
||||
# Connect to a peer or wait for a connection
|
||||
@@ -311,29 +345,114 @@ async def pair(
|
||||
print(color(f'Pairing failed: {error}', 'red'))
|
||||
return
|
||||
else:
|
||||
# Advertise so that peers can find us and connect
|
||||
await device.start_advertising(auto_restart=True)
|
||||
if mode == 'le':
|
||||
# Advertise so that peers can find us and connect
|
||||
await device.start_advertising(auto_restart=True)
|
||||
else:
|
||||
# Become discoverable and connectable
|
||||
await device.set_discoverable(True)
|
||||
await device.set_connectable(True)
|
||||
|
||||
await hci_source.wait_for_termination()
|
||||
# Run until the user asks to exit
|
||||
await Waiter.instance.wait_until_terminated()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class LogHandler(logging.Handler):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setFormatter(logging.Formatter('%(levelname)s:%(name)s:%(message)s'))
|
||||
|
||||
def emit(self, record):
|
||||
message = self.format(record)
|
||||
print(message)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@click.command()
|
||||
@click.option('--mode', type=click.Choice(['le', 'classic']), default='le', show_default=True)
|
||||
@click.option('--sc', type=bool, default=True, help='Use the Secure Connections protocol', show_default=True)
|
||||
@click.option('--mitm', type=bool, default=True, help='Request MITM protection', show_default=True)
|
||||
@click.option('--bond', type=bool, default=True, help='Enable bonding', show_default=True)
|
||||
@click.option('--io', type=click.Choice(['keyboard', 'display', 'display+keyboard', 'display+yes/no', 'none']), default='display+keyboard', show_default=True)
|
||||
@click.option(
|
||||
'--mode', type=click.Choice(['le', 'classic']), default='le', show_default=True
|
||||
)
|
||||
@click.option(
|
||||
'--sc',
|
||||
type=bool,
|
||||
default=True,
|
||||
help='Use the Secure Connections protocol',
|
||||
show_default=True,
|
||||
)
|
||||
@click.option(
|
||||
'--mitm', type=bool, default=True, help='Request MITM protection', show_default=True
|
||||
)
|
||||
@click.option(
|
||||
'--bond', type=bool, default=True, help='Enable bonding', show_default=True
|
||||
)
|
||||
@click.option(
|
||||
'--ctkd',
|
||||
type=bool,
|
||||
default=True,
|
||||
help='Enable CTKD',
|
||||
show_default=True,
|
||||
)
|
||||
@click.option(
|
||||
'--io',
|
||||
type=click.Choice(
|
||||
['keyboard', 'display', 'display+keyboard', 'display+yes/no', 'none']
|
||||
),
|
||||
default='display+keyboard',
|
||||
show_default=True,
|
||||
)
|
||||
@click.option('--prompt', is_flag=True, help='Prompt to accept/reject pairing request')
|
||||
@click.option('--request', is_flag=True, help='Request that the connecting peer initiate pairing')
|
||||
@click.option(
|
||||
'--request', is_flag=True, help='Request that the connecting peer initiate pairing'
|
||||
)
|
||||
@click.option('--print-keys', is_flag=True, help='Print the bond keys before pairing')
|
||||
@click.option('--keystore-file', help='File in which to store the pairing keys')
|
||||
@click.option(
|
||||
'--keystore-file',
|
||||
metavar='<filename>',
|
||||
help='File in which to store the pairing keys',
|
||||
)
|
||||
@click.argument('device-config')
|
||||
@click.argument('hci_transport')
|
||||
@click.argument('address-or-name', required=False)
|
||||
def main(mode, sc, mitm, bond, io, prompt, request, print_keys, keystore_file, device_config, hci_transport, address_or_name):
|
||||
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||
asyncio.run(pair(mode, sc, mitm, bond, io, prompt, request, print_keys, keystore_file, device_config, hci_transport, address_or_name))
|
||||
def main(
|
||||
mode,
|
||||
sc,
|
||||
mitm,
|
||||
bond,
|
||||
ctkd,
|
||||
io,
|
||||
prompt,
|
||||
request,
|
||||
print_keys,
|
||||
keystore_file,
|
||||
device_config,
|
||||
hci_transport,
|
||||
address_or_name,
|
||||
):
|
||||
# Setup logging
|
||||
log_handler = LogHandler()
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.addHandler(log_handler)
|
||||
root_logger.setLevel(os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||
|
||||
# Pair
|
||||
asyncio.run(
|
||||
pair(
|
||||
mode,
|
||||
sc,
|
||||
mitm,
|
||||
bond,
|
||||
ctkd,
|
||||
io,
|
||||
prompt,
|
||||
request,
|
||||
print_keys,
|
||||
keystore_file,
|
||||
device_config,
|
||||
hci_transport,
|
||||
address_or_name,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
126
apps/scan.py
126
apps/scan.py
@@ -19,20 +19,20 @@ import asyncio
|
||||
import os
|
||||
import logging
|
||||
import click
|
||||
from colors import color
|
||||
|
||||
from bumble.colors import color
|
||||
from bumble.device import Device
|
||||
from bumble.transport import open_transport_or_link
|
||||
from bumble.keys import JsonKeyStore
|
||||
from bumble.smp import AddressResolver
|
||||
from bumble.hci import HCI_LE_Advertising_Report_Event
|
||||
from bumble.core import AdvertisingData
|
||||
from bumble.device import Advertisement
|
||||
from bumble.hci import HCI_Constant, HCI_LE_1M_PHY, HCI_LE_CODED_PHY
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def make_rssi_bar(rssi):
|
||||
DISPLAY_MIN_RSSI = -105
|
||||
DISPLAY_MAX_RSSI = -30
|
||||
DISPLAY_MIN_RSSI = -105
|
||||
DISPLAY_MAX_RSSI = -30
|
||||
DEFAULT_RSSI_BAR_WIDTH = 30
|
||||
|
||||
blocks = ['', '▏', '▎', '▍', '▌', '▋', '▊', '▉']
|
||||
@@ -48,19 +48,24 @@ class AdvertisementPrinter:
|
||||
self.min_rssi = min_rssi
|
||||
self.resolver = resolver
|
||||
|
||||
def print_advertisement(self, address, address_color, ad_data, rssi):
|
||||
if self.min_rssi is not None and rssi < self.min_rssi:
|
||||
def print_advertisement(self, advertisement):
|
||||
address = advertisement.address
|
||||
address_color = 'yellow' if advertisement.is_connectable else 'red'
|
||||
|
||||
if self.min_rssi is not None and advertisement.rssi < self.min_rssi:
|
||||
return
|
||||
|
||||
address_qualifier = ''
|
||||
resolution_qualifier = ''
|
||||
if self.resolver and address.is_resolvable:
|
||||
resolved = self.resolver.resolve(address)
|
||||
if self.resolver and advertisement.address.is_resolvable:
|
||||
resolved = self.resolver.resolve(advertisement.address)
|
||||
if resolved is not None:
|
||||
resolution_qualifier = f'(resolved from {address})'
|
||||
resolution_qualifier = f'(resolved from {advertisement.address})'
|
||||
address = resolved
|
||||
|
||||
address_type_string = ('PUBLIC', 'RANDOM', 'PUBLIC_ID', 'RANDOM_ID')[address.address_type]
|
||||
address_type_string = ('PUBLIC', 'RANDOM', 'PUBLIC_ID', 'RANDOM_ID')[
|
||||
address.address_type
|
||||
]
|
||||
if address.is_public:
|
||||
type_color = 'cyan'
|
||||
else:
|
||||
@@ -74,18 +79,32 @@ class AdvertisementPrinter:
|
||||
type_color = 'blue'
|
||||
address_qualifier = '(non-resolvable)'
|
||||
|
||||
rssi_bar = make_rssi_bar(rssi)
|
||||
separator = '\n '
|
||||
print(f'>>> {color(address, address_color)} [{color(address_type_string, type_color)}]{address_qualifier}{resolution_qualifier}:{separator}RSSI:{rssi:4} {rssi_bar}{separator}{ad_data.to_string(separator)}\n')
|
||||
rssi_bar = make_rssi_bar(advertisement.rssi)
|
||||
if not advertisement.is_legacy:
|
||||
phy_info = (
|
||||
f'PHY: {HCI_Constant.le_phy_name(advertisement.primary_phy)}/'
|
||||
f'{HCI_Constant.le_phy_name(advertisement.secondary_phy)} '
|
||||
f'{separator}'
|
||||
)
|
||||
else:
|
||||
phy_info = ''
|
||||
|
||||
def on_advertisement(self, address, ad_data, rssi, connectable):
|
||||
address_color = 'yellow' if connectable else 'red'
|
||||
self.print_advertisement(address, address_color, ad_data, rssi)
|
||||
print(
|
||||
f'>>> {color(address, address_color)} '
|
||||
f'[{color(address_type_string, type_color)}]{address_qualifier}'
|
||||
f'{resolution_qualifier}:{separator}'
|
||||
f'{phy_info}'
|
||||
f'RSSI:{advertisement.rssi:4} {rssi_bar}{separator}'
|
||||
f'{advertisement.data.to_string(separator)}\n'
|
||||
)
|
||||
|
||||
def on_advertising_report(self, address, ad_data, rssi, event_type):
|
||||
print(f'{color("EVENT", "green")}: {HCI_LE_Advertising_Report_Event.event_type_name(event_type)}')
|
||||
ad_data = AdvertisingData.from_bytes(ad_data)
|
||||
self.print_advertisement(address, 'yellow', ad_data, rssi)
|
||||
def on_advertisement(self, advertisement):
|
||||
self.print_advertisement(advertisement)
|
||||
|
||||
def on_advertising_report(self, report):
|
||||
print(f'{color("EVENT", "green")}: {report.event_type_string()}')
|
||||
self.print_advertisement(Advertisement.from_advertising_report(report))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -94,20 +113,25 @@ async def scan(
|
||||
passive,
|
||||
scan_interval,
|
||||
scan_window,
|
||||
phy,
|
||||
filter_duplicates,
|
||||
raw,
|
||||
keystore_file,
|
||||
device_config,
|
||||
transport
|
||||
transport,
|
||||
):
|
||||
print('<<< connecting to HCI...')
|
||||
async with await open_transport_or_link(transport) as (hci_source, hci_sink):
|
||||
print('<<< connected')
|
||||
|
||||
if device_config:
|
||||
device = Device.from_config_file_with_hci(device_config, hci_source, hci_sink)
|
||||
device = Device.from_config_file_with_hci(
|
||||
device_config, hci_source, hci_sink
|
||||
)
|
||||
else:
|
||||
device = Device.with_hci('Bumble', 'F0:F1:F2:F3:F4:F5', hci_source, hci_sink)
|
||||
device = Device.with_hci(
|
||||
'Bumble', 'F0:F1:F2:F3:F4:F5', hci_source, hci_sink
|
||||
)
|
||||
|
||||
if keystore_file:
|
||||
keystore = JsonKeyStore(namespace=None, filename=keystore_file)
|
||||
@@ -126,11 +150,18 @@ async def scan(
|
||||
device.on('advertisement', printer.on_advertisement)
|
||||
|
||||
await device.power_on()
|
||||
|
||||
if phy is None:
|
||||
scanning_phys = [HCI_LE_1M_PHY, HCI_LE_CODED_PHY]
|
||||
else:
|
||||
scanning_phys = [{'1m': HCI_LE_1M_PHY, 'coded': HCI_LE_CODED_PHY}[phy]]
|
||||
|
||||
await device.start_scanning(
|
||||
active=(not passive),
|
||||
scan_interval=scan_interval,
|
||||
scan_window=scan_window,
|
||||
filter_duplicates=filter_duplicates
|
||||
filter_duplicates=filter_duplicates,
|
||||
scanning_phys=scanning_phys,
|
||||
)
|
||||
|
||||
await hci_source.wait_for_termination()
|
||||
@@ -142,14 +173,51 @@ async def scan(
|
||||
@click.option('--passive', is_flag=True, default=False, help='Perform passive scanning')
|
||||
@click.option('--scan-interval', type=int, default=60, help='Scan interval')
|
||||
@click.option('--scan-window', type=int, default=60, help='Scan window')
|
||||
@click.option('--filter-duplicates', type=bool, default=True, help='Filter duplicates at the controller level')
|
||||
@click.option('--raw', is_flag=True, default=False, help='Listen for raw advertising reports instead of processed ones')
|
||||
@click.option(
|
||||
'--phy', type=click.Choice(['1m', 'coded']), help='Only scan on the specified PHY'
|
||||
)
|
||||
@click.option(
|
||||
'--filter-duplicates',
|
||||
type=bool,
|
||||
default=True,
|
||||
help='Filter duplicates at the controller level',
|
||||
)
|
||||
@click.option(
|
||||
'--raw',
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help='Listen for raw advertising reports instead of processed ones',
|
||||
)
|
||||
@click.option('--keystore-file', help='Keystore file to use when resolving addresses')
|
||||
@click.option('--device-config', help='Device config file for the scanning device')
|
||||
@click.argument('transport')
|
||||
def main(min_rssi, passive, scan_interval, scan_window, filter_duplicates, raw, keystore_file, device_config, transport):
|
||||
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
|
||||
asyncio.run(scan(min_rssi, passive, scan_interval, scan_window, filter_duplicates, raw, keystore_file, device_config, transport))
|
||||
def main(
|
||||
min_rssi,
|
||||
passive,
|
||||
scan_interval,
|
||||
scan_window,
|
||||
phy,
|
||||
filter_duplicates,
|
||||
raw,
|
||||
keystore_file,
|
||||
device_config,
|
||||
transport,
|
||||
):
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
|
||||
asyncio.run(
|
||||
scan(
|
||||
min_rssi,
|
||||
passive,
|
||||
scan_interval,
|
||||
scan_window,
|
||||
phy,
|
||||
filter_duplicates,
|
||||
raw,
|
||||
keystore_file,
|
||||
device_config,
|
||||
transport,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
50
apps/show.py
50
apps/show.py
@@ -17,8 +17,8 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
import struct
|
||||
import click
|
||||
from colors import color
|
||||
|
||||
from bumble.colors import color
|
||||
from bumble import hci
|
||||
from bumble.transport.common import PacketReader
|
||||
from bumble.helpers import PacketTracer
|
||||
@@ -27,13 +27,14 @@ from bumble.helpers import PacketTracer
|
||||
# -----------------------------------------------------------------------------
|
||||
class SnoopPacketReader:
|
||||
'''
|
||||
Reader that reads HCI packets from a "snoop" file (based on RFC 1761, but not exactly the same...)
|
||||
Reader that reads HCI packets from a "snoop" file (based on RFC 1761, but not
|
||||
exactly the same...)
|
||||
'''
|
||||
|
||||
DATALINK_H1 = 1001
|
||||
DATALINK_H4 = 1002
|
||||
DATALINK_H1 = 1001
|
||||
DATALINK_H4 = 1002
|
||||
DATALINK_BSCP = 1003
|
||||
DATALINK_H5 = 1004
|
||||
DATALINK_H5 = 1004
|
||||
|
||||
def __init__(self, source):
|
||||
self.source = source
|
||||
@@ -41,9 +42,13 @@ class SnoopPacketReader:
|
||||
# Read the header
|
||||
identification_pattern = source.read(8)
|
||||
if identification_pattern.hex().lower() != '6274736e6f6f7000':
|
||||
raise ValueError('not a valid snoop file, unexpected identification pattern')
|
||||
(self.version_number, self.data_link_type) = struct.unpack('>II', source.read(8))
|
||||
if self.data_link_type != self.DATALINK_H4 and self.data_link_type != self.DATALINK_H1:
|
||||
raise ValueError(
|
||||
'not a valid snoop file, unexpected identification pattern'
|
||||
)
|
||||
(self.version_number, self.data_link_type) = struct.unpack(
|
||||
'>II', source.read(8)
|
||||
)
|
||||
if self.data_link_type not in (self.DATALINK_H4, self.DATALINK_H1):
|
||||
raise ValueError(f'datalink type {self.data_link_type} not supported')
|
||||
|
||||
def next_packet(self):
|
||||
@@ -55,9 +60,9 @@ class SnoopPacketReader:
|
||||
original_length,
|
||||
included_length,
|
||||
packet_flags,
|
||||
cumulative_drops,
|
||||
timestamp_seconds,
|
||||
timestamp_microsecond
|
||||
_cumulative_drops,
|
||||
_timestamp_seconds,
|
||||
_timestamp_microsecond,
|
||||
) = struct.unpack('>IIIIII', header)
|
||||
|
||||
# Abort on truncated packets
|
||||
@@ -79,24 +84,34 @@ class SnoopPacketReader:
|
||||
else:
|
||||
packet_type = hci.HCI_ACL_DATA_PACKET
|
||||
|
||||
return (packet_flags & 1, bytes([packet_type]) + self.source.read(included_length))
|
||||
else:
|
||||
return (packet_flags & 1, self.source.read(included_length))
|
||||
return (
|
||||
packet_flags & 1,
|
||||
bytes([packet_type]) + self.source.read(included_length),
|
||||
)
|
||||
|
||||
return (packet_flags & 1, self.source.read(included_length))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Main
|
||||
# -----------------------------------------------------------------------------
|
||||
@click.command()
|
||||
@click.option('--format', type=click.Choice(['h4', 'snoop']), default='h4', help='Format of the input file')
|
||||
@click.option(
|
||||
'--format',
|
||||
type=click.Choice(['h4', 'snoop']),
|
||||
default='h4',
|
||||
help='Format of the input file',
|
||||
)
|
||||
@click.argument('filename')
|
||||
# pylint: disable=redefined-builtin
|
||||
def main(format, filename):
|
||||
input = open(filename, 'rb')
|
||||
if format == 'h4':
|
||||
packet_reader = PacketReader(input)
|
||||
|
||||
def read_next_packet():
|
||||
(0, packet_reader.next_packet())
|
||||
return (0, packet_reader.next_packet())
|
||||
|
||||
else:
|
||||
packet_reader = SnoopPacketReader(input)
|
||||
read_next_packet = packet_reader.next_packet
|
||||
@@ -112,9 +127,8 @@ def main(format, filename):
|
||||
|
||||
except Exception as error:
|
||||
print(color(f'!!! {error}', 'red'))
|
||||
pass
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
main() # pylint: disable=no-value-for-parameter
|
||||
|
||||
@@ -54,7 +54,7 @@ async def unbond(keystore_file, device_config, address):
|
||||
@click.argument('device-config')
|
||||
@click.argument('address', required=False)
|
||||
def main(keystore_file, device_config, address):
|
||||
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||
asyncio.run(unbond(keystore_file, device_config, address))
|
||||
|
||||
|
||||
|
||||
@@ -28,18 +28,19 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
import os
|
||||
import logging
|
||||
import sys
|
||||
import click
|
||||
import usb1
|
||||
from colors import color
|
||||
|
||||
from bumble.colors import color
|
||||
from bumble.transport.usb import load_libusb
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
USB_DEVICE_CLASS_DEVICE = 0x00
|
||||
USB_DEVICE_CLASS_WIRELESS_CONTROLLER = 0xE0
|
||||
USB_DEVICE_SUBCLASS_RF_CONTROLLER = 0x01
|
||||
USB_DEVICE_CLASS_DEVICE = 0x00
|
||||
USB_DEVICE_CLASS_WIRELESS_CONTROLLER = 0xE0
|
||||
USB_DEVICE_SUBCLASS_RF_CONTROLLER = 0x01
|
||||
USB_DEVICE_PROTOCOL_BLUETOOTH_PRIMARY_CONTROLLER = 0x01
|
||||
|
||||
USB_DEVICE_CLASSES = {
|
||||
@@ -69,22 +70,22 @@ USB_DEVICE_CLASSES = {
|
||||
0x01: 'Bluetooth',
|
||||
0x02: 'UWB',
|
||||
0x03: 'Remote NDIS',
|
||||
0x04: 'Bluetooth AMP'
|
||||
0x04: 'Bluetooth AMP',
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
0xEF: 'Miscellaneous',
|
||||
0xFE: 'Application Specific',
|
||||
0xFF: 'Vendor Specific'
|
||||
0xFF: 'Vendor Specific',
|
||||
}
|
||||
|
||||
USB_ENDPOINT_IN = 0x80
|
||||
USB_ENDPOINT_IN = 0x80
|
||||
USB_ENDPOINT_TYPES = ['CONTROL', 'ISOCHRONOUS', 'BULK', 'INTERRUPT']
|
||||
|
||||
USB_BT_HCI_CLASS_TUPLE = (
|
||||
USB_DEVICE_CLASS_WIRELESS_CONTROLLER,
|
||||
USB_DEVICE_SUBCLASS_RF_CONTROLLER,
|
||||
USB_DEVICE_PROTOCOL_BLUETOOTH_PRIMARY_CONTROLLER
|
||||
USB_DEVICE_PROTOCOL_BLUETOOTH_PRIMARY_CONTROLLER,
|
||||
)
|
||||
|
||||
|
||||
@@ -94,19 +95,26 @@ def show_device_details(device):
|
||||
print(f' Configuration {configuration.getConfigurationValue()}')
|
||||
for interface in configuration:
|
||||
for setting in interface:
|
||||
alternateSetting = setting.getAlternateSetting()
|
||||
suffix = f'/{alternateSetting}' if interface.getNumSettings() > 1 else ''
|
||||
alternate_setting = setting.getAlternateSetting()
|
||||
suffix = (
|
||||
f'/{alternate_setting}' if interface.getNumSettings() > 1 else ''
|
||||
)
|
||||
(class_string, subclass_string) = get_class_info(
|
||||
setting.getClass(),
|
||||
setting.getSubClass(),
|
||||
setting.getProtocol()
|
||||
setting.getClass(), setting.getSubClass(), setting.getProtocol()
|
||||
)
|
||||
details = f'({class_string}, {subclass_string})'
|
||||
print(f' Interface: {setting.getNumber()}{suffix} {details}')
|
||||
for endpoint in setting:
|
||||
endpoint_type = USB_ENDPOINT_TYPES[endpoint.getAttributes() & 3]
|
||||
endpoint_direction = 'OUT' if (endpoint.getAddress() & USB_ENDPOINT_IN == 0) else 'IN'
|
||||
print(f' Endpoint 0x{endpoint.getAddress():02X}: {endpoint_type} {endpoint_direction}')
|
||||
endpoint_direction = (
|
||||
'OUT'
|
||||
if (endpoint.getAddress() & USB_ENDPOINT_IN == 0)
|
||||
else 'IN'
|
||||
)
|
||||
print(
|
||||
f' Endpoint 0x{endpoint.getAddress():02X}: '
|
||||
f'{endpoint_type} {endpoint_direction}'
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -116,7 +124,7 @@ def get_class_info(cls, subclass, protocol):
|
||||
if class_info is None:
|
||||
class_string = f'0x{cls:02X}'
|
||||
else:
|
||||
if type(class_info) is tuple:
|
||||
if isinstance(class_info, tuple):
|
||||
class_string = class_info[0]
|
||||
subclass_info = class_info[1].get(subclass)
|
||||
if subclass_info:
|
||||
@@ -135,7 +143,11 @@ def get_class_info(cls, subclass, protocol):
|
||||
# -----------------------------------------------------------------------------
|
||||
def is_bluetooth_hci(device):
|
||||
# Check if the device class indicates a match
|
||||
if (device.getDeviceClass(), device.getDeviceSubClass(), device.getDeviceProtocol()) == USB_BT_HCI_CLASS_TUPLE:
|
||||
if (
|
||||
device.getDeviceClass(),
|
||||
device.getDeviceSubClass(),
|
||||
device.getDeviceProtocol(),
|
||||
) == USB_BT_HCI_CLASS_TUPLE:
|
||||
return True
|
||||
|
||||
# If the device class is 'Device', look for a matching interface
|
||||
@@ -143,7 +155,11 @@ def is_bluetooth_hci(device):
|
||||
for configuration in device:
|
||||
for interface in configuration:
|
||||
for setting in interface:
|
||||
if (setting.getClass(), setting.getSubClass(), setting.getProtocol()) == USB_BT_HCI_CLASS_TUPLE:
|
||||
if (
|
||||
setting.getClass(),
|
||||
setting.getSubClass(),
|
||||
setting.getProtocol(),
|
||||
) == USB_BT_HCI_CLASS_TUPLE:
|
||||
return True
|
||||
|
||||
return False
|
||||
@@ -153,23 +169,22 @@ def is_bluetooth_hci(device):
|
||||
@click.command()
|
||||
@click.option('--verbose', is_flag=True, default=False, help='Print more details')
|
||||
def main(verbose):
|
||||
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
|
||||
|
||||
load_libusb()
|
||||
with usb1.USBContext() as context:
|
||||
bluetooth_device_count = 0
|
||||
devices = {}
|
||||
|
||||
for device in context.getDeviceIterator(skip_on_error=True):
|
||||
device_class = device.getDeviceClass()
|
||||
device_class = device.getDeviceClass()
|
||||
device_subclass = device.getDeviceSubClass()
|
||||
device_protocol = device.getDeviceProtocol()
|
||||
|
||||
device_id = (device.getVendorID(), device.getProductID())
|
||||
|
||||
(device_class_string, device_subclass_string) = get_class_info(
|
||||
device_class,
|
||||
device_subclass,
|
||||
device_protocol
|
||||
device_class, device_subclass, device_protocol
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -198,7 +213,9 @@ def main(verbose):
|
||||
|
||||
# Compute the different ways this can be referenced as a Bumble transport
|
||||
bumble_transport_names = []
|
||||
basic_transport_name = f'usb:{device.getVendorID():04X}:{device.getProductID():04X}'
|
||||
basic_transport_name = (
|
||||
f'usb:{device.getVendorID():04X}:{device.getProductID():04X}'
|
||||
)
|
||||
|
||||
if device_is_bluetooth_hci:
|
||||
bumble_transport_names.append(f'usb:{bluetooth_device_count - 1}')
|
||||
@@ -206,17 +223,39 @@ def main(verbose):
|
||||
if device_id not in devices:
|
||||
bumble_transport_names.append(basic_transport_name)
|
||||
else:
|
||||
bumble_transport_names.append(f'{basic_transport_name}#{len(devices[device_id])}')
|
||||
bumble_transport_names.append(
|
||||
f'{basic_transport_name}#{len(devices[device_id])}'
|
||||
)
|
||||
|
||||
if device_serial_number is not None:
|
||||
if device_id not in devices or device_serial_number not in devices[device_id]:
|
||||
bumble_transport_names.append(f'{basic_transport_name}/{device_serial_number}')
|
||||
if (
|
||||
device_id not in devices
|
||||
or device_serial_number not in devices[device_id]
|
||||
):
|
||||
bumble_transport_names.append(
|
||||
f'{basic_transport_name}/{device_serial_number}'
|
||||
)
|
||||
|
||||
# Print the results
|
||||
print(color(f'ID {device.getVendorID():04X}:{device.getProductID():04X}', fg=fg_color, bg=bg_color))
|
||||
print(
|
||||
color(
|
||||
f'ID {device.getVendorID():04X}:{device.getProductID():04X}',
|
||||
fg=fg_color,
|
||||
bg=bg_color,
|
||||
)
|
||||
)
|
||||
if bumble_transport_names:
|
||||
print(color(' Bumble Transport Names:', 'blue'), ' or '.join(color(x, 'cyan' if device_is_bluetooth_hci else 'red') for x in bumble_transport_names))
|
||||
print(color(' Bus/Device: ', 'green'), f'{device.getBusNumber():03}/{device.getDeviceAddress():03}')
|
||||
print(
|
||||
color(' Bumble Transport Names:', 'blue'),
|
||||
' or '.join(
|
||||
color(x, 'cyan' if device_is_bluetooth_hci else 'red')
|
||||
for x in bumble_transport_names
|
||||
),
|
||||
)
|
||||
print(
|
||||
color(' Bus/Device: ', 'green'),
|
||||
f'{device.getBusNumber():03}/{device.getDeviceAddress():03}',
|
||||
)
|
||||
print(color(' Class: ', 'green'), device_class_string)
|
||||
print(color(' Subclass/Protocol: ', 'green'), device_subclass_string)
|
||||
if device_serial_number is not None:
|
||||
@@ -236,4 +275,4 @@ def main(verbose):
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
main() # pylint: disable=no-value-for-parameter
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
try:
|
||||
from ._version import version as __version__
|
||||
except ImportError:
|
||||
__version__ = "unknown version"
|
||||
|
||||
477
bumble/a2dp.py
477
bumble/a2dp.py
@@ -16,10 +16,8 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import struct
|
||||
import bitstruct
|
||||
import logging
|
||||
from collections import namedtuple
|
||||
from colors import color
|
||||
|
||||
from .company_ids import COMPANY_IDENTIFIERS
|
||||
from .sdp import (
|
||||
@@ -30,7 +28,7 @@ from .sdp import (
|
||||
SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
|
||||
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
|
||||
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID
|
||||
SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
)
|
||||
from .core import (
|
||||
BT_L2CAP_PROTOCOL_ID,
|
||||
@@ -38,7 +36,7 @@ from .core import (
|
||||
BT_AUDIO_SINK_SERVICE,
|
||||
BT_AVDTP_PROTOCOL_ID,
|
||||
BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE,
|
||||
name_or_number
|
||||
name_or_number,
|
||||
)
|
||||
|
||||
|
||||
@@ -51,6 +49,7 @@ logger = logging.getLogger(__name__)
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
# fmt: off
|
||||
|
||||
A2DP_SBC_CODEC_TYPE = 0x00
|
||||
A2DP_MPEG_1_2_AUDIO_CODEC_TYPE = 0x01
|
||||
@@ -127,71 +126,115 @@ MPEG_2_4_OBJECT_TYPE_NAMES = {
|
||||
MPEG_4_AAC_SCALABLE_OBJECT_TYPE: 'MPEG_4_AAC_SCALABLE_OBJECT_TYPE'
|
||||
}
|
||||
|
||||
# fmt: on
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def flags_to_list(flags, values):
|
||||
result = []
|
||||
for i in range(len(values)):
|
||||
for i, value in enumerate(values):
|
||||
if flags & (1 << (len(values) - i - 1)):
|
||||
result.append(values[i])
|
||||
result.append(value)
|
||||
return result
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def make_audio_source_service_sdp_records(service_record_handle, version=(1, 3)):
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from .avdtp import AVDTP_PSM
|
||||
|
||||
version_int = version[0] << 8 | version[1]
|
||||
return [
|
||||
ServiceAttribute(SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID, DataElement.unsigned_integer_32(service_record_handle)),
|
||||
ServiceAttribute(SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID, DataElement.sequence([
|
||||
DataElement.uuid(SDP_PUBLIC_BROWSE_ROOT)
|
||||
])),
|
||||
ServiceAttribute(SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID, DataElement.sequence([
|
||||
DataElement.uuid(BT_AUDIO_SOURCE_SERVICE)
|
||||
])),
|
||||
ServiceAttribute(SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID, DataElement.sequence([
|
||||
DataElement.sequence([
|
||||
DataElement.uuid(BT_L2CAP_PROTOCOL_ID),
|
||||
DataElement.unsigned_integer_16(AVDTP_PSM)
|
||||
]),
|
||||
DataElement.sequence([
|
||||
DataElement.uuid(BT_AVDTP_PROTOCOL_ID),
|
||||
DataElement.unsigned_integer_16(version_int)
|
||||
])
|
||||
])),
|
||||
ServiceAttribute(SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID, DataElement.sequence([
|
||||
DataElement.uuid(BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE),
|
||||
DataElement.unsigned_integer_16(version_int)
|
||||
])),
|
||||
ServiceAttribute(
|
||||
SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
|
||||
DataElement.unsigned_integer_32(service_record_handle),
|
||||
),
|
||||
ServiceAttribute(
|
||||
SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
|
||||
DataElement.sequence([DataElement.uuid(SDP_PUBLIC_BROWSE_ROOT)]),
|
||||
),
|
||||
ServiceAttribute(
|
||||
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
|
||||
DataElement.sequence([DataElement.uuid(BT_AUDIO_SOURCE_SERVICE)]),
|
||||
),
|
||||
ServiceAttribute(
|
||||
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(BT_L2CAP_PROTOCOL_ID),
|
||||
DataElement.unsigned_integer_16(AVDTP_PSM),
|
||||
]
|
||||
),
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(BT_AVDTP_PROTOCOL_ID),
|
||||
DataElement.unsigned_integer_16(version_int),
|
||||
]
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
ServiceAttribute(
|
||||
SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE),
|
||||
DataElement.unsigned_integer_16(version_int),
|
||||
]
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def make_audio_sink_service_sdp_records(service_record_handle, version=(1, 3)):
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from .avdtp import AVDTP_PSM
|
||||
|
||||
version_int = version[0] << 8 | version[1]
|
||||
return [
|
||||
ServiceAttribute(SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID, DataElement.unsigned_integer_32(service_record_handle)),
|
||||
ServiceAttribute(SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID, DataElement.sequence([
|
||||
DataElement.uuid(SDP_PUBLIC_BROWSE_ROOT)
|
||||
])),
|
||||
ServiceAttribute(SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID, DataElement.sequence([
|
||||
DataElement.uuid(BT_AUDIO_SINK_SERVICE)
|
||||
])),
|
||||
ServiceAttribute(SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID, DataElement.sequence([
|
||||
DataElement.sequence([
|
||||
DataElement.uuid(BT_L2CAP_PROTOCOL_ID),
|
||||
DataElement.unsigned_integer_16(AVDTP_PSM)
|
||||
]),
|
||||
DataElement.sequence([
|
||||
DataElement.uuid(BT_AVDTP_PROTOCOL_ID),
|
||||
DataElement.unsigned_integer_16(version_int)
|
||||
])
|
||||
])),
|
||||
ServiceAttribute(SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID, DataElement.sequence([
|
||||
DataElement.uuid(BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE),
|
||||
DataElement.unsigned_integer_16(version_int)
|
||||
])),
|
||||
ServiceAttribute(
|
||||
SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
|
||||
DataElement.unsigned_integer_32(service_record_handle),
|
||||
),
|
||||
ServiceAttribute(
|
||||
SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
|
||||
DataElement.sequence([DataElement.uuid(SDP_PUBLIC_BROWSE_ROOT)]),
|
||||
),
|
||||
ServiceAttribute(
|
||||
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
|
||||
DataElement.sequence([DataElement.uuid(BT_AUDIO_SINK_SERVICE)]),
|
||||
),
|
||||
ServiceAttribute(
|
||||
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(BT_L2CAP_PROTOCOL_ID),
|
||||
DataElement.unsigned_integer_16(AVDTP_PSM),
|
||||
]
|
||||
),
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(BT_AVDTP_PROTOCOL_ID),
|
||||
DataElement.unsigned_integer_16(version_int),
|
||||
]
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
ServiceAttribute(
|
||||
SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE),
|
||||
DataElement.unsigned_integer_16(version_int),
|
||||
]
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@@ -206,45 +249,46 @@ class SbcMediaCodecInformation(
|
||||
'subbands',
|
||||
'allocation_method',
|
||||
'minimum_bitpool_value',
|
||||
'maximum_bitpool_value'
|
||||
]
|
||||
'maximum_bitpool_value',
|
||||
],
|
||||
)
|
||||
):
|
||||
'''
|
||||
A2DP spec - 4.3.2 Codec Specific Information Elements
|
||||
'''
|
||||
|
||||
BIT_FIELDS = 'u4u4u4u2u2u8u8'
|
||||
SAMPLING_FREQUENCY_BITS = {
|
||||
16000: 1 << 3,
|
||||
32000: 1 << 2,
|
||||
44100: 1 << 1,
|
||||
48000: 1
|
||||
}
|
||||
SAMPLING_FREQUENCY_BITS = {16000: 1 << 3, 32000: 1 << 2, 44100: 1 << 1, 48000: 1}
|
||||
CHANNEL_MODE_BITS = {
|
||||
SBC_MONO_CHANNEL_MODE: 1 << 3,
|
||||
SBC_DUAL_CHANNEL_MODE: 1 << 2,
|
||||
SBC_STEREO_CHANNEL_MODE: 1 << 1,
|
||||
SBC_JOINT_STEREO_CHANNEL_MODE: 1
|
||||
}
|
||||
BLOCK_LENGTH_BITS = {
|
||||
4: 1 << 3,
|
||||
8: 1 << 2,
|
||||
12: 1 << 1,
|
||||
16: 1
|
||||
}
|
||||
SUBBANDS_BITS = {
|
||||
4: 1 << 1,
|
||||
8: 1
|
||||
SBC_MONO_CHANNEL_MODE: 1 << 3,
|
||||
SBC_DUAL_CHANNEL_MODE: 1 << 2,
|
||||
SBC_STEREO_CHANNEL_MODE: 1 << 1,
|
||||
SBC_JOINT_STEREO_CHANNEL_MODE: 1,
|
||||
}
|
||||
BLOCK_LENGTH_BITS = {4: 1 << 3, 8: 1 << 2, 12: 1 << 1, 16: 1}
|
||||
SUBBANDS_BITS = {4: 1 << 1, 8: 1}
|
||||
ALLOCATION_METHOD_BITS = {
|
||||
SBC_SNR_ALLOCATION_METHOD: 1 << 1,
|
||||
SBC_LOUDNESS_ALLOCATION_METHOD: 1
|
||||
SBC_SNR_ALLOCATION_METHOD: 1 << 1,
|
||||
SBC_LOUDNESS_ALLOCATION_METHOD: 1,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(data):
|
||||
return SbcMediaCodecInformation(*bitstruct.unpack(SbcMediaCodecInformation.BIT_FIELDS, data))
|
||||
def from_bytes(data: bytes) -> 'SbcMediaCodecInformation':
|
||||
sampling_frequency = (data[0] >> 4) & 0x0F
|
||||
channel_mode = (data[0] >> 0) & 0x0F
|
||||
block_length = (data[1] >> 4) & 0x0F
|
||||
subbands = (data[1] >> 2) & 0x03
|
||||
allocation_method = (data[1] >> 0) & 0x03
|
||||
minimum_bitpool_value = (data[2] >> 0) & 0xFF
|
||||
maximum_bitpool_value = (data[3] >> 0) & 0xFF
|
||||
return SbcMediaCodecInformation(
|
||||
sampling_frequency,
|
||||
channel_mode,
|
||||
block_length,
|
||||
subbands,
|
||||
allocation_method,
|
||||
minimum_bitpool_value,
|
||||
maximum_bitpool_value,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_discrete_values(
|
||||
@@ -255,16 +299,16 @@ class SbcMediaCodecInformation(
|
||||
subbands,
|
||||
allocation_method,
|
||||
minimum_bitpool_value,
|
||||
maximum_bitpool_value
|
||||
maximum_bitpool_value,
|
||||
):
|
||||
return SbcMediaCodecInformation(
|
||||
sampling_frequency = cls.SAMPLING_FREQUENCY_BITS[sampling_frequency],
|
||||
channel_mode = cls.CHANNEL_MODE_BITS[channel_mode],
|
||||
block_length = cls.BLOCK_LENGTH_BITS[block_length],
|
||||
subbands = cls.SUBBANDS_BITS[subbands],
|
||||
allocation_method = cls.ALLOCATION_METHOD_BITS[allocation_method],
|
||||
minimum_bitpool_value = minimum_bitpool_value,
|
||||
maximum_bitpool_value = maximum_bitpool_value
|
||||
sampling_frequency=cls.SAMPLING_FREQUENCY_BITS[sampling_frequency],
|
||||
channel_mode=cls.CHANNEL_MODE_BITS[channel_mode],
|
||||
block_length=cls.BLOCK_LENGTH_BITS[block_length],
|
||||
subbands=cls.SUBBANDS_BITS[subbands],
|
||||
allocation_method=cls.ALLOCATION_METHOD_BITS[allocation_method],
|
||||
minimum_bitpool_value=minimum_bitpool_value,
|
||||
maximum_bitpool_value=maximum_bitpool_value,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -276,63 +320,71 @@ class SbcMediaCodecInformation(
|
||||
subbands,
|
||||
allocation_methods,
|
||||
minimum_bitpool_value,
|
||||
maximum_bitpool_value
|
||||
maximum_bitpool_value,
|
||||
):
|
||||
return SbcMediaCodecInformation(
|
||||
sampling_frequency = sum(cls.SAMPLING_FREQUENCY_BITS[x] for x in sampling_frequencies),
|
||||
channel_mode = sum(cls.CHANNEL_MODE_BITS[x] for x in channel_modes),
|
||||
block_length = sum(cls.BLOCK_LENGTH_BITS[x] for x in block_lengths),
|
||||
subbands = sum(cls.SUBBANDS_BITS[x] for x in subbands),
|
||||
allocation_method = sum(cls.ALLOCATION_METHOD_BITS[x] for x in allocation_methods),
|
||||
minimum_bitpool_value = minimum_bitpool_value,
|
||||
maximum_bitpool_value = maximum_bitpool_value
|
||||
sampling_frequency=sum(
|
||||
cls.SAMPLING_FREQUENCY_BITS[x] for x in sampling_frequencies
|
||||
),
|
||||
channel_mode=sum(cls.CHANNEL_MODE_BITS[x] for x in channel_modes),
|
||||
block_length=sum(cls.BLOCK_LENGTH_BITS[x] for x in block_lengths),
|
||||
subbands=sum(cls.SUBBANDS_BITS[x] for x in subbands),
|
||||
allocation_method=sum(
|
||||
cls.ALLOCATION_METHOD_BITS[x] for x in allocation_methods
|
||||
),
|
||||
minimum_bitpool_value=minimum_bitpool_value,
|
||||
maximum_bitpool_value=maximum_bitpool_value,
|
||||
)
|
||||
|
||||
def __bytes__(self):
|
||||
return bitstruct.pack(self.BIT_FIELDS, *self)
|
||||
def __bytes__(self) -> bytes:
|
||||
return bytes(
|
||||
[
|
||||
(self.sampling_frequency << 4) | self.channel_mode,
|
||||
(self.block_length << 4)
|
||||
| (self.subbands << 2)
|
||||
| self.allocation_method,
|
||||
self.minimum_bitpool_value,
|
||||
self.maximum_bitpool_value,
|
||||
]
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
channel_modes = ['MONO', 'DUAL_CHANNEL', 'STEREO', 'JOINT_STEREO']
|
||||
allocation_methods = ['SNR', 'Loudness']
|
||||
return '\n'.join([
|
||||
'SbcMediaCodecInformation(',
|
||||
f' sampling_frequency: {",".join([str(x) for x in flags_to_list(self.sampling_frequency, SBC_SAMPLING_FREQUENCIES)])}',
|
||||
f' channel_mode: {",".join([str(x) for x in flags_to_list(self.channel_mode, channel_modes)])}',
|
||||
f' block_length: {",".join([str(x) for x in flags_to_list(self.block_length, SBC_BLOCK_LENGTHS)])}',
|
||||
f' subbands: {",".join([str(x) for x in flags_to_list(self.subbands, SBC_SUBBANDS)])}',
|
||||
f' allocation_method: {",".join([str(x) for x in flags_to_list(self.allocation_method, allocation_methods)])}',
|
||||
f' minimum_bitpool_value: {self.minimum_bitpool_value}',
|
||||
f' maximum_bitpool_value: {self.maximum_bitpool_value}'
|
||||
')'
|
||||
])
|
||||
return '\n'.join(
|
||||
# pylint: disable=line-too-long
|
||||
[
|
||||
'SbcMediaCodecInformation(',
|
||||
f' sampling_frequency: {",".join([str(x) for x in flags_to_list(self.sampling_frequency, SBC_SAMPLING_FREQUENCIES)])}',
|
||||
f' channel_mode: {",".join([str(x) for x in flags_to_list(self.channel_mode, channel_modes)])}',
|
||||
f' block_length: {",".join([str(x) for x in flags_to_list(self.block_length, SBC_BLOCK_LENGTHS)])}',
|
||||
f' subbands: {",".join([str(x) for x in flags_to_list(self.subbands, SBC_SUBBANDS)])}',
|
||||
f' allocation_method: {",".join([str(x) for x in flags_to_list(self.allocation_method, allocation_methods)])}',
|
||||
f' minimum_bitpool_value: {self.minimum_bitpool_value}',
|
||||
f' maximum_bitpool_value: {self.maximum_bitpool_value}' ')',
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class AacMediaCodecInformation(
|
||||
namedtuple(
|
||||
'AacMediaCodecInformation',
|
||||
[
|
||||
'object_type',
|
||||
'sampling_frequency',
|
||||
'channels',
|
||||
'vbr',
|
||||
'bitrate'
|
||||
]
|
||||
['object_type', 'sampling_frequency', 'channels', 'rfa', 'vbr', 'bitrate'],
|
||||
)
|
||||
):
|
||||
'''
|
||||
A2DP spec - 4.5.2 Codec Specific Information Elements
|
||||
'''
|
||||
|
||||
BIT_FIELDS = 'u8u12u2p2u1u23'
|
||||
OBJECT_TYPE_BITS = {
|
||||
MPEG_2_AAC_LC_OBJECT_TYPE: 1 << 7,
|
||||
MPEG_4_AAC_LC_OBJECT_TYPE: 1 << 6,
|
||||
MPEG_4_AAC_LTP_OBJECT_TYPE: 1 << 5,
|
||||
MPEG_4_AAC_SCALABLE_OBJECT_TYPE: 1 << 4
|
||||
MPEG_2_AAC_LC_OBJECT_TYPE: 1 << 7,
|
||||
MPEG_4_AAC_LC_OBJECT_TYPE: 1 << 6,
|
||||
MPEG_4_AAC_LTP_OBJECT_TYPE: 1 << 5,
|
||||
MPEG_4_AAC_SCALABLE_OBJECT_TYPE: 1 << 4,
|
||||
}
|
||||
SAMPLING_FREQUENCY_BITS = {
|
||||
8000: 1 << 11,
|
||||
8000: 1 << 11,
|
||||
11025: 1 << 10,
|
||||
12000: 1 << 9,
|
||||
16000: 1 << 8,
|
||||
@@ -343,66 +395,82 @@ class AacMediaCodecInformation(
|
||||
48000: 1 << 3,
|
||||
64000: 1 << 2,
|
||||
88200: 1 << 1,
|
||||
96000: 1
|
||||
}
|
||||
CHANNELS_BITS = {
|
||||
1: 1 << 1,
|
||||
2: 1
|
||||
96000: 1,
|
||||
}
|
||||
CHANNELS_BITS = {1: 1 << 1, 2: 1}
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(data):
|
||||
return AacMediaCodecInformation(*bitstruct.unpack(AacMediaCodecInformation.BIT_FIELDS, data))
|
||||
def from_bytes(data: bytes) -> 'AacMediaCodecInformation':
|
||||
object_type = data[0]
|
||||
sampling_frequency = (data[1] << 4) | ((data[2] >> 4) & 0x0F)
|
||||
channels = (data[2] >> 2) & 0x03
|
||||
rfa = 0
|
||||
vbr = (data[3] >> 7) & 0x01
|
||||
bitrate = ((data[3] & 0x7F) << 16) | (data[4] << 8) | data[5]
|
||||
return AacMediaCodecInformation(
|
||||
object_type, sampling_frequency, channels, rfa, vbr, bitrate
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_discrete_values(
|
||||
cls,
|
||||
object_type,
|
||||
sampling_frequency,
|
||||
channels,
|
||||
vbr,
|
||||
bitrate
|
||||
cls, object_type, sampling_frequency, channels, vbr, bitrate
|
||||
):
|
||||
return AacMediaCodecInformation(
|
||||
object_type = cls.OBJECT_TYPE_BITS[object_type],
|
||||
sampling_frequency = cls.SAMPLING_FREQUENCY_BITS[sampling_frequency],
|
||||
channels = cls.CHANNELS_BITS[channels],
|
||||
vbr = vbr,
|
||||
bitrate = bitrate
|
||||
object_type=cls.OBJECT_TYPE_BITS[object_type],
|
||||
sampling_frequency=cls.SAMPLING_FREQUENCY_BITS[sampling_frequency],
|
||||
channels=cls.CHANNELS_BITS[channels],
|
||||
rfa=0,
|
||||
vbr=vbr,
|
||||
bitrate=bitrate,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_lists(
|
||||
cls,
|
||||
object_types,
|
||||
sampling_frequencies,
|
||||
channels,
|
||||
vbr,
|
||||
bitrate
|
||||
):
|
||||
def from_lists(cls, object_types, sampling_frequencies, channels, vbr, bitrate):
|
||||
return AacMediaCodecInformation(
|
||||
object_type = sum(cls.OBJECT_TYPE_BITS[x] for x in object_types),
|
||||
sampling_frequency = sum(cls.SAMPLING_FREQUENCY_BITS[x] for x in sampling_frequencies),
|
||||
channels = sum(cls.CHANNELS_BITS[x] for x in channels),
|
||||
vbr = vbr,
|
||||
bitrate = bitrate
|
||||
object_type=sum(cls.OBJECT_TYPE_BITS[x] for x in object_types),
|
||||
sampling_frequency=sum(
|
||||
cls.SAMPLING_FREQUENCY_BITS[x] for x in sampling_frequencies
|
||||
),
|
||||
channels=sum(cls.CHANNELS_BITS[x] for x in channels),
|
||||
vbr=vbr,
|
||||
bitrate=bitrate,
|
||||
)
|
||||
|
||||
def __bytes__(self):
|
||||
return bitstruct.pack(self.BIT_FIELDS, *self)
|
||||
def __bytes__(self) -> bytes:
|
||||
return bytes(
|
||||
[
|
||||
self.object_type & 0xFF,
|
||||
(self.sampling_frequency >> 4) & 0xFF,
|
||||
(((self.sampling_frequency & 0x0F) << 4) | (self.channels << 2)) & 0xFF,
|
||||
((self.vbr << 7) | ((self.bitrate >> 16) & 0x7F)) & 0xFF,
|
||||
((self.bitrate >> 8) & 0xFF) & 0xFF,
|
||||
self.bitrate & 0xFF,
|
||||
]
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
object_types = ['MPEG_2_AAC_LC', 'MPEG_4_AAC_LC', 'MPEG_4_AAC_LTP', 'MPEG_4_AAC_SCALABLE', '[4]', '[5]', '[6]', '[7]']
|
||||
object_types = [
|
||||
'MPEG_2_AAC_LC',
|
||||
'MPEG_4_AAC_LC',
|
||||
'MPEG_4_AAC_LTP',
|
||||
'MPEG_4_AAC_SCALABLE',
|
||||
'[4]',
|
||||
'[5]',
|
||||
'[6]',
|
||||
'[7]',
|
||||
]
|
||||
channels = [1, 2]
|
||||
return '\n'.join([
|
||||
'AacMediaCodecInformation(',
|
||||
f' object_type: {",".join([str(x) for x in flags_to_list(self.object_type, object_types)])}',
|
||||
f' sampling_frequency: {",".join([str(x) for x in flags_to_list(self.sampling_frequency, MPEG_2_4_AAC_SAMPLING_FREQUENCIES)])}',
|
||||
f' channels: {",".join([str(x) for x in flags_to_list(self.channels, channels)])}',
|
||||
f' vbr: {self.vbr}',
|
||||
f' bitrate: {self.bitrate}'
|
||||
')'
|
||||
])
|
||||
# pylint: disable=line-too-long
|
||||
return '\n'.join(
|
||||
[
|
||||
'AacMediaCodecInformation(',
|
||||
f' object_type: {",".join([str(x) for x in flags_to_list(self.object_type, object_types)])}',
|
||||
f' sampling_frequency: {",".join([str(x) for x in flags_to_list(self.sampling_frequency, MPEG_2_4_AAC_SAMPLING_FREQUENCIES)])}',
|
||||
f' channels: {",".join([str(x) for x in flags_to_list(self.channels, channels)])}',
|
||||
f' vbr: {self.vbr}',
|
||||
f' bitrate: {self.bitrate}' ')',
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -418,37 +486,34 @@ class VendorSpecificMediaCodecInformation:
|
||||
|
||||
def __init__(self, vendor_id, codec_id, value):
|
||||
self.vendor_id = vendor_id
|
||||
self.codec_id = codec_id
|
||||
self.value = value
|
||||
self.codec_id = codec_id
|
||||
self.value = value
|
||||
|
||||
def __bytes__(self):
|
||||
return struct.pack('<IH', self.vendor_id, self.codec_id, self.value)
|
||||
|
||||
def __str__(self):
|
||||
return '\n'.join([
|
||||
'VendorSpecificMediaCodecInformation(',
|
||||
f' vendor_id: {self.vendor_id:08X} ({name_or_number(COMPANY_IDENTIFIERS, self.vendor_id & 0xFFFF)})',
|
||||
f' codec_id: {self.codec_id:04X}',
|
||||
f' value: {self.value.hex()}'
|
||||
')'
|
||||
])
|
||||
# pylint: disable=line-too-long
|
||||
return '\n'.join(
|
||||
[
|
||||
'VendorSpecificMediaCodecInformation(',
|
||||
f' vendor_id: {self.vendor_id:08X} ({name_or_number(COMPANY_IDENTIFIERS, self.vendor_id & 0xFFFF)})',
|
||||
f' codec_id: {self.codec_id:04X}',
|
||||
f' value: {self.value.hex()}' ')',
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class SbcFrame:
|
||||
def __init__(
|
||||
self,
|
||||
sampling_frequency,
|
||||
block_count,
|
||||
channel_mode,
|
||||
subband_count,
|
||||
payload
|
||||
self, sampling_frequency, block_count, channel_mode, subband_count, payload
|
||||
):
|
||||
self.sampling_frequency = sampling_frequency
|
||||
self.block_count = block_count
|
||||
self.channel_mode = channel_mode
|
||||
self.subband_count = subband_count
|
||||
self.payload = payload
|
||||
self.block_count = block_count
|
||||
self.channel_mode = channel_mode
|
||||
self.subband_count = subband_count
|
||||
self.payload = payload
|
||||
|
||||
@property
|
||||
def sample_count(self):
|
||||
@@ -463,7 +528,13 @@ class SbcFrame:
|
||||
return self.sample_count / self.sampling_frequency
|
||||
|
||||
def __str__(self):
|
||||
return f'SBC(sf={self.sampling_frequency},cm={self.channel_mode},br={self.bitrate},sc={self.sample_count},size={len(self.payload)})'
|
||||
return (
|
||||
f'SBC(sf={self.sampling_frequency},'
|
||||
f'cm={self.channel_mode},'
|
||||
f'br={self.bitrate},'
|
||||
f'sc={self.sample_count},'
|
||||
f'size={len(self.payload)})'
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -487,24 +558,30 @@ class SbcParser:
|
||||
|
||||
# Extract some of the header fields
|
||||
sampling_frequency = SBC_SAMPLING_FREQUENCIES[(header[1] >> 6) & 3]
|
||||
blocks = 4 * (1 + ((header[1] >> 4) & 3))
|
||||
channel_mode = (header[1] >> 2) & 3
|
||||
channels = 1 if channel_mode == SBC_MONO_CHANNEL_MODE else 2
|
||||
subbands = 8 if ((header[1]) & 1) else 4
|
||||
bitpool = header[2]
|
||||
blocks = 4 * (1 + ((header[1] >> 4) & 3))
|
||||
channel_mode = (header[1] >> 2) & 3
|
||||
channels = 1 if channel_mode == SBC_MONO_CHANNEL_MODE else 2
|
||||
subbands = 8 if ((header[1]) & 1) else 4
|
||||
bitpool = header[2]
|
||||
|
||||
# Compute the frame length
|
||||
frame_length = 4 + (4 * subbands * channels) // 8
|
||||
if channel_mode in (SBC_MONO_CHANNEL_MODE, SBC_DUAL_CHANNEL_MODE):
|
||||
frame_length += (blocks * channels * bitpool) // 8
|
||||
else:
|
||||
frame_length += ((1 if channel_mode == SBC_JOINT_STEREO_CHANNEL_MODE else 0) * subbands + blocks * bitpool) // 8
|
||||
frame_length += (
|
||||
(1 if channel_mode == SBC_JOINT_STEREO_CHANNEL_MODE else 0)
|
||||
* subbands
|
||||
+ blocks * bitpool
|
||||
) // 8
|
||||
|
||||
# Read the rest of the frame
|
||||
payload = header + await self.read(frame_length - 4)
|
||||
|
||||
# Emit the next frame
|
||||
yield SbcFrame(sampling_frequency, blocks, channel_mode, subbands, payload)
|
||||
yield SbcFrame(
|
||||
sampling_frequency, blocks, channel_mode, subbands, payload
|
||||
)
|
||||
|
||||
return generate_frames()
|
||||
|
||||
@@ -512,19 +589,20 @@ class SbcParser:
|
||||
# -----------------------------------------------------------------------------
|
||||
class SbcPacketSource:
|
||||
def __init__(self, read, mtu, codec_capabilities):
|
||||
self.read = read
|
||||
self.mtu = mtu
|
||||
self.read = read
|
||||
self.mtu = mtu
|
||||
self.codec_capabilities = codec_capabilities
|
||||
|
||||
@property
|
||||
def packets(self):
|
||||
async def generate_packets():
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from .avdtp import MediaPacket # Import here to avoid a circular reference
|
||||
|
||||
sequence_number = 0
|
||||
timestamp = 0
|
||||
frames = []
|
||||
frames_size = 0
|
||||
timestamp = 0
|
||||
frames = []
|
||||
frames_size = 0
|
||||
max_rtp_payload = self.mtu - 12 - 1
|
||||
|
||||
# NOTE: this doesn't support frame fragments
|
||||
@@ -532,18 +610,25 @@ class SbcPacketSource:
|
||||
async for frame in sbc_parser.frames:
|
||||
print(frame)
|
||||
|
||||
if frames_size + len(frame.payload) > max_rtp_payload or len(frames) == 16:
|
||||
if (
|
||||
frames_size + len(frame.payload) > max_rtp_payload
|
||||
or len(frames) == 16
|
||||
):
|
||||
# Need to flush what has been accumulated so far
|
||||
|
||||
# Emit a packet
|
||||
sbc_payload = bytes([len(frames)]) + b''.join([frame.payload for frame in frames])
|
||||
packet = MediaPacket(2, 0, 0, 0, sequence_number, timestamp, 0, [], 96, sbc_payload)
|
||||
sbc_payload = bytes([len(frames)]) + b''.join(
|
||||
[frame.payload for frame in frames]
|
||||
)
|
||||
packet = MediaPacket(
|
||||
2, 0, 0, 0, sequence_number, timestamp, 0, [], 96, sbc_payload
|
||||
)
|
||||
packet.timestamp_seconds = timestamp / frame.sampling_frequency
|
||||
yield packet
|
||||
|
||||
# Prepare for next packets
|
||||
sequence_number += 1
|
||||
timestamp += sum([frame.sample_count for frame in frames])
|
||||
timestamp += sum((frame.sample_count for frame in frames))
|
||||
frames = [frame]
|
||||
frames_size = len(frame.payload)
|
||||
else:
|
||||
|
||||
442
bumble/att.py
442
bumble/att.py
@@ -22,15 +22,25 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from colors import color
|
||||
from __future__ import annotations
|
||||
import functools
|
||||
import struct
|
||||
from pyee import EventEmitter
|
||||
from typing import Dict, Type, TYPE_CHECKING
|
||||
|
||||
from .core import *
|
||||
from .hci import *
|
||||
from bumble.core import UUID, name_or_number, get_dict_key_by_value, ProtocolError
|
||||
from bumble.hci import HCI_Object, key_with_value, HCI_Constant
|
||||
from bumble.colors import color
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bumble.device import Connection
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
# fmt: off
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
ATT_CID = 0x04
|
||||
|
||||
ATT_ERROR_RESPONSE = 0x01
|
||||
@@ -163,30 +173,30 @@ ATT_ERROR_NAMES = {
|
||||
ATT_DEFAULT_MTU = 23
|
||||
|
||||
HANDLE_FIELD_SPEC = {'size': 2, 'mapper': lambda x: f'0x{x:04X}'}
|
||||
UUID_2_16_FIELD_SPEC = lambda x, y: UUID.parse_uuid(x, y) # noqa: E731
|
||||
# pylint: disable-next=unnecessary-lambda-assignment,unnecessary-lambda
|
||||
UUID_2_16_FIELD_SPEC = lambda x, y: UUID.parse_uuid(x, y)
|
||||
# pylint: disable-next=unnecessary-lambda-assignment,unnecessary-lambda
|
||||
UUID_2_FIELD_SPEC = lambda x, y: UUID.parse_uuid_2(x, y) # noqa: E731
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Utils
|
||||
# -----------------------------------------------------------------------------
|
||||
def key_with_value(dictionary, target_value):
|
||||
for key, value in dictionary.items():
|
||||
if value == target_value:
|
||||
return key
|
||||
return None
|
||||
|
||||
# fmt: on
|
||||
# pylint: enable=line-too-long
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Exceptions
|
||||
# -----------------------------------------------------------------------------
|
||||
class ATT_Error(Exception):
|
||||
def __init__(self, error_code, att_handle=0x0000):
|
||||
self.error_code = error_code
|
||||
class ATT_Error(ProtocolError):
|
||||
def __init__(self, error_code, att_handle=0x0000, message=''):
|
||||
super().__init__(
|
||||
error_code,
|
||||
error_namespace='att',
|
||||
error_name=ATT_PDU.error_name(error_code),
|
||||
)
|
||||
self.att_handle = att_handle
|
||||
self.message = message
|
||||
|
||||
def __str__(self):
|
||||
return f'ATT_Error({ATT_PDU.error_name(self.error_code)})'
|
||||
return f'ATT_Error(error={self.error_name}, handle={self.att_handle:04X}): {self.message}'
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -196,8 +206,10 @@ class ATT_PDU:
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.3 ATTRIBUTE PDU
|
||||
'''
|
||||
pdu_classes = {}
|
||||
|
||||
pdu_classes: Dict[int, Type[ATT_PDU]] = {}
|
||||
op_code = 0
|
||||
name = None
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(pdu):
|
||||
@@ -274,11 +286,13 @@ class ATT_PDU:
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('request_opcode_in_error', {'size': 1, 'mapper': ATT_PDU.pdu_name}),
|
||||
('attribute_handle_in_error', HANDLE_FIELD_SPEC),
|
||||
('error_code', {'size': 1, 'mapper': ATT_PDU.error_name})
|
||||
])
|
||||
@ATT_PDU.subclass(
|
||||
[
|
||||
('request_opcode_in_error', {'size': 1, 'mapper': ATT_PDU.pdu_name}),
|
||||
('attribute_handle_in_error', HANDLE_FIELD_SPEC),
|
||||
('error_code', {'size': 1, 'mapper': ATT_PDU.error_name}),
|
||||
]
|
||||
)
|
||||
class ATT_Error_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.1.1 Error Response
|
||||
@@ -286,9 +300,7 @@ class ATT_Error_Response(ATT_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('client_rx_mtu', 2)
|
||||
])
|
||||
@ATT_PDU.subclass([('client_rx_mtu', 2)])
|
||||
class ATT_Exchange_MTU_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.2.1 Exchange MTU Request
|
||||
@@ -296,9 +308,7 @@ class ATT_Exchange_MTU_Request(ATT_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('server_rx_mtu', 2)
|
||||
])
|
||||
@ATT_PDU.subclass([('server_rx_mtu', 2)])
|
||||
class ATT_Exchange_MTU_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.2.2 Exchange MTU Response
|
||||
@@ -306,10 +316,9 @@ class ATT_Exchange_MTU_Response(ATT_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('starting_handle', HANDLE_FIELD_SPEC),
|
||||
('ending_handle', HANDLE_FIELD_SPEC)
|
||||
])
|
||||
@ATT_PDU.subclass(
|
||||
[('starting_handle', HANDLE_FIELD_SPEC), ('ending_handle', HANDLE_FIELD_SPEC)]
|
||||
)
|
||||
class ATT_Find_Information_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.3.1 Find Information Request
|
||||
@@ -317,10 +326,7 @@ class ATT_Find_Information_Request(ATT_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('format', 1),
|
||||
('information_data', '*')
|
||||
])
|
||||
@ATT_PDU.subclass([('format', 1), ('information_data', '*')])
|
||||
class ATT_Find_Information_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.3.2 Find Information Response
|
||||
@@ -332,7 +338,7 @@ class ATT_Find_Information_Response(ATT_PDU):
|
||||
uuid_size = 2 if self.format == 1 else 16
|
||||
while offset + uuid_size <= len(self.information_data):
|
||||
handle = struct.unpack_from('<H', self.information_data, offset)[0]
|
||||
uuid = self.information_data[2 + offset:2 + offset + uuid_size]
|
||||
uuid = self.information_data[2 + offset : 2 + offset + uuid_size]
|
||||
self.information.append((handle, uuid))
|
||||
offset += 2 + uuid_size
|
||||
|
||||
@@ -346,20 +352,33 @@ class ATT_Find_Information_Response(ATT_PDU):
|
||||
|
||||
def __str__(self):
|
||||
result = color(self.name, 'yellow')
|
||||
result += ':\n' + HCI_Object.format_fields(self.__dict__, [
|
||||
('format', 1),
|
||||
('information', {'mapper': lambda x: ', '.join([f'0x{handle:04X}:{uuid.hex()}' for handle, uuid in x])})
|
||||
], ' ')
|
||||
result += ':\n' + HCI_Object.format_fields(
|
||||
self.__dict__,
|
||||
[
|
||||
('format', 1),
|
||||
(
|
||||
'information',
|
||||
{
|
||||
'mapper': lambda x: ', '.join(
|
||||
[f'0x{handle:04X}:{uuid.hex()}' for handle, uuid in x]
|
||||
)
|
||||
},
|
||||
),
|
||||
],
|
||||
' ',
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('starting_handle', HANDLE_FIELD_SPEC),
|
||||
('ending_handle', HANDLE_FIELD_SPEC),
|
||||
('attribute_type', UUID_2_FIELD_SPEC),
|
||||
('attribute_value', '*')
|
||||
])
|
||||
@ATT_PDU.subclass(
|
||||
[
|
||||
('starting_handle', HANDLE_FIELD_SPEC),
|
||||
('ending_handle', HANDLE_FIELD_SPEC),
|
||||
('attribute_type', UUID_2_FIELD_SPEC),
|
||||
('attribute_value', '*'),
|
||||
]
|
||||
)
|
||||
class ATT_Find_By_Type_Value_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.3.3 Find By Type Value Request
|
||||
@@ -367,9 +386,7 @@ class ATT_Find_By_Type_Value_Request(ATT_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('handles_information_list', '*')
|
||||
])
|
||||
@ATT_PDU.subclass([('handles_information_list', '*')])
|
||||
class ATT_Find_By_Type_Value_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.3.4 Find By Type Value Response
|
||||
@@ -379,7 +396,9 @@ class ATT_Find_By_Type_Value_Response(ATT_PDU):
|
||||
self.handles_information = []
|
||||
offset = 0
|
||||
while offset + 4 <= len(self.handles_information_list):
|
||||
found_attribute_handle, group_end_handle = struct.unpack_from('<HH', self.handles_information_list, offset)
|
||||
found_attribute_handle, group_end_handle = struct.unpack_from(
|
||||
'<HH', self.handles_information_list, offset
|
||||
)
|
||||
self.handles_information.append((found_attribute_handle, group_end_handle))
|
||||
offset += 4
|
||||
|
||||
@@ -393,18 +412,34 @@ class ATT_Find_By_Type_Value_Response(ATT_PDU):
|
||||
|
||||
def __str__(self):
|
||||
result = color(self.name, 'yellow')
|
||||
result += ':\n' + HCI_Object.format_fields(self.__dict__, [
|
||||
('handles_information', {'mapper': lambda x: ', '.join([f'0x{handle1:04X}-0x{handle2:04X}' for handle1, handle2 in x])})
|
||||
], ' ')
|
||||
result += ':\n' + HCI_Object.format_fields(
|
||||
self.__dict__,
|
||||
[
|
||||
(
|
||||
'handles_information',
|
||||
{
|
||||
'mapper': lambda x: ', '.join(
|
||||
[
|
||||
f'0x{handle1:04X}-0x{handle2:04X}'
|
||||
for handle1, handle2 in x
|
||||
]
|
||||
)
|
||||
},
|
||||
)
|
||||
],
|
||||
' ',
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('starting_handle', HANDLE_FIELD_SPEC),
|
||||
('ending_handle', HANDLE_FIELD_SPEC),
|
||||
('attribute_type', UUID_2_16_FIELD_SPEC)
|
||||
])
|
||||
@ATT_PDU.subclass(
|
||||
[
|
||||
('starting_handle', HANDLE_FIELD_SPEC),
|
||||
('ending_handle', HANDLE_FIELD_SPEC),
|
||||
('attribute_type', UUID_2_16_FIELD_SPEC),
|
||||
]
|
||||
)
|
||||
class ATT_Read_By_Type_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.1 Read By Type Request
|
||||
@@ -412,10 +447,7 @@ class ATT_Read_By_Type_Request(ATT_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('length', 1),
|
||||
('attribute_data_list', '*')
|
||||
])
|
||||
@ATT_PDU.subclass([('length', 1), ('attribute_data_list', '*')])
|
||||
class ATT_Read_By_Type_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.2 Read By Type Response
|
||||
@@ -424,9 +456,15 @@ class ATT_Read_By_Type_Response(ATT_PDU):
|
||||
def parse_attribute_data_list(self):
|
||||
self.attributes = []
|
||||
offset = 0
|
||||
while self.length != 0 and offset + self.length <= len(self.attribute_data_list):
|
||||
attribute_handle, = struct.unpack_from('<H', self.attribute_data_list, offset)
|
||||
attribute_value = self.attribute_data_list[offset + 2:offset + self.length]
|
||||
while self.length != 0 and offset + self.length <= len(
|
||||
self.attribute_data_list
|
||||
):
|
||||
(attribute_handle,) = struct.unpack_from(
|
||||
'<H', self.attribute_data_list, offset
|
||||
)
|
||||
attribute_value = self.attribute_data_list[
|
||||
offset + 2 : offset + self.length
|
||||
]
|
||||
self.attributes.append((attribute_handle, attribute_value))
|
||||
offset += self.length
|
||||
|
||||
@@ -440,17 +478,26 @@ class ATT_Read_By_Type_Response(ATT_PDU):
|
||||
|
||||
def __str__(self):
|
||||
result = color(self.name, 'yellow')
|
||||
result += ':\n' + HCI_Object.format_fields(self.__dict__, [
|
||||
('length', 1),
|
||||
('attributes', {'mapper': lambda x: ', '.join([f'0x{handle:04X}:{value.hex()}' for handle, value in x])})
|
||||
], ' ')
|
||||
result += ':\n' + HCI_Object.format_fields(
|
||||
self.__dict__,
|
||||
[
|
||||
('length', 1),
|
||||
(
|
||||
'attributes',
|
||||
{
|
||||
'mapper': lambda x: ', '.join(
|
||||
[f'0x{handle:04X}:{value.hex()}' for handle, value in x]
|
||||
)
|
||||
},
|
||||
),
|
||||
],
|
||||
' ',
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('attribute_handle', HANDLE_FIELD_SPEC)
|
||||
])
|
||||
@ATT_PDU.subclass([('attribute_handle', HANDLE_FIELD_SPEC)])
|
||||
class ATT_Read_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.3 Read Request
|
||||
@@ -458,9 +505,7 @@ class ATT_Read_Request(ATT_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('attribute_value', '*')
|
||||
])
|
||||
@ATT_PDU.subclass([('attribute_value', '*')])
|
||||
class ATT_Read_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.4 Read Response
|
||||
@@ -468,10 +513,7 @@ class ATT_Read_Response(ATT_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('attribute_handle', HANDLE_FIELD_SPEC),
|
||||
('value_offset', 2)
|
||||
])
|
||||
@ATT_PDU.subclass([('attribute_handle', HANDLE_FIELD_SPEC), ('value_offset', 2)])
|
||||
class ATT_Read_Blob_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.5 Read Blob Request
|
||||
@@ -479,9 +521,7 @@ class ATT_Read_Blob_Request(ATT_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('part_attribute_value', '*')
|
||||
])
|
||||
@ATT_PDU.subclass([('part_attribute_value', '*')])
|
||||
class ATT_Read_Blob_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.6 Read Blob Response
|
||||
@@ -489,9 +529,7 @@ class ATT_Read_Blob_Response(ATT_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('set_of_handles', '*')
|
||||
])
|
||||
@ATT_PDU.subclass([('set_of_handles', '*')])
|
||||
class ATT_Read_Multiple_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.7 Read Multiple Request
|
||||
@@ -499,9 +537,7 @@ class ATT_Read_Multiple_Request(ATT_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('set_of_values', '*')
|
||||
])
|
||||
@ATT_PDU.subclass([('set_of_values', '*')])
|
||||
class ATT_Read_Multiple_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.8 Read Multiple Response
|
||||
@@ -509,11 +545,13 @@ class ATT_Read_Multiple_Response(ATT_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('starting_handle', HANDLE_FIELD_SPEC),
|
||||
('ending_handle', HANDLE_FIELD_SPEC),
|
||||
('attribute_group_type', UUID_2_16_FIELD_SPEC)
|
||||
])
|
||||
@ATT_PDU.subclass(
|
||||
[
|
||||
('starting_handle', HANDLE_FIELD_SPEC),
|
||||
('ending_handle', HANDLE_FIELD_SPEC),
|
||||
('attribute_group_type', UUID_2_16_FIELD_SPEC),
|
||||
]
|
||||
)
|
||||
class ATT_Read_By_Group_Type_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.9 Read by Group Type Request
|
||||
@@ -521,10 +559,7 @@ class ATT_Read_By_Group_Type_Request(ATT_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('length', 1),
|
||||
('attribute_data_list', '*')
|
||||
])
|
||||
@ATT_PDU.subclass([('length', 1), ('attribute_data_list', '*')])
|
||||
class ATT_Read_By_Group_Type_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.10 Read by Group Type Response
|
||||
@@ -533,10 +568,18 @@ class ATT_Read_By_Group_Type_Response(ATT_PDU):
|
||||
def parse_attribute_data_list(self):
|
||||
self.attributes = []
|
||||
offset = 0
|
||||
while self.length != 0 and offset + self.length <= len(self.attribute_data_list):
|
||||
attribute_handle, end_group_handle = struct.unpack_from('<HH', self.attribute_data_list, offset)
|
||||
attribute_value = self.attribute_data_list[offset + 4:offset + self.length]
|
||||
self.attributes.append((attribute_handle, end_group_handle, attribute_value))
|
||||
while self.length != 0 and offset + self.length <= len(
|
||||
self.attribute_data_list
|
||||
):
|
||||
attribute_handle, end_group_handle = struct.unpack_from(
|
||||
'<HH', self.attribute_data_list, offset
|
||||
)
|
||||
attribute_value = self.attribute_data_list[
|
||||
offset + 4 : offset + self.length
|
||||
]
|
||||
self.attributes.append(
|
||||
(attribute_handle, end_group_handle, attribute_value)
|
||||
)
|
||||
offset += self.length
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -549,18 +592,29 @@ class ATT_Read_By_Group_Type_Response(ATT_PDU):
|
||||
|
||||
def __str__(self):
|
||||
result = color(self.name, 'yellow')
|
||||
result += ':\n' + HCI_Object.format_fields(self.__dict__, [
|
||||
('length', 1),
|
||||
('attributes', {'mapper': lambda x: ', '.join([f'0x{handle:04X}-0x{end:04X}:{value.hex()}' for handle, end, value in x])})
|
||||
], ' ')
|
||||
result += ':\n' + HCI_Object.format_fields(
|
||||
self.__dict__,
|
||||
[
|
||||
('length', 1),
|
||||
(
|
||||
'attributes',
|
||||
{
|
||||
'mapper': lambda x: ', '.join(
|
||||
[
|
||||
f'0x{handle:04X}-0x{end:04X}:{value.hex()}'
|
||||
for handle, end, value in x
|
||||
]
|
||||
)
|
||||
},
|
||||
),
|
||||
],
|
||||
' ',
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('attribute_handle', HANDLE_FIELD_SPEC),
|
||||
('attribute_value', '*')
|
||||
])
|
||||
@ATT_PDU.subclass([('attribute_handle', HANDLE_FIELD_SPEC), ('attribute_value', '*')])
|
||||
class ATT_Write_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.5.1 Write Request
|
||||
@@ -576,10 +630,7 @@ class ATT_Write_Response(ATT_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('attribute_handle', HANDLE_FIELD_SPEC),
|
||||
('attribute_value', '*')
|
||||
])
|
||||
@ATT_PDU.subclass([('attribute_handle', HANDLE_FIELD_SPEC), ('attribute_value', '*')])
|
||||
class ATT_Write_Command(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.5.3 Write Command
|
||||
@@ -587,11 +638,13 @@ class ATT_Write_Command(ATT_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('attribute_handle', HANDLE_FIELD_SPEC),
|
||||
('attribute_value', '*')
|
||||
# ('authentication_signature', 'TODO')
|
||||
])
|
||||
@ATT_PDU.subclass(
|
||||
[
|
||||
('attribute_handle', HANDLE_FIELD_SPEC),
|
||||
('attribute_value', '*')
|
||||
# ('authentication_signature', 'TODO')
|
||||
]
|
||||
)
|
||||
class ATT_Signed_Write_Command(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.5.4 Signed Write Command
|
||||
@@ -599,11 +652,13 @@ class ATT_Signed_Write_Command(ATT_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('attribute_handle', HANDLE_FIELD_SPEC),
|
||||
('value_offset', 2),
|
||||
('part_attribute_value', '*')
|
||||
])
|
||||
@ATT_PDU.subclass(
|
||||
[
|
||||
('attribute_handle', HANDLE_FIELD_SPEC),
|
||||
('value_offset', 2),
|
||||
('part_attribute_value', '*'),
|
||||
]
|
||||
)
|
||||
class ATT_Prepare_Write_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.6.1 Prepare Write Request
|
||||
@@ -611,11 +666,13 @@ class ATT_Prepare_Write_Request(ATT_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('attribute_handle', HANDLE_FIELD_SPEC),
|
||||
('value_offset', 2),
|
||||
('part_attribute_value', '*')
|
||||
])
|
||||
@ATT_PDU.subclass(
|
||||
[
|
||||
('attribute_handle', HANDLE_FIELD_SPEC),
|
||||
('value_offset', 2),
|
||||
('part_attribute_value', '*'),
|
||||
]
|
||||
)
|
||||
class ATT_Prepare_Write_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.6.2 Prepare Write Response
|
||||
@@ -639,10 +696,7 @@ class ATT_Execute_Write_Response(ATT_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('attribute_handle', HANDLE_FIELD_SPEC),
|
||||
('attribute_value', '*')
|
||||
])
|
||||
@ATT_PDU.subclass([('attribute_handle', HANDLE_FIELD_SPEC), ('attribute_value', '*')])
|
||||
class ATT_Handle_Value_Notification(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.7.1 Handle Value Notification
|
||||
@@ -650,10 +704,7 @@ class ATT_Handle_Value_Notification(ATT_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('attribute_handle', HANDLE_FIELD_SPEC),
|
||||
('attribute_value', '*')
|
||||
])
|
||||
@ATT_PDU.subclass([('attribute_handle', HANDLE_FIELD_SPEC), ('attribute_value', '*')])
|
||||
class ATT_Handle_Value_Indication(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.7.2 Handle Value Indication
|
||||
@@ -671,31 +722,58 @@ class ATT_Handle_Value_Confirmation(ATT_PDU):
|
||||
# -----------------------------------------------------------------------------
|
||||
class Attribute(EventEmitter):
|
||||
# Permission flags
|
||||
READABLE = 0x01
|
||||
WRITEABLE = 0x02
|
||||
READ_REQUIRES_ENCRYPTION = 0x04
|
||||
WRITE_REQUIRES_ENCRYPTION = 0x08
|
||||
READ_REQUIRES_AUTHENTICATION = 0x10
|
||||
READABLE = 0x01
|
||||
WRITEABLE = 0x02
|
||||
READ_REQUIRES_ENCRYPTION = 0x04
|
||||
WRITE_REQUIRES_ENCRYPTION = 0x08
|
||||
READ_REQUIRES_AUTHENTICATION = 0x10
|
||||
WRITE_REQUIRES_AUTHENTICATION = 0x20
|
||||
READ_REQUIRES_AUTHORIZATION = 0x40
|
||||
WRITE_REQUIRES_AUTHORIZATION = 0x80
|
||||
READ_REQUIRES_AUTHORIZATION = 0x40
|
||||
WRITE_REQUIRES_AUTHORIZATION = 0x80
|
||||
|
||||
def __init__(self, attribute_type, permissions, value = b''):
|
||||
PERMISSION_NAMES = {
|
||||
READABLE: 'READABLE',
|
||||
WRITEABLE: 'WRITEABLE',
|
||||
READ_REQUIRES_ENCRYPTION: 'READ_REQUIRES_ENCRYPTION',
|
||||
WRITE_REQUIRES_ENCRYPTION: 'WRITE_REQUIRES_ENCRYPTION',
|
||||
READ_REQUIRES_AUTHENTICATION: 'READ_REQUIRES_AUTHENTICATION',
|
||||
WRITE_REQUIRES_AUTHENTICATION: 'WRITE_REQUIRES_AUTHENTICATION',
|
||||
READ_REQUIRES_AUTHORIZATION: 'READ_REQUIRES_AUTHORIZATION',
|
||||
WRITE_REQUIRES_AUTHORIZATION: 'WRITE_REQUIRES_AUTHORIZATION',
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def string_to_permissions(permissions_str: str):
|
||||
try:
|
||||
return functools.reduce(
|
||||
lambda x, y: x | get_dict_key_by_value(Attribute.PERMISSION_NAMES, y),
|
||||
permissions_str.split(","),
|
||||
0,
|
||||
)
|
||||
except TypeError as exc:
|
||||
raise TypeError(
|
||||
f"Attribute::permissions error:\nExpected a string containing any of the keys, separated by commas: {','.join(Attribute.PERMISSION_NAMES.values())}\nGot: {permissions_str}"
|
||||
) from exc
|
||||
|
||||
def __init__(self, attribute_type, permissions, value=b''):
|
||||
EventEmitter.__init__(self)
|
||||
self.handle = 0
|
||||
self.handle = 0
|
||||
self.end_group_handle = 0
|
||||
self.permissions = permissions
|
||||
if isinstance(permissions, str):
|
||||
self.permissions = self.string_to_permissions(permissions)
|
||||
else:
|
||||
self.permissions = permissions
|
||||
|
||||
# Convert the type to a UUID object if it isn't already
|
||||
if type(attribute_type) is str:
|
||||
if isinstance(attribute_type, str):
|
||||
self.type = UUID(attribute_type)
|
||||
elif type(attribute_type) is bytes:
|
||||
elif isinstance(attribute_type, bytes):
|
||||
self.type = UUID.from_bytes(attribute_type)
|
||||
else:
|
||||
self.type = attribute_type
|
||||
|
||||
# Convert the value to a byte array
|
||||
if type(value) is str:
|
||||
if isinstance(value, str):
|
||||
self.value = bytes(value, 'utf-8')
|
||||
else:
|
||||
self.value = value
|
||||
@@ -706,32 +784,72 @@ class Attribute(EventEmitter):
|
||||
def decode_value(self, value_bytes):
|
||||
return value_bytes
|
||||
|
||||
def read_value(self, connection):
|
||||
def read_value(self, connection: Connection):
|
||||
if (
|
||||
self.permissions & self.READ_REQUIRES_ENCRYPTION
|
||||
) and not connection.encryption:
|
||||
raise ATT_Error(
|
||||
error_code=ATT_INSUFFICIENT_ENCRYPTION_ERROR, att_handle=self.handle
|
||||
)
|
||||
if (
|
||||
self.permissions & self.READ_REQUIRES_AUTHENTICATION
|
||||
) and not connection.authenticated:
|
||||
raise ATT_Error(
|
||||
error_code=ATT_INSUFFICIENT_AUTHENTICATION_ERROR, att_handle=self.handle
|
||||
)
|
||||
if self.permissions & self.READ_REQUIRES_AUTHORIZATION:
|
||||
# TODO: handle authorization better
|
||||
raise ATT_Error(
|
||||
error_code=ATT_INSUFFICIENT_AUTHORIZATION_ERROR, att_handle=self.handle
|
||||
)
|
||||
|
||||
if read := getattr(self.value, 'read', None):
|
||||
try:
|
||||
value = read(connection)
|
||||
value = read(connection) # pylint: disable=not-callable
|
||||
except ATT_Error as error:
|
||||
raise ATT_Error(error_code=error.error_code, att_handle=self.handle)
|
||||
raise ATT_Error(
|
||||
error_code=error.error_code, att_handle=self.handle
|
||||
) from error
|
||||
else:
|
||||
value = self.value
|
||||
|
||||
return self.encode_value(value)
|
||||
|
||||
def write_value(self, connection, value_bytes):
|
||||
def write_value(self, connection: Connection, value_bytes):
|
||||
if (
|
||||
self.permissions & self.WRITE_REQUIRES_ENCRYPTION
|
||||
) and not connection.encryption:
|
||||
raise ATT_Error(
|
||||
error_code=ATT_INSUFFICIENT_ENCRYPTION_ERROR, att_handle=self.handle
|
||||
)
|
||||
if (
|
||||
self.permissions & self.WRITE_REQUIRES_AUTHENTICATION
|
||||
) and not connection.authenticated:
|
||||
raise ATT_Error(
|
||||
error_code=ATT_INSUFFICIENT_AUTHENTICATION_ERROR, att_handle=self.handle
|
||||
)
|
||||
if self.permissions & self.WRITE_REQUIRES_AUTHORIZATION:
|
||||
# TODO: handle authorization better
|
||||
raise ATT_Error(
|
||||
error_code=ATT_INSUFFICIENT_AUTHORIZATION_ERROR, att_handle=self.handle
|
||||
)
|
||||
|
||||
value = self.decode_value(value_bytes)
|
||||
|
||||
if write := getattr(self.value, 'write', None):
|
||||
try:
|
||||
write(connection, value)
|
||||
write(connection, value) # pylint: disable=not-callable
|
||||
except ATT_Error as error:
|
||||
raise ATT_Error(error_code=error.error_code, att_handle=self.handle)
|
||||
raise ATT_Error(
|
||||
error_code=error.error_code, att_handle=self.handle
|
||||
) from error
|
||||
else:
|
||||
self.value = value
|
||||
|
||||
self.emit('write', connection, value)
|
||||
|
||||
def __repr__(self):
|
||||
if type(self.value) is bytes:
|
||||
if isinstance(self.value, bytes):
|
||||
value_str = self.value.hex()
|
||||
else:
|
||||
value_str = str(self.value)
|
||||
@@ -739,4 +857,8 @@ class Attribute(EventEmitter):
|
||||
value_string = f', value={self.value.hex()}'
|
||||
else:
|
||||
value_string = ''
|
||||
return f'Attribute(handle=0x{self.handle:04X}, type={self.type}, permissions={self.permissions}{value_string})'
|
||||
return (
|
||||
f'Attribute(handle=0x{self.handle:04X}, '
|
||||
f'type={self.type}, '
|
||||
f'permissions={self.permissions}{value_string})'
|
||||
)
|
||||
|
||||
648
bumble/avdtp.py
648
bumble/avdtp.py
File diff suppressed because it is too large
Load Diff
@@ -30,10 +30,10 @@ logger = logging.getLogger(__name__)
|
||||
class HCI_Bridge:
|
||||
class Forwarder:
|
||||
def __init__(self, hci_sink, sender_hci_sink, packet_filter, trace):
|
||||
self.hci_sink = hci_sink
|
||||
self.hci_sink = hci_sink
|
||||
self.sender_hci_sink = sender_hci_sink
|
||||
self.packet_filter = packet_filter
|
||||
self.trace = trace
|
||||
self.packet_filter = packet_filter
|
||||
self.trace = trace
|
||||
|
||||
def on_packet(self, packet):
|
||||
# Convert the packet bytes to an object
|
||||
@@ -61,15 +61,15 @@ class HCI_Bridge:
|
||||
hci_host_sink,
|
||||
hci_controller_source,
|
||||
hci_controller_sink,
|
||||
host_to_controller_filter = None,
|
||||
controller_to_host_filter = None
|
||||
host_to_controller_filter=None,
|
||||
controller_to_host_filter=None,
|
||||
):
|
||||
tracer = PacketTracer(emit_message=logger.info)
|
||||
host_to_controller_forwarder = HCI_Bridge.Forwarder(
|
||||
hci_controller_sink,
|
||||
hci_host_sink,
|
||||
host_to_controller_filter,
|
||||
lambda packet: tracer.trace(packet, 0)
|
||||
lambda packet: tracer.trace(packet, 0),
|
||||
)
|
||||
hci_host_source.set_packet_sink(host_to_controller_forwarder)
|
||||
|
||||
@@ -77,6 +77,6 @@ class HCI_Bridge:
|
||||
hci_host_sink,
|
||||
hci_controller_sink,
|
||||
controller_to_host_filter,
|
||||
lambda packet: tracer.trace(packet, 1)
|
||||
lambda packet: tracer.trace(packet, 1),
|
||||
)
|
||||
hci_controller_source.set_packet_sink(controller_to_host_forwarder)
|
||||
|
||||
103
bumble/colors.py
Normal file
103
bumble/colors.py
Normal file
@@ -0,0 +1,103 @@
|
||||
# Copyright (c) 2012 Giorgos Verigakis <verigak@gmail.com>
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software for any
|
||||
# purpose with or without fee is hereby granted, provided that the above
|
||||
# copyright notice and this permission notice appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
from functools import partial
|
||||
from typing import List, Optional, Union
|
||||
|
||||
|
||||
# ANSI color names. There is also a "default"
|
||||
COLORS = ('black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white')
|
||||
|
||||
# ANSI style names
|
||||
STYLES = (
|
||||
'none',
|
||||
'bold',
|
||||
'faint',
|
||||
'italic',
|
||||
'underline',
|
||||
'blink',
|
||||
'blink2',
|
||||
'negative',
|
||||
'concealed',
|
||||
'crossed',
|
||||
)
|
||||
|
||||
|
||||
ColorSpec = Union[str, int]
|
||||
|
||||
|
||||
def _join(*values: ColorSpec) -> str:
|
||||
return ';'.join(str(v) for v in values)
|
||||
|
||||
|
||||
def _color_code(spec: ColorSpec, base: int) -> str:
|
||||
if isinstance(spec, str):
|
||||
spec = spec.strip().lower()
|
||||
|
||||
if spec == 'default':
|
||||
return _join(base + 9)
|
||||
elif spec in COLORS:
|
||||
return _join(base + COLORS.index(spec))
|
||||
elif isinstance(spec, int) and 0 <= spec <= 255:
|
||||
return _join(base + 8, 5, spec)
|
||||
else:
|
||||
raise ValueError('Invalid color spec "%s"' % spec)
|
||||
|
||||
|
||||
def color(
|
||||
s: str,
|
||||
fg: Optional[ColorSpec] = None,
|
||||
bg: Optional[ColorSpec] = None,
|
||||
style: Optional[str] = None,
|
||||
) -> str:
|
||||
codes: List[ColorSpec] = []
|
||||
|
||||
if fg:
|
||||
codes.append(_color_code(fg, 30))
|
||||
if bg:
|
||||
codes.append(_color_code(bg, 40))
|
||||
if style:
|
||||
for style_part in style.split('+'):
|
||||
if style_part in STYLES:
|
||||
codes.append(STYLES.index(style_part))
|
||||
else:
|
||||
raise ValueError('Invalid style "%s"' % style_part)
|
||||
|
||||
if codes:
|
||||
return '\x1b[{0}m{1}\x1b[0m'.format(_join(*codes), s)
|
||||
else:
|
||||
return s
|
||||
|
||||
|
||||
# Foreground color shortcuts
|
||||
black = partial(color, fg='black')
|
||||
red = partial(color, fg='red')
|
||||
green = partial(color, fg='green')
|
||||
yellow = partial(color, fg='yellow')
|
||||
blue = partial(color, fg='blue')
|
||||
magenta = partial(color, fg='magenta')
|
||||
cyan = partial(color, fg='cyan')
|
||||
white = partial(color, fg='white')
|
||||
|
||||
# Style shortcuts
|
||||
bold = partial(color, style='bold')
|
||||
none = partial(color, style='none')
|
||||
faint = partial(color, style='faint')
|
||||
italic = partial(color, style='italic')
|
||||
underline = partial(color, style='underline')
|
||||
blink = partial(color, style='blink')
|
||||
blink2 = partial(color, style='blink2')
|
||||
negative = partial(color, style='negative')
|
||||
concealed = partial(color, style='concealed')
|
||||
crossed = partial(color, style='crossed')
|
||||
@@ -17,6 +17,7 @@
|
||||
# the `generate_company_id_list.py` script
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# pylint: disable=line-too-long
|
||||
COMPANY_IDENTIFIERS = {
|
||||
0x0000: "Ericsson Technology Licensing",
|
||||
0x0001: "Nokia Mobile Phones",
|
||||
@@ -196,28 +197,28 @@ COMPANY_IDENTIFIERS = {
|
||||
0x00AF: "Cinetix",
|
||||
0x00B0: "Passif Semiconductor Corp",
|
||||
0x00B1: "Saris Cycling Group, Inc",
|
||||
0x00B2: "Bekey A/S",
|
||||
0x00B3: "Clarinox Technologies Pty. Ltd.",
|
||||
0x00B4: "BDE Technology Co., Ltd.",
|
||||
0x00B2: "Bekey A/S",
|
||||
0x00B3: "Clarinox Technologies Pty. Ltd.",
|
||||
0x00B4: "BDE Technology Co., Ltd.",
|
||||
0x00B5: "Swirl Networks",
|
||||
0x00B6: "Meso international",
|
||||
0x00B7: "TreLab Ltd",
|
||||
0x00B8: "Qualcomm Innovation Center, Inc. (QuIC)",
|
||||
0x00B9: "Johnson Controls, Inc.",
|
||||
0x00BA: "Starkey Laboratories Inc.",
|
||||
0x00BB: "S-Power Electronics Limited",
|
||||
0x00BC: "Ace Sensor Inc",
|
||||
0x00BD: "Aplix Corporation",
|
||||
0x00BE: "AAMP of America",
|
||||
0x00BF: "Stalmart Technology Limited",
|
||||
0x00C0: "AMICCOM Electronics Corporation",
|
||||
0x00C1: "Shenzhen Excelsecu Data Technology Co.,Ltd",
|
||||
0x00C2: "Geneq Inc.",
|
||||
0x00C3: "adidas AG",
|
||||
0x00C4: "LG Electronics",
|
||||
0x00C5: "Onset Computer Corporation",
|
||||
0x00C6: "Selfly BV",
|
||||
0x00C7: "Quuppa Oy.",
|
||||
0x00B6: "Meso international",
|
||||
0x00B7: "TreLab Ltd",
|
||||
0x00B8: "Qualcomm Innovation Center, Inc. (QuIC)",
|
||||
0x00B9: "Johnson Controls, Inc.",
|
||||
0x00BA: "Starkey Laboratories Inc.",
|
||||
0x00BB: "S-Power Electronics Limited",
|
||||
0x00BC: "Ace Sensor Inc",
|
||||
0x00BD: "Aplix Corporation",
|
||||
0x00BE: "AAMP of America",
|
||||
0x00BF: "Stalmart Technology Limited",
|
||||
0x00C0: "AMICCOM Electronics Corporation",
|
||||
0x00C1: "Shenzhen Excelsecu Data Technology Co.,Ltd",
|
||||
0x00C2: "Geneq Inc.",
|
||||
0x00C3: "adidas AG",
|
||||
0x00C4: "LG Electronics",
|
||||
0x00C5: "Onset Computer Corporation",
|
||||
0x00C6: "Selfly BV",
|
||||
0x00C7: "Quuppa Oy.",
|
||||
0x00C8: "GeLo Inc",
|
||||
0x00C9: "Evluma",
|
||||
0x00CA: "MC10",
|
||||
@@ -249,10 +250,10 @@ COMPANY_IDENTIFIERS = {
|
||||
0x00E4: "Laird Connectivity, Inc. formerly L.S. Research Inc.",
|
||||
0x00E5: "Eden Software Consultants Ltd.",
|
||||
0x00E6: "Freshtemp",
|
||||
0x00E7: "KS Technologies",
|
||||
0x00E8: "ACTS Technologies",
|
||||
0x00E9: "Vtrack Systems",
|
||||
0x00EA: "Nielsen-Kellerman Company",
|
||||
0x00E7: "KS Technologies",
|
||||
0x00E8: "ACTS Technologies",
|
||||
0x00E9: "Vtrack Systems",
|
||||
0x00EA: "Nielsen-Kellerman Company",
|
||||
0x00EB: "Server Technology Inc.",
|
||||
0x00EC: "BioResearch Associates",
|
||||
0x00ED: "Jolly Logic, LLC",
|
||||
@@ -2704,5 +2705,5 @@ COMPANY_IDENTIFIERS = {
|
||||
0x0A7C: "WAFERLOCK",
|
||||
0x0A7D: "Freedman Electronics Pty Ltd",
|
||||
0x0A7E: "Keba AG",
|
||||
0x0A7F: "Intuity Medical"
|
||||
}
|
||||
0x0A7F: "Intuity Medical",
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
353
bumble/core.py
353
bumble/core.py
@@ -15,7 +15,9 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
import struct
|
||||
from typing import List, Optional, Tuple, Union, cast
|
||||
|
||||
from .company_ids import COMPANY_IDENTIFIERS
|
||||
|
||||
@@ -23,6 +25,8 @@ from .company_ids import COMPANY_IDENTIFIERS
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
# fmt: off
|
||||
|
||||
BT_CENTRAL_ROLE = 0
|
||||
BT_PERIPHERAL_ROLE = 1
|
||||
|
||||
@@ -30,6 +34,9 @@ BT_BR_EDR_TRANSPORT = 0
|
||||
BT_LE_TRANSPORT = 1
|
||||
|
||||
|
||||
# fmt: on
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Utils
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -58,17 +65,25 @@ def padded_bytes(buffer, size):
|
||||
return buffer + bytes(padding_size)
|
||||
|
||||
|
||||
def get_dict_key_by_value(dictionary, value):
|
||||
for key, val in dictionary.items():
|
||||
if val == value:
|
||||
return key
|
||||
return None
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Exceptions
|
||||
# -----------------------------------------------------------------------------
|
||||
class BaseError(Exception):
|
||||
""" Base class for errors with an error code, error name and namespace"""
|
||||
"""Base class for errors with an error code, error name and namespace"""
|
||||
|
||||
def __init__(self, error_code, error_namespace='', error_name='', details=''):
|
||||
super().__init__()
|
||||
self.error_code = error_code
|
||||
self.error_code = error_code
|
||||
self.error_namespace = error_namespace
|
||||
self.error_name = error_name
|
||||
self.details = details
|
||||
self.error_name = error_name
|
||||
self.details = details
|
||||
|
||||
def __str__(self):
|
||||
if self.error_namespace:
|
||||
@@ -84,22 +99,40 @@ class BaseError(Exception):
|
||||
|
||||
|
||||
class ProtocolError(BaseError):
|
||||
""" Protocol Error """
|
||||
"""Protocol Error"""
|
||||
|
||||
|
||||
class TimeoutError(Exception):
|
||||
""" Timeout Error """
|
||||
class TimeoutError(Exception): # pylint: disable=redefined-builtin
|
||||
"""Timeout Error"""
|
||||
|
||||
|
||||
class CommandTimeoutError(Exception):
|
||||
"""Command Timeout Error"""
|
||||
|
||||
|
||||
class InvalidStateError(Exception):
|
||||
""" Invalid State Error """
|
||||
"""Invalid State Error"""
|
||||
|
||||
|
||||
class ConnectionError(BaseError):
|
||||
""" Connection Error """
|
||||
FAILURE = 0x01
|
||||
class ConnectionError(BaseError): # pylint: disable=redefined-builtin
|
||||
"""Connection Error"""
|
||||
|
||||
FAILURE = 0x01
|
||||
CONNECTION_REFUSED = 0x02
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
error_code,
|
||||
transport,
|
||||
peer_address,
|
||||
error_namespace='',
|
||||
error_name='',
|
||||
details='',
|
||||
):
|
||||
super().__init__(error_code, error_namespace, error_name, details)
|
||||
self.transport = transport
|
||||
self.peer_address = peer_address
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# UUID
|
||||
@@ -111,27 +144,37 @@ class ConnectionError(BaseError):
|
||||
class UUID:
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part B - 2.5.1 UUID
|
||||
'''
|
||||
BASE_UUID = bytes.fromhex('00001000800000805F9B34FB')
|
||||
UUIDS = [] # Registry of all instances created
|
||||
|
||||
def __init__(self, uuid_str_or_int, name = None):
|
||||
if type(uuid_str_or_int) is int:
|
||||
Note that this class expects and works in little-endian byte-order throughout.
|
||||
The exception is when interacting with strings, which are in big-endian byte-order.
|
||||
'''
|
||||
|
||||
BASE_UUID = bytes.fromhex('00001000800000805F9B34FB')[::-1] # little-endian
|
||||
UUIDS: List[UUID] = [] # Registry of all instances created
|
||||
|
||||
def __init__(self, uuid_str_or_int, name=None):
|
||||
if isinstance(uuid_str_or_int, int):
|
||||
self.uuid_bytes = struct.pack('<H', uuid_str_or_int)
|
||||
else:
|
||||
if len(uuid_str_or_int) == 36:
|
||||
if uuid_str_or_int[8] != '-' or uuid_str_or_int[13] != '-' or uuid_str_or_int[18] != '-' or uuid_str_or_int[23] != '-':
|
||||
if (
|
||||
uuid_str_or_int[8] != '-'
|
||||
or uuid_str_or_int[13] != '-'
|
||||
or uuid_str_or_int[18] != '-'
|
||||
or uuid_str_or_int[23] != '-'
|
||||
):
|
||||
raise ValueError('invalid UUID format')
|
||||
uuid_str = uuid_str_or_int.replace('-', '')
|
||||
else:
|
||||
uuid_str = uuid_str_or_int
|
||||
if len(uuid_str) != 32 and len(uuid_str) != 8 and len(uuid_str) != 4:
|
||||
raise ValueError('invalid UUID format')
|
||||
raise ValueError(f"invalid UUID format: {uuid_str}")
|
||||
self.uuid_bytes = bytes(reversed(bytes.fromhex(uuid_str)))
|
||||
self.name = name
|
||||
|
||||
def register(self):
|
||||
# Register this object in the class registry, and update the entry's name if it wasn't set already
|
||||
# Register this object in the class registry, and update the entry's name if
|
||||
# it wasn't set already
|
||||
for uuid in self.UUIDS:
|
||||
if self == uuid:
|
||||
if uuid.name is None:
|
||||
@@ -142,39 +185,47 @@ class UUID:
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, uuid_bytes, name = None):
|
||||
if len(uuid_bytes) in {2, 4, 16}:
|
||||
def from_bytes(cls, uuid_bytes: bytes, name: Optional[str] = None) -> UUID:
|
||||
if len(uuid_bytes) in (2, 4, 16):
|
||||
self = cls.__new__(cls)
|
||||
self.uuid_bytes = uuid_bytes
|
||||
self.name = name
|
||||
|
||||
return self.register()
|
||||
else:
|
||||
raise ValueError('only 2, 4 and 16 bytes are allowed')
|
||||
|
||||
raise ValueError('only 2, 4 and 16 bytes are allowed')
|
||||
|
||||
@classmethod
|
||||
def from_16_bits(cls, uuid_16, name = None):
|
||||
def from_16_bits(cls, uuid_16, name=None):
|
||||
return cls.from_bytes(struct.pack('<H', uuid_16), name)
|
||||
|
||||
@classmethod
|
||||
def from_32_bits(cls, uuid_32, name = None):
|
||||
def from_32_bits(cls, uuid_32, name=None):
|
||||
return cls.from_bytes(struct.pack('<I', uuid_32), name)
|
||||
|
||||
@classmethod
|
||||
def parse_uuid(cls, bytes, offset):
|
||||
return len(bytes), cls.from_bytes(bytes[offset:])
|
||||
def parse_uuid(cls, uuid_as_bytes, offset):
|
||||
return len(uuid_as_bytes), cls.from_bytes(uuid_as_bytes[offset:])
|
||||
|
||||
@classmethod
|
||||
def parse_uuid_2(cls, bytes, offset):
|
||||
return offset + 2, cls.from_bytes(bytes[offset:offset + 2])
|
||||
def parse_uuid_2(cls, uuid_as_bytes, offset):
|
||||
return offset + 2, cls.from_bytes(uuid_as_bytes[offset : offset + 2])
|
||||
|
||||
def to_bytes(self, force_128 = False):
|
||||
if len(self.uuid_bytes) == 16 or not force_128:
|
||||
def to_bytes(self, force_128=False):
|
||||
'''
|
||||
Serialize UUID in little-endian byte-order
|
||||
'''
|
||||
if not force_128:
|
||||
return self.uuid_bytes
|
||||
|
||||
if len(self.uuid_bytes) == 2:
|
||||
return self.BASE_UUID + self.uuid_bytes + bytes([0, 0])
|
||||
elif len(self.uuid_bytes) == 4:
|
||||
return self.uuid_bytes + UUID.BASE_UUID
|
||||
return self.BASE_UUID + self.uuid_bytes
|
||||
elif len(self.uuid_bytes) == 16:
|
||||
return self.uuid_bytes
|
||||
else:
|
||||
return self.uuid_bytes + bytes([0, 0]) + UUID.BASE_UUID
|
||||
assert False, "unreachable"
|
||||
|
||||
def to_pdu_bytes(self):
|
||||
'''
|
||||
@@ -183,27 +234,30 @@ class UUID:
|
||||
"All 32-bit Attribute UUIDs shall be converted to 128-bit UUIDs when the
|
||||
Attribute UUID is contained in an ATT PDU."
|
||||
'''
|
||||
return self.to_bytes(force_128 = (len(self.uuid_bytes) == 4))
|
||||
return self.to_bytes(force_128=(len(self.uuid_bytes) == 4))
|
||||
|
||||
def to_hex_str(self):
|
||||
def to_hex_str(self) -> str:
|
||||
if len(self.uuid_bytes) == 2 or len(self.uuid_bytes) == 4:
|
||||
return bytes(reversed(self.uuid_bytes)).hex().upper()
|
||||
else:
|
||||
return ''.join([
|
||||
|
||||
return ''.join(
|
||||
[
|
||||
bytes(reversed(self.uuid_bytes[12:16])).hex(),
|
||||
bytes(reversed(self.uuid_bytes[10:12])).hex(),
|
||||
bytes(reversed(self.uuid_bytes[8:10])).hex(),
|
||||
bytes(reversed(self.uuid_bytes[6:8])).hex(),
|
||||
bytes(reversed(self.uuid_bytes[0:6])).hex()
|
||||
]).upper()
|
||||
bytes(reversed(self.uuid_bytes[0:6])).hex(),
|
||||
]
|
||||
).upper()
|
||||
|
||||
def __bytes__(self):
|
||||
return self.to_bytes()
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, UUID):
|
||||
return self.to_bytes(force_128 = True) == other.to_bytes(force_128 = True)
|
||||
elif type(other) is str:
|
||||
return self.to_bytes(force_128=True) == other.to_bytes(force_128=True)
|
||||
|
||||
if isinstance(other, str):
|
||||
return UUID(other) == self
|
||||
|
||||
return False
|
||||
@@ -213,23 +267,26 @@ class UUID:
|
||||
|
||||
def __str__(self):
|
||||
if len(self.uuid_bytes) == 2:
|
||||
v = struct.unpack('<H', self.uuid_bytes)[0]
|
||||
result = f'UUID-16:{v:04X}'
|
||||
uuid = struct.unpack('<H', self.uuid_bytes)[0]
|
||||
result = f'UUID-16:{uuid:04X}'
|
||||
elif len(self.uuid_bytes) == 4:
|
||||
v = struct.unpack('<I', self.uuid_bytes)[0]
|
||||
result = f'UUID-32:{v:08X}'
|
||||
uuid = struct.unpack('<I', self.uuid_bytes)[0]
|
||||
result = f'UUID-32:{uuid:08X}'
|
||||
else:
|
||||
result = '-'.join([
|
||||
bytes(reversed(self.uuid_bytes[12:16])).hex(),
|
||||
bytes(reversed(self.uuid_bytes[10:12])).hex(),
|
||||
bytes(reversed(self.uuid_bytes[8:10])).hex(),
|
||||
bytes(reversed(self.uuid_bytes[6:8])).hex(),
|
||||
bytes(reversed(self.uuid_bytes[0:6])).hex()
|
||||
]).upper()
|
||||
result = '-'.join(
|
||||
[
|
||||
bytes(reversed(self.uuid_bytes[12:16])).hex(),
|
||||
bytes(reversed(self.uuid_bytes[10:12])).hex(),
|
||||
bytes(reversed(self.uuid_bytes[8:10])).hex(),
|
||||
bytes(reversed(self.uuid_bytes[6:8])).hex(),
|
||||
bytes(reversed(self.uuid_bytes[0:6])).hex(),
|
||||
]
|
||||
).upper()
|
||||
|
||||
if self.name is not None:
|
||||
return result + f' ({self.name})'
|
||||
else:
|
||||
return result
|
||||
|
||||
return result
|
||||
|
||||
def __repr__(self):
|
||||
return str(self)
|
||||
@@ -238,6 +295,8 @@ class UUID:
|
||||
# -----------------------------------------------------------------------------
|
||||
# Common UUID constants
|
||||
# -----------------------------------------------------------------------------
|
||||
# fmt: off
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
# Protocol Identifiers
|
||||
BT_SDP_PROTOCOL_ID = UUID.from_16_bits(0x0001, 'SDP')
|
||||
@@ -343,11 +402,17 @@ BT_HDP_SERVICE = UUID.from_16_bits(0x1400,
|
||||
BT_HDP_SOURCE_SERVICE = UUID.from_16_bits(0x1401, 'HDP Source')
|
||||
BT_HDP_SINK_SERVICE = UUID.from_16_bits(0x1402, 'HDP Sink')
|
||||
|
||||
# fmt: on
|
||||
# pylint: enable=line-too-long
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# DeviceClass
|
||||
# -----------------------------------------------------------------------------
|
||||
class DeviceClass:
|
||||
# fmt: off
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
# Major Service Classes (flags combined with OR)
|
||||
LIMITED_DISCOVERABLE_MODE_SERVICE_CLASS = (1 << 0)
|
||||
LE_AUDIO_SERVICE_CLASS = (1 << 1)
|
||||
@@ -515,11 +580,18 @@ class DeviceClass:
|
||||
PERIPHERAL_MAJOR_DEVICE_CLASS: PERIPHERAL_MINOR_DEVICE_CLASS_NAMES
|
||||
}
|
||||
|
||||
# fmt: on
|
||||
# pylint: enable=line-too-long
|
||||
|
||||
@staticmethod
|
||||
def split_class_of_device(class_of_device):
|
||||
# Split the bit fields of the composite class of device value into:
|
||||
# (service_classes, major_device_class, minor_device_class)
|
||||
return ((class_of_device >> 13 & 0x7FF), (class_of_device >> 8 & 0x1F), (class_of_device >> 2 & 0x3F))
|
||||
return (
|
||||
(class_of_device >> 13 & 0x7FF),
|
||||
(class_of_device >> 8 & 0x1F),
|
||||
(class_of_device >> 2 & 0x3F),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def pack_class_of_device(service_classes, major_device_class, minor_device_class):
|
||||
@@ -527,7 +599,9 @@ class DeviceClass:
|
||||
|
||||
@staticmethod
|
||||
def service_class_labels(service_class_flags):
|
||||
return bit_flags_to_strings(service_class_flags, DeviceClass.SERVICE_CLASS_LABELS)
|
||||
return bit_flags_to_strings(
|
||||
service_class_flags, DeviceClass.SERVICE_CLASS_LABELS
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def major_device_class_name(device_class):
|
||||
@@ -544,7 +618,15 @@ class DeviceClass:
|
||||
# -----------------------------------------------------------------------------
|
||||
# Advertising Data
|
||||
# -----------------------------------------------------------------------------
|
||||
AdvertisingObject = Union[
|
||||
List[UUID], Tuple[UUID, bytes], bytes, str, int, Tuple[int, int], Tuple[int, bytes]
|
||||
]
|
||||
|
||||
|
||||
class AdvertisingData:
|
||||
# fmt: off
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
# This list is only partial, it still needs to be filled in from the spec
|
||||
FLAGS = 0x01
|
||||
INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS = 0x02
|
||||
@@ -656,7 +738,14 @@ class AdvertisingData:
|
||||
BR_EDR_CONTROLLER_FLAG = 0x08
|
||||
BR_EDR_HOST_FLAG = 0x10
|
||||
|
||||
def __init__(self, ad_structures = []):
|
||||
ad_structures: List[Tuple[int, bytes]]
|
||||
|
||||
# fmt: on
|
||||
# pylint: enable=line-too-long
|
||||
|
||||
def __init__(self, ad_structures: Optional[List[Tuple[int, bytes]]] = None) -> None:
|
||||
if ad_structures is None:
|
||||
ad_structures = []
|
||||
self.ad_structures = ad_structures[:]
|
||||
|
||||
@staticmethod
|
||||
@@ -667,36 +756,36 @@ class AdvertisingData:
|
||||
|
||||
@staticmethod
|
||||
def flags_to_string(flags, short=False):
|
||||
flag_names = [
|
||||
'LE Limited',
|
||||
'LE General',
|
||||
'No BR/EDR',
|
||||
'BR/EDR C',
|
||||
'BR/EDR H'
|
||||
] if short else [
|
||||
'LE Limited Discoverable Mode',
|
||||
'LE General Discoverable Mode',
|
||||
'BR/EDR Not Supported',
|
||||
'Simultaneous LE and BR/EDR (Controller)',
|
||||
'Simultaneous LE and BR/EDR (Host)'
|
||||
]
|
||||
flag_names = (
|
||||
['LE Limited', 'LE General', 'No BR/EDR', 'BR/EDR C', 'BR/EDR H']
|
||||
if short
|
||||
else [
|
||||
'LE Limited Discoverable Mode',
|
||||
'LE General Discoverable Mode',
|
||||
'BR/EDR Not Supported',
|
||||
'Simultaneous LE and BR/EDR (Controller)',
|
||||
'Simultaneous LE and BR/EDR (Host)',
|
||||
]
|
||||
)
|
||||
return ','.join(bit_flags_to_strings(flags, flag_names))
|
||||
|
||||
@staticmethod
|
||||
def uuid_list_to_objects(ad_data, uuid_size):
|
||||
def uuid_list_to_objects(ad_data: bytes, uuid_size: int) -> List[UUID]:
|
||||
uuids = []
|
||||
offset = 0
|
||||
while (uuid_size * (offset + 1)) <= len(ad_data):
|
||||
uuids.append(UUID.from_bytes(ad_data[offset:offset + uuid_size]))
|
||||
uuids.append(UUID.from_bytes(ad_data[offset : offset + uuid_size]))
|
||||
offset += uuid_size
|
||||
return uuids
|
||||
|
||||
@staticmethod
|
||||
def uuid_list_to_string(ad_data, uuid_size):
|
||||
return ', '.join([
|
||||
str(uuid)
|
||||
for uuid in AdvertisingData.uuid_list_to_objects(ad_data, uuid_size)
|
||||
])
|
||||
return ', '.join(
|
||||
[
|
||||
str(uuid)
|
||||
for uuid in AdvertisingData.uuid_list_to_objects(ad_data, uuid_size)
|
||||
]
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def ad_data_to_string(ad_type, ad_data):
|
||||
@@ -756,40 +845,65 @@ class AdvertisingData:
|
||||
|
||||
return f'[{ad_type_str}]: {ad_data_str}'
|
||||
|
||||
# pylint: disable=too-many-return-statements
|
||||
@staticmethod
|
||||
def ad_data_to_object(ad_type, ad_data):
|
||||
if ad_type in {
|
||||
def ad_data_to_object(ad_type: int, ad_data: bytes) -> AdvertisingObject:
|
||||
if ad_type in (
|
||||
AdvertisingData.COMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
|
||||
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS
|
||||
}:
|
||||
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
|
||||
AdvertisingData.LIST_OF_16_BIT_SERVICE_SOLICITATION_UUIDS,
|
||||
):
|
||||
return AdvertisingData.uuid_list_to_objects(ad_data, 2)
|
||||
elif ad_type in {
|
||||
|
||||
if ad_type in (
|
||||
AdvertisingData.COMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS,
|
||||
AdvertisingData.INCOMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS
|
||||
}:
|
||||
AdvertisingData.INCOMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS,
|
||||
AdvertisingData.LIST_OF_32_BIT_SERVICE_SOLICITATION_UUIDS,
|
||||
):
|
||||
return AdvertisingData.uuid_list_to_objects(ad_data, 4)
|
||||
elif ad_type in {
|
||||
|
||||
if ad_type in (
|
||||
AdvertisingData.COMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS,
|
||||
AdvertisingData.INCOMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS
|
||||
}:
|
||||
AdvertisingData.INCOMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS,
|
||||
AdvertisingData.LIST_OF_128_BIT_SERVICE_SOLICITATION_UUIDS,
|
||||
):
|
||||
return AdvertisingData.uuid_list_to_objects(ad_data, 16)
|
||||
elif ad_type == AdvertisingData.SERVICE_DATA_16_BIT_UUID:
|
||||
|
||||
if ad_type == AdvertisingData.SERVICE_DATA_16_BIT_UUID:
|
||||
return (UUID.from_bytes(ad_data[:2]), ad_data[2:])
|
||||
elif ad_type == AdvertisingData.SERVICE_DATA_32_BIT_UUID:
|
||||
|
||||
if ad_type == AdvertisingData.SERVICE_DATA_32_BIT_UUID:
|
||||
return (UUID.from_bytes(ad_data[:4]), ad_data[4:])
|
||||
elif ad_type == AdvertisingData.SERVICE_DATA_128_BIT_UUID:
|
||||
|
||||
if ad_type == AdvertisingData.SERVICE_DATA_128_BIT_UUID:
|
||||
return (UUID.from_bytes(ad_data[:16]), ad_data[16:])
|
||||
elif ad_type in {
|
||||
|
||||
if ad_type in (
|
||||
AdvertisingData.SHORTENED_LOCAL_NAME,
|
||||
AdvertisingData.COMPLETE_LOCAL_NAME
|
||||
}:
|
||||
AdvertisingData.COMPLETE_LOCAL_NAME,
|
||||
AdvertisingData.URI,
|
||||
):
|
||||
return ad_data.decode("utf-8")
|
||||
elif ad_type == AdvertisingData.TX_POWER_LEVEL:
|
||||
return ad_data[0]
|
||||
elif ad_type == AdvertisingData.MANUFACTURER_SPECIFIC_DATA:
|
||||
return (struct.unpack_from('<H', ad_data, 0)[0], ad_data[2:])
|
||||
else:
|
||||
return ad_data
|
||||
|
||||
if ad_type in (AdvertisingData.TX_POWER_LEVEL, AdvertisingData.FLAGS):
|
||||
return cast(int, struct.unpack('B', ad_data)[0])
|
||||
|
||||
if ad_type in (
|
||||
AdvertisingData.APPEARANCE,
|
||||
AdvertisingData.ADVERTISING_INTERVAL,
|
||||
):
|
||||
return cast(int, struct.unpack('<H', ad_data)[0])
|
||||
|
||||
if ad_type == AdvertisingData.CLASS_OF_DEVICE:
|
||||
return cast(int, struct.unpack('<I', bytes([*ad_data, 0]))[0])
|
||||
|
||||
if ad_type == AdvertisingData.PERIPHERAL_CONNECTION_INTERVAL_RANGE:
|
||||
return cast(Tuple[int, int], struct.unpack('<HH', ad_data))
|
||||
|
||||
if ad_type == AdvertisingData.MANUFACTURER_SPECIFIC_DATA:
|
||||
return (cast(int, struct.unpack_from('<H', ad_data, 0)[0]), ad_data[2:])
|
||||
|
||||
return ad_data
|
||||
|
||||
def append(self, data):
|
||||
offset = 0
|
||||
@@ -798,30 +912,41 @@ class AdvertisingData:
|
||||
offset += 1
|
||||
if length > 0:
|
||||
ad_type = data[offset]
|
||||
ad_data = data[offset + 1:offset + length]
|
||||
ad_data = data[offset + 1 : offset + length]
|
||||
self.ad_structures.append((ad_type, ad_data))
|
||||
offset += length
|
||||
|
||||
def get(self, type_id, return_all=False, raw=True):
|
||||
def get_all(self, type_id: int, raw: bool = False) -> List[AdvertisingObject]:
|
||||
'''
|
||||
Get Advertising Data Structure(s) with a given type
|
||||
|
||||
If return_all is True, returns a (possibly empty) list of matches,
|
||||
else returns the first entry, or None if no structure matches.
|
||||
Returns a (possibly empty) list of matches.
|
||||
'''
|
||||
def process_ad_data(ad_data):
|
||||
|
||||
def process_ad_data(ad_data: bytes) -> AdvertisingObject:
|
||||
return ad_data if raw else self.ad_data_to_object(type_id, ad_data)
|
||||
|
||||
if return_all:
|
||||
return [process_ad_data(ad[1]) for ad in self.ad_structures if ad[0] == type_id]
|
||||
else:
|
||||
return next((process_ad_data(ad[1]) for ad in self.ad_structures if ad[0] == type_id), None)
|
||||
return [process_ad_data(ad[1]) for ad in self.ad_structures if ad[0] == type_id]
|
||||
|
||||
def get(self, type_id: int, raw: bool = False) -> Optional[AdvertisingObject]:
|
||||
'''
|
||||
Get Advertising Data Structure(s) with a given type
|
||||
|
||||
Returns the first entry, or None if no structure matches.
|
||||
'''
|
||||
|
||||
all = self.get_all(type_id, raw=raw)
|
||||
return all[0] if all else None
|
||||
|
||||
def __bytes__(self):
|
||||
return b''.join([bytes([len(x[1]) + 1, x[0]]) + x[1] for x in self.ad_structures])
|
||||
return b''.join(
|
||||
[bytes([len(x[1]) + 1, x[0]]) + x[1] for x in self.ad_structures]
|
||||
)
|
||||
|
||||
def to_string(self, separator=', '):
|
||||
return separator.join([AdvertisingData.ad_data_to_string(x[0], x[1]) for x in self.ad_structures])
|
||||
return separator.join(
|
||||
[AdvertisingData.ad_data_to_string(x[0], x[1]) for x in self.ad_structures]
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.to_string()
|
||||
@@ -831,13 +956,17 @@ class AdvertisingData:
|
||||
# Connection Parameters
|
||||
# -----------------------------------------------------------------------------
|
||||
class ConnectionParameters:
|
||||
def __init__(self, connection_interval, connection_latency, supervision_timeout):
|
||||
def __init__(self, connection_interval, peripheral_latency, supervision_timeout):
|
||||
self.connection_interval = connection_interval
|
||||
self.connection_latency = connection_latency
|
||||
self.peripheral_latency = peripheral_latency
|
||||
self.supervision_timeout = supervision_timeout
|
||||
|
||||
def __str__(self):
|
||||
return f'ConnectionParameters(connection_interval={self.connection_interval}, connection_latency={self.connection_latency}, supervision_timeout={self.supervision_timeout}'
|
||||
return (
|
||||
f'ConnectionParameters(connection_interval={self.connection_interval}, '
|
||||
f'peripheral_latency={self.peripheral_latency}, '
|
||||
f'supervision_timeout={self.supervision_timeout}'
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
138
bumble/crypto.py
138
bumble/crypto.py
@@ -24,19 +24,16 @@
|
||||
import logging
|
||||
import operator
|
||||
import platform
|
||||
|
||||
if platform.system() != 'Emscripten':
|
||||
import secrets
|
||||
from cryptography.hazmat.primitives.ciphers import (
|
||||
Cipher,
|
||||
algorithms,
|
||||
modes
|
||||
)
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
from cryptography.hazmat.primitives.asymmetric.ec import (
|
||||
generate_private_key,
|
||||
ECDH,
|
||||
EllipticCurvePublicNumbers,
|
||||
EllipticCurvePrivateNumbers,
|
||||
SECP256R1
|
||||
SECP256R1,
|
||||
)
|
||||
from cryptography.hazmat.primitives import cmac
|
||||
else:
|
||||
@@ -66,16 +63,26 @@ class EccKey:
|
||||
d = int.from_bytes(d_bytes, byteorder='big', signed=False)
|
||||
x = int.from_bytes(x_bytes, byteorder='big', signed=False)
|
||||
y = int.from_bytes(y_bytes, byteorder='big', signed=False)
|
||||
private_key = EllipticCurvePrivateNumbers(d, EllipticCurvePublicNumbers(x, y, SECP256R1())).private_key()
|
||||
private_key = EllipticCurvePrivateNumbers(
|
||||
d, EllipticCurvePublicNumbers(x, y, SECP256R1())
|
||||
).private_key()
|
||||
return cls(private_key)
|
||||
|
||||
@property
|
||||
def x(self):
|
||||
return self.private_key.public_key().public_numbers().x.to_bytes(32, byteorder='big')
|
||||
return (
|
||||
self.private_key.public_key()
|
||||
.public_numbers()
|
||||
.x.to_bytes(32, byteorder='big')
|
||||
)
|
||||
|
||||
@property
|
||||
def y(self):
|
||||
return self.private_key.public_key().public_numbers().y.to_bytes(32, byteorder='big')
|
||||
return (
|
||||
self.private_key.public_key()
|
||||
.public_numbers()
|
||||
.y.to_bytes(32, byteorder='big')
|
||||
)
|
||||
|
||||
def dh(self, public_key_x, public_key_y):
|
||||
x = int.from_bytes(public_key_x, byteorder='big', signed=False)
|
||||
@@ -92,7 +99,7 @@ class EccKey:
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def xor(x, y):
|
||||
assert(len(x) == len(y))
|
||||
assert len(x) == len(y)
|
||||
return bytes(map(operator.xor, x, y))
|
||||
|
||||
|
||||
@@ -118,7 +125,7 @@ def e(key, data):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def ah(k, r):
|
||||
def ah(k, r): # pylint: disable=redefined-outer-name
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part H - 2.2.2 Random Address Hash function ah
|
||||
'''
|
||||
@@ -129,9 +136,10 @@ def ah(k, r):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def c1(k, r, preq, pres, iat, rat, ia, ra):
|
||||
def c1(k, r, preq, pres, iat, rat, ia, ra): # pylint: disable=redefined-outer-name
|
||||
'''
|
||||
See Bluetooth spec, Vol 3, Part H - 2.2.3 Confirm value generation function c1 for LE Legacy Pairing
|
||||
See Bluetooth spec, Vol 3, Part H - 2.2.3 Confirm value generation function c1 for
|
||||
LE Legacy Pairing
|
||||
'''
|
||||
|
||||
p1 = bytes([iat, rat]) + preq + pres
|
||||
@@ -142,7 +150,8 @@ def c1(k, r, preq, pres, iat, rat, ia, ra):
|
||||
# -----------------------------------------------------------------------------
|
||||
def s1(k, r1, r2):
|
||||
'''
|
||||
See Bluetooth spec, Vol 3, Part H - 2.2.4 Key generation function s1 for LE Legacy Pairing
|
||||
See Bluetooth spec, Vol 3, Part H - 2.2.4 Key generation function s1 for LE Legacy
|
||||
Pairing
|
||||
'''
|
||||
|
||||
return e(k, r2[0:8] + r1[0:8])
|
||||
@@ -163,71 +172,95 @@ def aes_cmac(m, k):
|
||||
# -----------------------------------------------------------------------------
|
||||
def f4(u, v, x, z):
|
||||
'''
|
||||
See Bluetooth spec, Vol 3, Part H - 2.2.6 LE Secure Connections Confirm Value Generation Function f4
|
||||
See Bluetooth spec, Vol 3, Part H - 2.2.6 LE Secure Connections Confirm Value
|
||||
Generation Function f4
|
||||
'''
|
||||
return bytes(reversed(aes_cmac(bytes(reversed(u)) + bytes(reversed(v)) + z, bytes(reversed(x)))))
|
||||
return bytes(
|
||||
reversed(
|
||||
aes_cmac(bytes(reversed(u)) + bytes(reversed(v)) + z, bytes(reversed(x)))
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def f5(w, n1, n2, a1, a2):
|
||||
'''
|
||||
See Bluetooth spec, Vol 3, Part H - 2.2.7 LE Secure Connections Key Generation Function f5
|
||||
See Bluetooth spec, Vol 3, Part H - 2.2.7 LE Secure Connections Key Generation
|
||||
Function f5
|
||||
|
||||
NOTE: this returns a tuple: (MacKey, LTK) in little-endian byte order
|
||||
'''
|
||||
salt = bytes.fromhex('6C888391AAF5A53860370BDB5A6083BE')
|
||||
t = aes_cmac(bytes(reversed(w)), salt)
|
||||
key_id = bytes([0x62, 0x74, 0x6c, 0x65])
|
||||
key_id = bytes([0x62, 0x74, 0x6C, 0x65])
|
||||
return (
|
||||
bytes(reversed(aes_cmac(
|
||||
bytes([0]) +
|
||||
key_id +
|
||||
bytes(reversed(n1)) +
|
||||
bytes(reversed(n2)) +
|
||||
bytes(reversed(a1)) +
|
||||
bytes(reversed(a2)) +
|
||||
bytes([1, 0]),
|
||||
t
|
||||
))),
|
||||
bytes(reversed(aes_cmac(
|
||||
bytes([1]) +
|
||||
key_id +
|
||||
bytes(reversed(n1)) +
|
||||
bytes(reversed(n2)) +
|
||||
bytes(reversed(a1)) +
|
||||
bytes(reversed(a2)) +
|
||||
bytes([1, 0]),
|
||||
t
|
||||
)))
|
||||
bytes(
|
||||
reversed(
|
||||
aes_cmac(
|
||||
bytes([0])
|
||||
+ key_id
|
||||
+ bytes(reversed(n1))
|
||||
+ bytes(reversed(n2))
|
||||
+ bytes(reversed(a1))
|
||||
+ bytes(reversed(a2))
|
||||
+ bytes([1, 0]),
|
||||
t,
|
||||
)
|
||||
)
|
||||
),
|
||||
bytes(
|
||||
reversed(
|
||||
aes_cmac(
|
||||
bytes([1])
|
||||
+ key_id
|
||||
+ bytes(reversed(n1))
|
||||
+ bytes(reversed(n2))
|
||||
+ bytes(reversed(a1))
|
||||
+ bytes(reversed(a2))
|
||||
+ bytes([1, 0]),
|
||||
t,
|
||||
)
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def f6(w, n1, n2, r, io_cap, a1, a2):
|
||||
def f6(w, n1, n2, r, io_cap, a1, a2): # pylint: disable=redefined-outer-name
|
||||
'''
|
||||
See Bluetooth spec, Vol 3, Part H - 2.2.8 LE Secure Connections Check Value Generation Function f6
|
||||
See Bluetooth spec, Vol 3, Part H - 2.2.8 LE Secure Connections Check Value
|
||||
Generation Function f6
|
||||
'''
|
||||
return bytes(reversed(aes_cmac(
|
||||
bytes(reversed(n1)) +
|
||||
bytes(reversed(n2)) +
|
||||
bytes(reversed(r)) +
|
||||
bytes(reversed(io_cap)) +
|
||||
bytes(reversed(a1)) +
|
||||
bytes(reversed(a2)),
|
||||
bytes(reversed(w))
|
||||
)))
|
||||
return bytes(
|
||||
reversed(
|
||||
aes_cmac(
|
||||
bytes(reversed(n1))
|
||||
+ bytes(reversed(n2))
|
||||
+ bytes(reversed(r))
|
||||
+ bytes(reversed(io_cap))
|
||||
+ bytes(reversed(a1))
|
||||
+ bytes(reversed(a2)),
|
||||
bytes(reversed(w)),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def g2(u, v, x, y):
|
||||
'''
|
||||
See Bluetooth spec, Vol 3, Part H - 2.2.9 LE Secure Connections Numeric Comparison Value Generation Function g2
|
||||
See Bluetooth spec, Vol 3, Part H - 2.2.9 LE Secure Connections Numeric Comparison
|
||||
Value Generation Function g2
|
||||
'''
|
||||
return int.from_bytes(
|
||||
aes_cmac(bytes(reversed(u)) + bytes(reversed(v)) + bytes(reversed(y)), bytes(reversed(x)))[-4:],
|
||||
byteorder='big'
|
||||
aes_cmac(
|
||||
bytes(reversed(u)) + bytes(reversed(v)) + bytes(reversed(y)),
|
||||
bytes(reversed(x)),
|
||||
)[-4:],
|
||||
byteorder='big',
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def h6(w, key_id):
|
||||
'''
|
||||
@@ -235,6 +268,7 @@ def h6(w, key_id):
|
||||
'''
|
||||
return aes_cmac(key_id, w)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def h7(salt, w):
|
||||
'''
|
||||
|
||||
416
bumble/decoder.py
Normal file
416
bumble/decoder.py
Normal file
@@ -0,0 +1,416 @@
|
||||
# Copyright 2023 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
# fmt: off
|
||||
|
||||
WL = [-60, -30, 58, 172, 334, 538, 1198, 3042]
|
||||
RL42 = [0, 7, 6, 5, 4, 3, 2, 1, 7, 6, 5, 4, 3, 2, 1, 0]
|
||||
ILB = [
|
||||
2048,
|
||||
2093,
|
||||
2139,
|
||||
2186,
|
||||
2233,
|
||||
2282,
|
||||
2332,
|
||||
2383,
|
||||
2435,
|
||||
2489,
|
||||
2543,
|
||||
2599,
|
||||
2656,
|
||||
2714,
|
||||
2774,
|
||||
2834,
|
||||
2896,
|
||||
2960,
|
||||
3025,
|
||||
3091,
|
||||
3158,
|
||||
3228,
|
||||
3298,
|
||||
3371,
|
||||
3444,
|
||||
3520,
|
||||
3597,
|
||||
3676,
|
||||
3756,
|
||||
3838,
|
||||
3922,
|
||||
4008,
|
||||
]
|
||||
WH = [0, -214, 798]
|
||||
RH2 = [2, 1, 2, 1]
|
||||
# Values in QM2/QM4/QM6 left shift three bits than original g722 specification.
|
||||
QM2 = [-7408, -1616, 7408, 1616]
|
||||
QM4 = [
|
||||
0,
|
||||
-20456,
|
||||
-12896,
|
||||
-8968,
|
||||
-6288,
|
||||
-4240,
|
||||
-2584,
|
||||
-1200,
|
||||
20456,
|
||||
12896,
|
||||
8968,
|
||||
6288,
|
||||
4240,
|
||||
2584,
|
||||
1200,
|
||||
0,
|
||||
]
|
||||
QM6 = [
|
||||
-136,
|
||||
-136,
|
||||
-136,
|
||||
-136,
|
||||
-24808,
|
||||
-21904,
|
||||
-19008,
|
||||
-16704,
|
||||
-14984,
|
||||
-13512,
|
||||
-12280,
|
||||
-11192,
|
||||
-10232,
|
||||
-9360,
|
||||
-8576,
|
||||
-7856,
|
||||
-7192,
|
||||
-6576,
|
||||
-6000,
|
||||
-5456,
|
||||
-4944,
|
||||
-4464,
|
||||
-4008,
|
||||
-3576,
|
||||
-3168,
|
||||
-2776,
|
||||
-2400,
|
||||
-2032,
|
||||
-1688,
|
||||
-1360,
|
||||
-1040,
|
||||
-728,
|
||||
24808,
|
||||
21904,
|
||||
19008,
|
||||
16704,
|
||||
14984,
|
||||
13512,
|
||||
12280,
|
||||
11192,
|
||||
10232,
|
||||
9360,
|
||||
8576,
|
||||
7856,
|
||||
7192,
|
||||
6576,
|
||||
6000,
|
||||
5456,
|
||||
4944,
|
||||
4464,
|
||||
4008,
|
||||
3576,
|
||||
3168,
|
||||
2776,
|
||||
2400,
|
||||
2032,
|
||||
1688,
|
||||
1360,
|
||||
1040,
|
||||
728,
|
||||
432,
|
||||
136,
|
||||
-432,
|
||||
-136,
|
||||
]
|
||||
QMF_COEFFS = [3, -11, 12, 32, -210, 951, 3876, -805, 362, -156, 53, -11]
|
||||
|
||||
# fmt: on
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Classes
|
||||
# -----------------------------------------------------------------------------
|
||||
class G722Decoder(object):
|
||||
"""G.722 decoder with bitrate 64kbit/s.
|
||||
|
||||
For the Blocks in the sub-band decoders, please refer to the G.722
|
||||
specification for the required information. G722 specification:
|
||||
https://www.itu.int/rec/T-REC-G.722-201209-I
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._x = [0] * 24
|
||||
self._band = [Band(), Band()]
|
||||
# The initial value in BLOCK 3L
|
||||
self._band[0].det = 32
|
||||
# The initial value in BLOCK 3H
|
||||
self._band[1].det = 8
|
||||
|
||||
def decode_frame(self, encoded_data) -> bytearray:
|
||||
result_array = bytearray(len(encoded_data) * 4)
|
||||
self.g722_decode(result_array, encoded_data)
|
||||
return result_array
|
||||
|
||||
def g722_decode(self, result_array, encoded_data) -> int:
|
||||
"""Decode the data frame using g722 decoder."""
|
||||
result_length = 0
|
||||
|
||||
for code in encoded_data:
|
||||
higher_bits = (code >> 6) & 0x03
|
||||
lower_bits = code & 0x3F
|
||||
|
||||
rlow = self.lower_sub_band_decoder(lower_bits)
|
||||
rhigh = self.higher_sub_band_decoder(higher_bits)
|
||||
|
||||
# Apply the receive QMF
|
||||
self._x[:22] = self._x[2:]
|
||||
self._x[22] = rlow + rhigh
|
||||
self._x[23] = rlow - rhigh
|
||||
|
||||
xout2 = sum(self._x[2 * i] * QMF_COEFFS[i] for i in range(12))
|
||||
xout1 = sum(self._x[2 * i + 1] * QMF_COEFFS[11 - i] for i in range(12))
|
||||
|
||||
result_length = self.update_decoded_result(
|
||||
xout1, result_length, result_array
|
||||
)
|
||||
result_length = self.update_decoded_result(
|
||||
xout2, result_length, result_array
|
||||
)
|
||||
|
||||
return result_length
|
||||
|
||||
def update_decoded_result(self, xout, byte_length, byte_array) -> int:
|
||||
result = (int)(xout >> 11)
|
||||
bytes_result = result.to_bytes(2, 'little', signed=True)
|
||||
byte_array[byte_length] = bytes_result[0]
|
||||
byte_array[byte_length + 1] = bytes_result[1]
|
||||
return byte_length + 2
|
||||
|
||||
def lower_sub_band_decoder(self, lower_bits) -> int:
|
||||
"""Lower sub-band decoder for last six bits."""
|
||||
|
||||
# Block 5L
|
||||
# INVQBL
|
||||
wd1 = lower_bits
|
||||
wd2 = QM6[wd1]
|
||||
wd1 >>= 2
|
||||
wd2 = (self._band[0].det * wd2) >> 15
|
||||
# RECONS
|
||||
rlow = self._band[0].s + wd2
|
||||
|
||||
# Block 6L
|
||||
# LIMIT
|
||||
if rlow > 16383:
|
||||
rlow = 16383
|
||||
elif rlow < -16384:
|
||||
rlow = -16384
|
||||
|
||||
# Block 2L
|
||||
# INVQAL
|
||||
wd2 = QM4[wd1]
|
||||
dlowt = (self._band[0].det * wd2) >> 15
|
||||
|
||||
# Block 3L
|
||||
# LOGSCL
|
||||
wd2 = RL42[wd1]
|
||||
wd1 = (self._band[0].nb * 127) >> 7
|
||||
wd1 += WL[wd2]
|
||||
|
||||
if wd1 < 0:
|
||||
wd1 = 0
|
||||
elif wd1 > 18432:
|
||||
wd1 = 18432
|
||||
|
||||
self._band[0].nb = wd1
|
||||
|
||||
# SCALEL
|
||||
wd1 = (self._band[0].nb >> 6) & 31
|
||||
wd2 = 8 - (self._band[0].nb >> 11)
|
||||
|
||||
if wd2 < 0:
|
||||
wd3 = ILB[wd1] << -wd2
|
||||
else:
|
||||
wd3 = ILB[wd1] >> wd2
|
||||
|
||||
self._band[0].det = wd3 << 2
|
||||
|
||||
# Block 4L
|
||||
self._band[0].block4(dlowt)
|
||||
|
||||
return rlow
|
||||
|
||||
def higher_sub_band_decoder(self, higher_bits) -> int:
|
||||
"""Higher sub-band decoder for first two bits."""
|
||||
|
||||
# Block 2H
|
||||
# INVQAH
|
||||
wd2 = QM2[higher_bits]
|
||||
dhigh = (self._band[1].det * wd2) >> 15
|
||||
|
||||
# Block 5H
|
||||
# RECONS
|
||||
rhigh = dhigh + self._band[1].s
|
||||
|
||||
# Block 6H
|
||||
# LIMIT
|
||||
if rhigh > 16383:
|
||||
rhigh = 16383
|
||||
elif rhigh < -16384:
|
||||
rhigh = -16384
|
||||
|
||||
# Block 3H
|
||||
# LOGSCH
|
||||
wd2 = RH2[higher_bits]
|
||||
wd1 = (self._band[1].nb * 127) >> 7
|
||||
wd1 += WH[wd2]
|
||||
|
||||
if wd1 < 0:
|
||||
wd1 = 0
|
||||
elif wd1 > 22528:
|
||||
wd1 = 22528
|
||||
self._band[1].nb = wd1
|
||||
|
||||
# SCALEH
|
||||
wd1 = (self._band[1].nb >> 6) & 31
|
||||
wd2 = 10 - (self._band[1].nb >> 11)
|
||||
|
||||
if wd2 < 0:
|
||||
wd3 = ILB[wd1] << -wd2
|
||||
else:
|
||||
wd3 = ILB[wd1] >> wd2
|
||||
self._band[1].det = wd3 << 2
|
||||
|
||||
# Block 4H
|
||||
self._band[1].block4(dhigh)
|
||||
|
||||
return rhigh
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Band(object):
|
||||
"""Structure for G722 decode proccessing."""
|
||||
|
||||
s: int = 0
|
||||
nb: int = 0
|
||||
det: int = 0
|
||||
|
||||
def __init__(self):
|
||||
self._sp = 0
|
||||
self._sz = 0
|
||||
self._r = [0] * 3
|
||||
self._a = [0] * 3
|
||||
self._ap = [0] * 3
|
||||
self._p = [0] * 3
|
||||
self._d = [0] * 7
|
||||
self._b = [0] * 7
|
||||
self._bp = [0] * 7
|
||||
self._sg = [0] * 7
|
||||
|
||||
def saturate(self, amp: int) -> int:
|
||||
if amp > 32767:
|
||||
return 32767
|
||||
elif amp < -32768:
|
||||
return -32768
|
||||
else:
|
||||
return amp
|
||||
|
||||
def block4(self, d: int) -> None:
|
||||
"""Block4 for both lower and higher sub-band decoder."""
|
||||
wd1 = 0
|
||||
wd2 = 0
|
||||
wd3 = 0
|
||||
|
||||
# RECONS
|
||||
self._d[0] = d
|
||||
self._r[0] = self.saturate(self.s + d)
|
||||
|
||||
# PARREC
|
||||
self._p[0] = self.saturate(self._sz + d)
|
||||
|
||||
# UPPOL2
|
||||
for i in range(3):
|
||||
self._sg[i] = (self._p[i]) >> 15
|
||||
wd1 = self.saturate((self._a[1]) << 2)
|
||||
wd2 = -wd1 if self._sg[0] == self._sg[1] else wd1
|
||||
|
||||
if wd2 > 32767:
|
||||
wd2 = 32767
|
||||
|
||||
wd3 = 128 if self._sg[0] == self._sg[2] else -128
|
||||
wd3 += wd2 >> 7
|
||||
wd3 += (self._a[2] * 32512) >> 15
|
||||
|
||||
if wd3 > 12288:
|
||||
wd3 = 12288
|
||||
elif wd3 < -12288:
|
||||
wd3 = -12288
|
||||
self._ap[2] = wd3
|
||||
|
||||
# UPPOL1
|
||||
self._sg[0] = (self._p[0]) >> 15
|
||||
self._sg[1] = (self._p[1]) >> 15
|
||||
wd1 = 192 if self._sg[0] == self._sg[1] else -192
|
||||
wd2 = (self._a[1] * 32640) >> 15
|
||||
|
||||
self._ap[1] = self.saturate(wd1 + wd2)
|
||||
wd3 = self.saturate(15360 - self._ap[2])
|
||||
|
||||
if self._ap[1] > wd3:
|
||||
self._ap[1] = wd3
|
||||
elif self._ap[1] < -wd3:
|
||||
self._ap[1] = -wd3
|
||||
|
||||
# UPZERO
|
||||
wd1 = 0 if d == 0 else 128
|
||||
self._sg[0] = d >> 15
|
||||
for i in range(1, 7):
|
||||
self._sg[i] = (self._d[i]) >> 15
|
||||
wd2 = wd1 if self._sg[i] == self._sg[0] else -wd1
|
||||
wd3 = (self._b[i] * 32640) >> 15
|
||||
self._bp[i] = self.saturate(wd2 + wd3)
|
||||
|
||||
# DELAYA
|
||||
for i in range(6, 0, -1):
|
||||
self._d[i] = self._d[i - 1]
|
||||
self._b[i] = self._bp[i]
|
||||
|
||||
for i in range(2, 0, -1):
|
||||
self._r[i] = self._r[i - 1]
|
||||
self._p[i] = self._p[i - 1]
|
||||
self._a[i] = self._ap[i]
|
||||
|
||||
# FILTEP
|
||||
self._sp = 0
|
||||
for i in range(1, 3):
|
||||
wd1 = self.saturate(self._r[i] + self._r[i])
|
||||
self._sp += (self._a[i] * wd1) >> 15
|
||||
self._sp = self.saturate(self._sp)
|
||||
|
||||
# FILTEZ
|
||||
self._sz = 0
|
||||
for i in range(6, 0, -1):
|
||||
wd1 = self.saturate(self._d[i] + self._d[i])
|
||||
self._sz += (self._b[i] * wd1) >> 15
|
||||
self._sz = self.saturate(self._sz)
|
||||
|
||||
# PREDIC
|
||||
self.s = self.saturate(self._sp + self._sz)
|
||||
2631
bumble/device.py
2631
bumble/device.py
File diff suppressed because it is too large
Load Diff
@@ -23,7 +23,7 @@ from .gatt import (
|
||||
Characteristic,
|
||||
GATT_GENERIC_ACCESS_SERVICE,
|
||||
GATT_DEVICE_NAME_CHARACTERISTIC,
|
||||
GATT_APPEARANCE_CHARACTERISTIC
|
||||
GATT_APPEARANCE_CHARACTERISTIC,
|
||||
)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -38,22 +38,22 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class GenericAccessService(Service):
|
||||
def __init__(self, device_name, appearance = (0, 0)):
|
||||
def __init__(self, device_name, appearance=(0, 0)):
|
||||
device_name_characteristic = Characteristic(
|
||||
GATT_DEVICE_NAME_CHARACTERISTIC,
|
||||
Characteristic.READ,
|
||||
Characteristic.Properties.READ,
|
||||
Characteristic.READABLE,
|
||||
device_name.encode('utf-8')[:248]
|
||||
device_name.encode('utf-8')[:248],
|
||||
)
|
||||
|
||||
appearance_characteristic = Characteristic(
|
||||
GATT_APPEARANCE_CHARACTERISTIC,
|
||||
Characteristic.READ,
|
||||
Characteristic.Properties.READ,
|
||||
Characteristic.READABLE,
|
||||
struct.pack('<H', (appearance[0] << 6) | appearance[1])
|
||||
struct.pack('<H', (appearance[0] << 6) | appearance[1]),
|
||||
)
|
||||
|
||||
super().__init__(GATT_GENERIC_ACCESS_SERVICE, [
|
||||
device_name_characteristic,
|
||||
appearance_characteristic
|
||||
])
|
||||
super().__init__(
|
||||
GATT_GENERIC_ACCESS_SERVICE,
|
||||
[device_name_characteristic, appearance_characteristic],
|
||||
)
|
||||
|
||||
264
bumble/gatt.py
264
bumble/gatt.py
@@ -22,14 +22,18 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
import asyncio
|
||||
import types
|
||||
import enum
|
||||
import functools
|
||||
import logging
|
||||
from colors import color
|
||||
import struct
|
||||
from typing import Optional, Sequence, List
|
||||
|
||||
from .colors import color
|
||||
from .core import UUID, get_dict_key_by_value
|
||||
from .att import Attribute
|
||||
|
||||
from .core import *
|
||||
from .hci import *
|
||||
from .att import *
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -39,6 +43,9 @@ logger = logging.getLogger(__name__)
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
# fmt: off
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
GATT_REQUEST_TIMEOUT = 30 # seconds
|
||||
|
||||
GATT_MAX_ATTRIBUTE_VALUE_SIZE = 512
|
||||
@@ -149,6 +156,14 @@ GATT_HEART_RATE_CONTROL_POINT_CHARACTERISTIC = UUID.from_16_bits(0x2A39, 'Heart
|
||||
# Battery Service
|
||||
GATT_BATTERY_LEVEL_CHARACTERISTIC = UUID.from_16_bits(0x2A19, 'Battery Level')
|
||||
|
||||
# ASHA Service
|
||||
GATT_ASHA_SERVICE = UUID.from_16_bits(0xFDF0, 'Audio Streaming for Hearing Aid')
|
||||
GATT_ASHA_READ_ONLY_PROPERTIES_CHARACTERISTIC = UUID('6333651e-c481-4a3e-9169-7c902aad37bb', 'ReadOnlyProperties')
|
||||
GATT_ASHA_AUDIO_CONTROL_POINT_CHARACTERISTIC = UUID('f0d4de7e-4a88-476c-9d9f-1937b0996cc0', 'AudioControlPoint')
|
||||
GATT_ASHA_AUDIO_STATUS_CHARACTERISTIC = UUID('38663f1a-e711-4cac-b641-326b56404837', 'AudioStatus')
|
||||
GATT_ASHA_VOLUME_CHARACTERISTIC = UUID('00e4ca9e-ab14-41e4-8823-f9e70c7e91df', 'Volume')
|
||||
GATT_ASHA_LE_PSM_OUT_CHARACTERISTIC = UUID('2d410339-82b6-42aa-b34e-e2e01df8cc1a', 'LE_PSM_OUT')
|
||||
|
||||
# Misc
|
||||
GATT_DEVICE_NAME_CHARACTERISTIC = UUID.from_16_bits(0x2A00, 'Device Name')
|
||||
GATT_APPEARANCE_CHARACTERISTIC = UUID.from_16_bits(0x2A01, 'Appearance')
|
||||
@@ -163,11 +178,15 @@ GATT_CURRENT_TIME_CHARACTERISTIC = UUID.from_16_bi
|
||||
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')
|
||||
|
||||
# fmt: on
|
||||
# pylint: enable=line-too-long
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Utils
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def show_services(services):
|
||||
for service in services:
|
||||
print(color(str(service), 'cyan'))
|
||||
@@ -185,23 +204,40 @@ class Service(Attribute):
|
||||
See Vol 3, Part G - 3.1 SERVICE DEFINITION
|
||||
'''
|
||||
|
||||
def __init__(self, uuid, characteristics, primary=True):
|
||||
uuid: UUID
|
||||
|
||||
def __init__(self, uuid, characteristics: list[Characteristic], primary=True):
|
||||
# Convert the uuid to a UUID object if it isn't already
|
||||
if type(uuid) is str:
|
||||
if isinstance(uuid, str):
|
||||
uuid = UUID(uuid)
|
||||
|
||||
super().__init__(
|
||||
GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE if primary else GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE,
|
||||
GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE
|
||||
if primary
|
||||
else GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE,
|
||||
Attribute.READABLE,
|
||||
uuid.to_pdu_bytes()
|
||||
uuid.to_pdu_bytes(),
|
||||
)
|
||||
self.uuid = uuid
|
||||
self.included_services = []
|
||||
self.characteristics = characteristics[:]
|
||||
self.primary = primary
|
||||
self.uuid = uuid
|
||||
# self.included_services = []
|
||||
self.characteristics = characteristics[:]
|
||||
self.primary = primary
|
||||
|
||||
def get_advertising_data(self) -> Optional[bytes]:
|
||||
"""
|
||||
Get Service specific advertising data
|
||||
Defined by each Service, default value is empty
|
||||
:return Service data for advertising
|
||||
"""
|
||||
return None
|
||||
|
||||
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}, '
|
||||
f'end=0x{self.end_group_handle:04X}, '
|
||||
f'uuid={self.uuid})'
|
||||
f'{"" if self.primary else "*"}'
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -210,6 +246,7 @@ 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):
|
||||
@@ -222,51 +259,115 @@ class Characteristic(Attribute):
|
||||
See Vol 3, Part G - 3.3 CHARACTERISTIC DEFINITION
|
||||
'''
|
||||
|
||||
# Property flags
|
||||
BROADCAST = 0x01
|
||||
READ = 0x02
|
||||
WRITE_WITHOUT_RESPONSE = 0x04
|
||||
WRITE = 0x08
|
||||
NOTIFY = 0x10
|
||||
INDICATE = 0X20
|
||||
AUTHENTICATED_SIGNED_WRITES = 0X40
|
||||
EXTENDED_PROPERTIES = 0X80
|
||||
uuid: UUID
|
||||
properties: Characteristic.Properties
|
||||
|
||||
PROPERTY_NAMES = {
|
||||
BROADCAST: 'BROADCAST',
|
||||
READ: 'READ',
|
||||
WRITE_WITHOUT_RESPONSE: 'WRITE_WITHOUT_RESPONSE',
|
||||
WRITE: 'WRITE',
|
||||
NOTIFY: 'NOTIFY',
|
||||
INDICATE: 'INDICATE',
|
||||
AUTHENTICATED_SIGNED_WRITES: 'AUTHENTICATED_SIGNED_WRITES',
|
||||
EXTENDED_PROPERTIES: 'EXTENDED_PROPERTIES'
|
||||
}
|
||||
class Properties(enum.IntFlag):
|
||||
"""Property flags"""
|
||||
|
||||
@staticmethod
|
||||
def property_name(property):
|
||||
return Characteristic.PROPERTY_NAMES.get(property, '')
|
||||
BROADCAST = 0x01
|
||||
READ = 0x02
|
||||
WRITE_WITHOUT_RESPONSE = 0x04
|
||||
WRITE = 0x08
|
||||
NOTIFY = 0x10
|
||||
INDICATE = 0x20
|
||||
AUTHENTICATED_SIGNED_WRITES = 0x40
|
||||
EXTENDED_PROPERTIES = 0x80
|
||||
|
||||
@staticmethod
|
||||
def properties_as_string(properties):
|
||||
return ','.join([
|
||||
Characteristic.property_name(p) for p in Characteristic.PROPERTY_NAMES.keys()
|
||||
if properties & p
|
||||
])
|
||||
@staticmethod
|
||||
def from_string(properties_str: str) -> Characteristic.Properties:
|
||||
property_names: List[str] = []
|
||||
for property in Characteristic.Properties:
|
||||
if property.name is None:
|
||||
raise TypeError()
|
||||
property_names.append(property.name)
|
||||
|
||||
def __init__(self, uuid, properties, permissions, value = b'', descriptors = []):
|
||||
def string_to_property(property_string) -> Characteristic.Properties:
|
||||
for property in zip(Characteristic.Properties, property_names):
|
||||
if property_string == property[1]:
|
||||
return property[0]
|
||||
raise TypeError(f"Unable to convert {property_string} to Property")
|
||||
|
||||
try:
|
||||
return functools.reduce(
|
||||
lambda x, y: x | string_to_property(y),
|
||||
properties_str.split(","),
|
||||
Characteristic.Properties(0),
|
||||
)
|
||||
except TypeError:
|
||||
raise TypeError(
|
||||
f"Characteristic.Properties::from_string() error:\nExpected a string containing any of the keys, separated by commas: {','.join(property_names)}\nGot: {properties_str}"
|
||||
)
|
||||
|
||||
# For backwards compatibility these are defined here
|
||||
# For new code, please use Characteristic.Properties.X
|
||||
BROADCAST = Properties.BROADCAST
|
||||
READ = Properties.READ
|
||||
WRITE_WITHOUT_RESPONSE = Properties.WRITE_WITHOUT_RESPONSE
|
||||
WRITE = Properties.WRITE
|
||||
NOTIFY = Properties.NOTIFY
|
||||
INDICATE = Properties.INDICATE
|
||||
AUTHENTICATED_SIGNED_WRITES = Properties.AUTHENTICATED_SIGNED_WRITES
|
||||
EXTENDED_PROPERTIES = Properties.EXTENDED_PROPERTIES
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
uuid,
|
||||
properties: Characteristic.Properties,
|
||||
permissions,
|
||||
value=b'',
|
||||
descriptors: Sequence[Descriptor] = (),
|
||||
):
|
||||
super().__init__(uuid, permissions, value)
|
||||
self.uuid = self.type
|
||||
self.properties = properties
|
||||
self.uuid = self.type
|
||||
self.properties = properties
|
||||
self.descriptors = descriptors
|
||||
|
||||
def get_descriptor(self, descriptor_type):
|
||||
for descriptor in self.descriptors:
|
||||
if descriptor.uuid == descriptor_type:
|
||||
if descriptor.type == descriptor_type:
|
||||
return descriptor
|
||||
|
||||
return None
|
||||
|
||||
def has_properties(self, properties: Characteristic.Properties) -> bool:
|
||||
return self.properties & properties == properties
|
||||
|
||||
def __str__(self):
|
||||
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)})'
|
||||
return (
|
||||
f'Characteristic(handle=0x{self.handle:04X}, '
|
||||
f'end=0x{self.end_group_handle:04X}, '
|
||||
f'uuid={self.uuid}, '
|
||||
f'{self.properties!s})'
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class CharacteristicDeclaration(Attribute):
|
||||
'''
|
||||
See Vol 3, Part G - 3.3.1 CHARACTERISTIC DECLARATION
|
||||
'''
|
||||
|
||||
characteristic: Characteristic
|
||||
|
||||
def __init__(self, characteristic, value_handle):
|
||||
declaration_bytes = (
|
||||
struct.pack('<BH', characteristic.properties, value_handle)
|
||||
+ characteristic.uuid.to_pdu_bytes()
|
||||
)
|
||||
super().__init__(
|
||||
GATT_CHARACTERISTIC_ATTRIBUTE_TYPE, Attribute.READABLE, declaration_bytes
|
||||
)
|
||||
self.value_handle = value_handle
|
||||
self.characteristic = characteristic
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f'CharacteristicDeclaration(handle=0x{self.handle:04X}, '
|
||||
f'value_handle=0x{self.value_handle:04X}, '
|
||||
f'uuid={self.characteristic.uuid}, '
|
||||
f'{self.characteristic.properties!s})'
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -275,6 +376,7 @@ 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):
|
||||
self._read = read
|
||||
self._write = write
|
||||
@@ -301,18 +403,18 @@ class CharacteristicAdapter:
|
||||
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
|
||||
self.subscribers = {} # Map from subscriber to proxy subscriber
|
||||
|
||||
if (
|
||||
asyncio.iscoroutinefunction(characteristic.read_value) and
|
||||
asyncio.iscoroutinefunction(characteristic.write_value)
|
||||
):
|
||||
self.read_value = self.read_decoded_value
|
||||
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.read_value = self.read_encoded_value
|
||||
self.write_value = self.write_encoded_value
|
||||
|
||||
if hasattr(self.wrapped_characteristic, 'subscribe'):
|
||||
@@ -325,14 +427,14 @@ class CharacteristicAdapter:
|
||||
return getattr(self.wrapped_characteristic, name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
if name in {
|
||||
if name in (
|
||||
'wrapped_characteristic',
|
||||
'subscribers',
|
||||
'read_value',
|
||||
'write_value',
|
||||
'subscribe',
|
||||
'unsubscribe'
|
||||
}:
|
||||
'unsubscribe',
|
||||
):
|
||||
super().__setattr__(name, value)
|
||||
else:
|
||||
setattr(self.wrapped_characteristic, name, value)
|
||||
@@ -341,13 +443,17 @@ class CharacteristicAdapter:
|
||||
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))
|
||||
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))
|
||||
async def write_decoded_value(self, value, with_response=False):
|
||||
return await self.wrapped_characteristic.write_value(
|
||||
self.encode_value(value), with_response
|
||||
)
|
||||
|
||||
def encode_value(self, value):
|
||||
return value
|
||||
@@ -366,6 +472,7 @@ class CharacteristicAdapter:
|
||||
|
||||
def on_change(value):
|
||||
original_subscriber(self.decode_value(value))
|
||||
|
||||
self.subscribers[subscriber] = on_change
|
||||
subscriber = on_change
|
||||
|
||||
@@ -387,6 +494,7 @@ 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
|
||||
@@ -409,9 +517,10 @@ class PackedCharacteristicAdapter(CharacteristicAdapter):
|
||||
they return/accept a tuple with the same number of elements as is required for
|
||||
the format.
|
||||
'''
|
||||
def __init__(self, characteristic, format):
|
||||
|
||||
def __init__(self, characteristic, pack_format):
|
||||
super().__init__(characteristic)
|
||||
self.struct = struct.Struct(format)
|
||||
self.struct = struct.Struct(pack_format)
|
||||
|
||||
def pack(self, *values):
|
||||
return self.struct.pack(*values)
|
||||
@@ -420,7 +529,7 @@ class PackedCharacteristicAdapter(CharacteristicAdapter):
|
||||
return self.struct.unpack(buffer)
|
||||
|
||||
def encode_value(self, value):
|
||||
return self.pack(*value if type(value) is tuple else (value,))
|
||||
return self.pack(*value if isinstance(value, tuple) else (value,))
|
||||
|
||||
def decode_value(self, value):
|
||||
unpacked = self.unpack(value)
|
||||
@@ -433,13 +542,15 @@ 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.
|
||||
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)
|
||||
|
||||
def __init__(self, characteristic, pack_format, keys):
|
||||
super().__init__(characteristic, pack_format)
|
||||
self.keys = keys
|
||||
|
||||
# pylint: disable=arguments-differ
|
||||
def pack(self, values):
|
||||
return super().pack(*(values[key] for key in self.keys))
|
||||
|
||||
@@ -452,6 +563,7 @@ class UTF8CharacteristicAdapter(CharacteristicAdapter):
|
||||
'''
|
||||
Adapter that converts strings to/from bytes using UTF-8 encoding
|
||||
'''
|
||||
|
||||
def encode_value(self, value):
|
||||
return value.encode('utf-8')
|
||||
|
||||
@@ -465,8 +577,20 @@ class Descriptor(Attribute):
|
||||
See Vol 3, Part G - 3.3.3 Characteristic Descriptor Declarations
|
||||
'''
|
||||
|
||||
def __init__(self, descriptor_type, permissions, value = b''):
|
||||
super().__init__(descriptor_type, permissions, value)
|
||||
|
||||
def __str__(self):
|
||||
return f'Descriptor(handle=0x{self.handle:04X}, type={self.type}, value={self.read_value(None).hex()})'
|
||||
return (
|
||||
f'Descriptor(handle=0x{self.handle:04X}, '
|
||||
f'type={self.type}, '
|
||||
f'value={self.read_value(None).hex()})'
|
||||
)
|
||||
|
||||
|
||||
class ClientCharacteristicConfigurationBits(enum.IntFlag):
|
||||
'''
|
||||
See Vol 3, Part G - 3.3.3.3 - Table 3.11 Client Characteristic Configuration bit
|
||||
field definition
|
||||
'''
|
||||
|
||||
DEFAULT = 0x0000
|
||||
NOTIFICATION = 0x0001
|
||||
INDICATION = 0x0002
|
||||
|
||||
@@ -23,21 +23,48 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
import asyncio
|
||||
import logging
|
||||
import struct
|
||||
from colors import color
|
||||
from datetime import datetime
|
||||
from typing import List, Optional, Dict, Tuple, Callable, Union, Any
|
||||
|
||||
from .core import ProtocolError, TimeoutError
|
||||
from .hci import *
|
||||
from .att import *
|
||||
from pyee import EventEmitter
|
||||
|
||||
from .colors import color
|
||||
from .hci import HCI_Constant
|
||||
from .att import (
|
||||
ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
||||
ATT_ATTRIBUTE_NOT_LONG_ERROR,
|
||||
ATT_CID,
|
||||
ATT_DEFAULT_MTU,
|
||||
ATT_ERROR_RESPONSE,
|
||||
ATT_INVALID_OFFSET_ERROR,
|
||||
ATT_PDU,
|
||||
ATT_RESPONSES,
|
||||
ATT_Exchange_MTU_Request,
|
||||
ATT_Find_By_Type_Value_Request,
|
||||
ATT_Find_Information_Request,
|
||||
ATT_Handle_Value_Confirmation,
|
||||
ATT_Read_Blob_Request,
|
||||
ATT_Read_By_Group_Type_Request,
|
||||
ATT_Read_By_Type_Request,
|
||||
ATT_Read_Request,
|
||||
ATT_Write_Command,
|
||||
ATT_Write_Request,
|
||||
ATT_Error,
|
||||
)
|
||||
from . import core
|
||||
from .core import UUID, InvalidStateError, ProtocolError
|
||||
from .gatt import (
|
||||
GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
|
||||
GATT_REQUEST_TIMEOUT,
|
||||
GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
|
||||
GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE,
|
||||
GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
|
||||
Characteristic
|
||||
GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
|
||||
GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
|
||||
GATT_REQUEST_TIMEOUT,
|
||||
GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE,
|
||||
Characteristic,
|
||||
ClientCharacteristicConfigurationBits,
|
||||
)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -50,18 +77,24 @@ logger = logging.getLogger(__name__)
|
||||
# Proxies
|
||||
# -----------------------------------------------------------------------------
|
||||
class AttributeProxy(EventEmitter):
|
||||
client: Client
|
||||
|
||||
def __init__(self, client, handle, end_group_handle, attribute_type):
|
||||
EventEmitter.__init__(self)
|
||||
self.client = client
|
||||
self.handle = handle
|
||||
self.client = client
|
||||
self.handle = handle
|
||||
self.end_group_handle = end_group_handle
|
||||
self.type = attribute_type
|
||||
self.type = attribute_type
|
||||
|
||||
async def read_value(self, no_long_read=False):
|
||||
return self.decode_value(await self.client.read_value(self.handle, no_long_read))
|
||||
return self.decode_value(
|
||||
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, self.encode_value(value), with_response)
|
||||
return await self.client.write_value(
|
||||
self.handle, self.encode_value(value), with_response
|
||||
)
|
||||
|
||||
def encode_value(self, value):
|
||||
return value
|
||||
@@ -70,24 +103,32 @@ class AttributeProxy(EventEmitter):
|
||||
return value_bytes
|
||||
|
||||
def __str__(self):
|
||||
return f'Attribute(handle=0x{self.handle:04X}, type={self.uuid})'
|
||||
return f'Attribute(handle=0x{self.handle:04X}, type={self.type})'
|
||||
|
||||
|
||||
class ServiceProxy(AttributeProxy):
|
||||
uuid: UUID
|
||||
characteristics: List[CharacteristicProxy]
|
||||
|
||||
@staticmethod
|
||||
def from_client(cls, client, service_uuid):
|
||||
# The service and its characteristics are considered to have already been discovered
|
||||
def from_client(service_class, 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
|
||||
return service_class(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
|
||||
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.uuid = uuid
|
||||
self.characteristics = []
|
||||
|
||||
async def discover_characteristics(self, uuids=[]):
|
||||
async def discover_characteristics(self, uuids=()):
|
||||
return await self.client.discover_characteristics(uuids, self)
|
||||
|
||||
def get_characteristics_by_uuid(self, uuid):
|
||||
@@ -98,23 +139,38 @@ class ServiceProxy(AttributeProxy):
|
||||
|
||||
|
||||
class CharacteristicProxy(AttributeProxy):
|
||||
def __init__(self, client, handle, end_group_handle, uuid, properties):
|
||||
properties: Characteristic.Properties
|
||||
descriptors: List[DescriptorProxy]
|
||||
subscribers: Dict[Any, Callable]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
client,
|
||||
handle,
|
||||
end_group_handle,
|
||||
uuid,
|
||||
properties: int,
|
||||
):
|
||||
super().__init__(client, handle, end_group_handle, uuid)
|
||||
self.uuid = uuid
|
||||
self.properties = properties
|
||||
self.descriptors = []
|
||||
self.uuid = uuid
|
||||
self.properties = Characteristic.Properties(properties)
|
||||
self.descriptors = []
|
||||
self.descriptors_discovered = False
|
||||
self.subscribers = {} # Map from subscriber to proxy subscriber
|
||||
self.subscribers = {} # Map from subscriber to proxy subscriber
|
||||
|
||||
def get_descriptor(self, descriptor_type):
|
||||
for descriptor in self.descriptors:
|
||||
if descriptor.type == descriptor_type:
|
||||
return descriptor
|
||||
|
||||
return None
|
||||
|
||||
async def discover_descriptors(self):
|
||||
return await self.client.discover_descriptors(self)
|
||||
|
||||
async def subscribe(self, subscriber=None):
|
||||
async def subscribe(
|
||||
self, subscriber: Optional[Callable] = None, prefer_notify=True
|
||||
):
|
||||
if subscriber is not None:
|
||||
if subscriber in self.subscribers:
|
||||
# We already have a proxy subscriber
|
||||
@@ -125,10 +181,11 @@ class CharacteristicProxy(AttributeProxy):
|
||||
|
||||
def on_change(value):
|
||||
original_subscriber(self.decode_value(value))
|
||||
|
||||
self.subscribers[subscriber] = on_change
|
||||
subscriber = on_change
|
||||
|
||||
return await self.client.subscribe(self, subscriber)
|
||||
return await self.client.subscribe(self, subscriber, prefer_notify)
|
||||
|
||||
async def unsubscribe(self, subscriber=None):
|
||||
if subscriber in self.subscribers:
|
||||
@@ -137,7 +194,11 @@ class CharacteristicProxy(AttributeProxy):
|
||||
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)})'
|
||||
return (
|
||||
f'Characteristic(handle=0x{self.handle:04X}, '
|
||||
f'uuid={self.uuid}, '
|
||||
f'{self.properties!s})'
|
||||
)
|
||||
|
||||
|
||||
class DescriptorProxy(AttributeProxy):
|
||||
@@ -152,6 +213,7 @@ 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)
|
||||
@@ -161,50 +223,65 @@ class ProfileServiceProxy:
|
||||
# GATT Client
|
||||
# -----------------------------------------------------------------------------
|
||||
class Client:
|
||||
services: List[ServiceProxy]
|
||||
cached_values: Dict[int, Tuple[datetime, bytes]]
|
||||
|
||||
def __init__(self, connection):
|
||||
self.connection = connection
|
||||
self.mtu_exchange_done = False
|
||||
self.request_semaphore = asyncio.Semaphore(1)
|
||||
self.pending_request = None
|
||||
self.pending_response = None
|
||||
self.notification_subscribers = {} # Notification subscribers, by attribute handle
|
||||
self.indication_subscribers = {} # Indication subscribers, by attribute handle
|
||||
self.services = []
|
||||
self.connection = connection
|
||||
self.mtu_exchange_done = False
|
||||
self.request_semaphore = asyncio.Semaphore(1)
|
||||
self.pending_request = None
|
||||
self.pending_response = None
|
||||
self.notification_subscribers = (
|
||||
{}
|
||||
) # Notification subscribers, by attribute handle
|
||||
self.indication_subscribers = {} # Indication subscribers, by attribute handle
|
||||
self.services = []
|
||||
self.cached_values = {}
|
||||
|
||||
def send_gatt_pdu(self, pdu):
|
||||
self.connection.send_l2cap_pdu(ATT_CID, pdu)
|
||||
|
||||
async def send_command(self, command):
|
||||
logger.debug(f'GATT Command from client: [0x{self.connection.handle:04X}] {command}')
|
||||
logger.debug(
|
||||
f'GATT Command from client: [0x{self.connection.handle:04X}] {command}'
|
||||
)
|
||||
self.send_gatt_pdu(command.to_bytes())
|
||||
|
||||
async def send_request(self, request):
|
||||
logger.debug(f'GATT Request from client: [0x{self.connection.handle:04X}] {request}')
|
||||
logger.debug(
|
||||
f'GATT Request from client: [0x{self.connection.handle:04X}] {request}'
|
||||
)
|
||||
|
||||
# Wait until we can send (only one pending command at a time for the connection)
|
||||
response = None
|
||||
async with self.request_semaphore:
|
||||
assert(self.pending_request is None)
|
||||
assert(self.pending_response is None)
|
||||
assert self.pending_request is None
|
||||
assert self.pending_response is None
|
||||
|
||||
# Create a future value to hold the eventual response
|
||||
self.pending_response = asyncio.get_running_loop().create_future()
|
||||
self.pending_request = request
|
||||
self.pending_request = request
|
||||
|
||||
try:
|
||||
self.send_gatt_pdu(request.to_bytes())
|
||||
response = await asyncio.wait_for(self.pending_response, GATT_REQUEST_TIMEOUT)
|
||||
except asyncio.TimeoutError:
|
||||
response = await asyncio.wait_for(
|
||||
self.pending_response, GATT_REQUEST_TIMEOUT
|
||||
)
|
||||
except asyncio.TimeoutError as error:
|
||||
logger.warning(color('!!! GATT Request timeout', 'red'))
|
||||
raise TimeoutError(f'GATT timeout for {request.name}')
|
||||
raise core.TimeoutError(f'GATT timeout for {request.name}') from error
|
||||
finally:
|
||||
self.pending_request = None
|
||||
self.pending_request = None
|
||||
self.pending_response = None
|
||||
|
||||
return response
|
||||
|
||||
def send_confirmation(self, confirmation):
|
||||
logger.debug(f'GATT Confirmation from client: [0x{self.connection.handle:04X}] {confirmation}')
|
||||
logger.debug(
|
||||
f'GATT Confirmation from client: [0x{self.connection.handle:04X}] '
|
||||
f'{confirmation}'
|
||||
)
|
||||
self.send_gatt_pdu(confirmation.to_bytes())
|
||||
|
||||
async def request_mtu(self, mtu):
|
||||
@@ -220,13 +297,13 @@ class Client:
|
||||
|
||||
# Send the request
|
||||
self.mtu_exchange_done = True
|
||||
response = await self.send_request(ATT_Exchange_MTU_Request(client_rx_mtu = mtu))
|
||||
response = await self.send_request(ATT_Exchange_MTU_Request(client_rx_mtu=mtu))
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
raise ProtocolError(
|
||||
response.error_code,
|
||||
'att',
|
||||
ATT_PDU.error_name(response.error_code),
|
||||
response
|
||||
response,
|
||||
)
|
||||
|
||||
# Compute the final MTU
|
||||
@@ -237,12 +314,45 @@ class Client:
|
||||
def get_services_by_uuid(self, uuid):
|
||||
return [service for service in self.services if service.uuid == uuid]
|
||||
|
||||
def get_characteristics_by_uuid(self, uuid, service = None):
|
||||
def get_characteristics_by_uuid(self, uuid, service=None):
|
||||
services = [service] if service else self.services
|
||||
return [c for c in [c for s in services for c in s.characteristics] if c.uuid == uuid]
|
||||
return [
|
||||
c
|
||||
for c in [c for s in services for c in s.characteristics]
|
||||
if c.uuid == uuid
|
||||
]
|
||||
|
||||
def get_attribute_grouping(
|
||||
self, attribute_handle: int
|
||||
) -> Optional[
|
||||
Union[
|
||||
ServiceProxy,
|
||||
Tuple[ServiceProxy, CharacteristicProxy],
|
||||
Tuple[ServiceProxy, CharacteristicProxy, DescriptorProxy],
|
||||
]
|
||||
]:
|
||||
"""
|
||||
Get the attribute(s) associated with an attribute handle
|
||||
"""
|
||||
for service in self.services:
|
||||
if service.handle == attribute_handle:
|
||||
return service
|
||||
if service.handle <= attribute_handle <= service.end_group_handle:
|
||||
for characteristic in service.characteristics:
|
||||
if characteristic.handle == attribute_handle:
|
||||
return (service, characteristic)
|
||||
if (
|
||||
characteristic.handle
|
||||
<= attribute_handle
|
||||
<= characteristic.end_group_handle
|
||||
):
|
||||
for descriptor in characteristic.descriptors:
|
||||
if descriptor.handle == attribute_handle:
|
||||
return (service, characteristic, descriptor)
|
||||
return None
|
||||
|
||||
def on_service_discovered(self, service):
|
||||
''' Add a service to the service list if it wasn't already there '''
|
||||
'''Add a service to the service list if it wasn't already there'''
|
||||
already_known = False
|
||||
for existing_service in self.services:
|
||||
if existing_service.handle == service.handle:
|
||||
@@ -251,7 +361,7 @@ class Client:
|
||||
if not already_known:
|
||||
self.services.append(service)
|
||||
|
||||
async def discover_services(self, uuids = None):
|
||||
async def discover_services(self, uuids=None) -> List[ServiceProxy]:
|
||||
'''
|
||||
See Vol 3, Part G - 4.4.1 Discover All Primary Services
|
||||
'''
|
||||
@@ -260,9 +370,9 @@ class Client:
|
||||
while starting_handle < 0xFFFF:
|
||||
response = await self.send_request(
|
||||
ATT_Read_By_Group_Type_Request(
|
||||
starting_handle = starting_handle,
|
||||
ending_handle = 0xFFFF,
|
||||
attribute_group_type = GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE
|
||||
starting_handle=starting_handle,
|
||||
ending_handle=0xFFFF,
|
||||
attribute_group_type=GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
|
||||
)
|
||||
)
|
||||
if response is None:
|
||||
@@ -273,16 +383,30 @@ class Client:
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
# Unexpected end
|
||||
logger.waning(f'!!! unexpected error while discovering services: {HCI_Constant.error_name(response.error_code)}')
|
||||
# TODO raise appropriate exception
|
||||
return
|
||||
logger.warning(
|
||||
'!!! unexpected error while discovering services: '
|
||||
f'{HCI_Constant.error_name(response.error_code)}'
|
||||
)
|
||||
raise ATT_Error(
|
||||
error_code=response.error_code,
|
||||
message='Unexpected error while discovering services',
|
||||
)
|
||||
break
|
||||
|
||||
for attribute_handle, end_group_handle, attribute_value in response.attributes:
|
||||
if attribute_handle < starting_handle or end_group_handle < attribute_handle:
|
||||
for (
|
||||
attribute_handle,
|
||||
end_group_handle,
|
||||
attribute_value,
|
||||
) in response.attributes:
|
||||
if (
|
||||
attribute_handle < starting_handle
|
||||
or end_group_handle < attribute_handle
|
||||
):
|
||||
# Something's not right
|
||||
logger.warning(f'bogus handle values: {attribute_handle} {end_group_handle}')
|
||||
return
|
||||
logger.warning(
|
||||
f'bogus handle values: {attribute_handle} {end_group_handle}'
|
||||
)
|
||||
return []
|
||||
|
||||
# Create a service proxy for this service
|
||||
service = ServiceProxy(
|
||||
@@ -290,7 +414,7 @@ class Client:
|
||||
attribute_handle,
|
||||
end_group_handle,
|
||||
UUID.from_bytes(attribute_value),
|
||||
True
|
||||
True,
|
||||
)
|
||||
|
||||
# Filter out returned services based on the given uuids list
|
||||
@@ -315,7 +439,7 @@ class Client:
|
||||
'''
|
||||
|
||||
# Force uuid to be a UUID object
|
||||
if type(uuid) is str:
|
||||
if isinstance(uuid, str):
|
||||
uuid = UUID(uuid)
|
||||
|
||||
starting_handle = 0x0001
|
||||
@@ -323,10 +447,10 @@ class Client:
|
||||
while starting_handle < 0xFFFF:
|
||||
response = await self.send_request(
|
||||
ATT_Find_By_Type_Value_Request(
|
||||
starting_handle = starting_handle,
|
||||
ending_handle = 0xFFFF,
|
||||
attribute_type = GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
|
||||
attribute_value = uuid.to_pdu_bytes()
|
||||
starting_handle=starting_handle,
|
||||
ending_handle=0xFFFF,
|
||||
attribute_type=GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
|
||||
attribute_value=uuid.to_pdu_bytes(),
|
||||
)
|
||||
)
|
||||
if response is None:
|
||||
@@ -337,19 +461,29 @@ class Client:
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
# Unexpected end
|
||||
logger.waning(f'!!! unexpected error while discovering services: {HCI_Constant.error_name(response.error_code)}')
|
||||
logger.warning(
|
||||
'!!! unexpected error while discovering services: '
|
||||
f'{HCI_Constant.error_name(response.error_code)}'
|
||||
)
|
||||
# TODO raise appropriate exception
|
||||
return
|
||||
break
|
||||
|
||||
for attribute_handle, end_group_handle in response.handles_information:
|
||||
if attribute_handle < starting_handle or end_group_handle < attribute_handle:
|
||||
if (
|
||||
attribute_handle < starting_handle
|
||||
or end_group_handle < attribute_handle
|
||||
):
|
||||
# Something's not right
|
||||
logger.warning(f'bogus handle values: {attribute_handle} {end_group_handle}')
|
||||
logger.warning(
|
||||
f'bogus handle values: {attribute_handle} {end_group_handle}'
|
||||
)
|
||||
return
|
||||
|
||||
# Create a service proxy for this service
|
||||
service = ServiceProxy(self, attribute_handle, end_group_handle, uuid, True)
|
||||
service = ServiceProxy(
|
||||
self, attribute_handle, end_group_handle, uuid, True
|
||||
)
|
||||
|
||||
# Add the service to the peer's service list
|
||||
services.append(service)
|
||||
@@ -368,37 +502,40 @@ class Client:
|
||||
|
||||
return services
|
||||
|
||||
async def discover_included_services(self, service):
|
||||
async def discover_included_services(self, _service):
|
||||
'''
|
||||
See Vol 3, Part G - 4.5.1 Find Included Services
|
||||
'''
|
||||
# TODO
|
||||
return []
|
||||
|
||||
async def discover_characteristics(self, uuids, service):
|
||||
async def discover_characteristics(
|
||||
self, uuids, service: Optional[ServiceProxy]
|
||||
) -> List[CharacteristicProxy]:
|
||||
'''
|
||||
See Vol 3, Part G - 4.6.1 Discover All Characteristics of a Service and 4.6.2 Discover Characteristics by UUID
|
||||
See Vol 3, Part G - 4.6.1 Discover All Characteristics of a Service and 4.6.2
|
||||
Discover Characteristics by UUID
|
||||
'''
|
||||
|
||||
# Cast the UUIDs type from string to object if needed
|
||||
uuids = [UUID(uuid) if type(uuid) is str else uuid for uuid in uuids]
|
||||
uuids = [UUID(uuid) if isinstance(uuid, str) else uuid for uuid in uuids]
|
||||
|
||||
# Decide which services to discover for
|
||||
services = [service] if service else self.services
|
||||
|
||||
# Perform characteristic discovery for each service
|
||||
discovered_characteristics = []
|
||||
discovered_characteristics: List[CharacteristicProxy] = []
|
||||
for service in services:
|
||||
starting_handle = service.handle
|
||||
ending_handle = service.end_group_handle
|
||||
ending_handle = service.end_group_handle
|
||||
|
||||
characteristics = []
|
||||
characteristics: List[CharacteristicProxy] = []
|
||||
while starting_handle <= ending_handle:
|
||||
response = await self.send_request(
|
||||
ATT_Read_By_Type_Request(
|
||||
starting_handle = starting_handle,
|
||||
ending_handle = ending_handle,
|
||||
attribute_type = GATT_CHARACTERISTIC_ATTRIBUTE_TYPE
|
||||
starting_handle=starting_handle,
|
||||
ending_handle=ending_handle,
|
||||
attribute_type=GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
|
||||
)
|
||||
)
|
||||
if response is None:
|
||||
@@ -409,9 +546,14 @@ class Client:
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
# Unexpected end
|
||||
logger.warning(f'!!! unexpected error while discovering characteristics: {HCI_Constant.error_name(response.error_code)}')
|
||||
# TODO raise appropriate exception
|
||||
return
|
||||
logger.warning(
|
||||
'!!! unexpected error while discovering characteristics: '
|
||||
f'{HCI_Constant.error_name(response.error_code)}'
|
||||
)
|
||||
raise ATT_Error(
|
||||
error_code=response.error_code,
|
||||
message='Unexpected error while discovering characteristics',
|
||||
)
|
||||
break
|
||||
|
||||
# Stop if for some reason the list was empty
|
||||
@@ -427,7 +569,9 @@ class Client:
|
||||
|
||||
properties, handle = struct.unpack_from('<BH', attribute_value)
|
||||
characteristic_uuid = UUID.from_bytes(attribute_value[3:])
|
||||
characteristic = CharacteristicProxy(self, handle, 0, characteristic_uuid, properties)
|
||||
characteristic = CharacteristicProxy(
|
||||
self, handle, 0, characteristic_uuid, properties
|
||||
)
|
||||
|
||||
# Set the previous characteristic's end handle
|
||||
if characteristics:
|
||||
@@ -443,31 +587,37 @@ class Client:
|
||||
characteristics[-1].end_group_handle = service.end_group_handle
|
||||
|
||||
# Set the service's characteristics
|
||||
characteristics = [c for c in characteristics if not uuids or c.uuid in uuids]
|
||||
characteristics = [
|
||||
c for c in characteristics if not uuids or c.uuid in uuids
|
||||
]
|
||||
service.characteristics = characteristics
|
||||
discovered_characteristics.extend(characteristics)
|
||||
|
||||
return discovered_characteristics
|
||||
|
||||
async def discover_descriptors(self, characteristic = None, start_handle = None, end_handle = None):
|
||||
async def discover_descriptors(
|
||||
self,
|
||||
characteristic: Optional[CharacteristicProxy] = None,
|
||||
start_handle=None,
|
||||
end_handle=None,
|
||||
) -> List[DescriptorProxy]:
|
||||
'''
|
||||
See Vol 3, Part G - 4.7.1 Discover All Characteristic Descriptors
|
||||
'''
|
||||
if characteristic:
|
||||
starting_handle = characteristic.handle + 1
|
||||
ending_handle = characteristic.end_group_handle
|
||||
ending_handle = characteristic.end_group_handle
|
||||
elif start_handle and end_handle:
|
||||
starting_handle = start_handle
|
||||
ending_handle = end_handle
|
||||
ending_handle = end_handle
|
||||
else:
|
||||
return []
|
||||
|
||||
descriptors = []
|
||||
descriptors: List[DescriptorProxy] = []
|
||||
while starting_handle <= ending_handle:
|
||||
response = await self.send_request(
|
||||
ATT_Find_Information_Request(
|
||||
starting_handle = starting_handle,
|
||||
ending_handle = ending_handle
|
||||
starting_handle=starting_handle, ending_handle=ending_handle
|
||||
)
|
||||
)
|
||||
if response is None:
|
||||
@@ -478,7 +628,10 @@ class Client:
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
# Unexpected end
|
||||
logger.warning(f'!!! unexpected error while discovering descriptors: {HCI_Constant.error_name(response.error_code)}')
|
||||
logger.warning(
|
||||
'!!! unexpected error while discovering descriptors: '
|
||||
f'{HCI_Constant.error_name(response.error_code)}'
|
||||
)
|
||||
# TODO raise appropriate exception
|
||||
return []
|
||||
break
|
||||
@@ -494,7 +647,9 @@ class Client:
|
||||
logger.warning(f'bogus handle value: {attribute_handle}')
|
||||
return []
|
||||
|
||||
descriptor = DescriptorProxy(self, attribute_handle, UUID.from_bytes(attribute_uuid))
|
||||
descriptor = DescriptorProxy(
|
||||
self, attribute_handle, UUID.from_bytes(attribute_uuid)
|
||||
)
|
||||
descriptors.append(descriptor)
|
||||
# TODO: read descriptor value
|
||||
|
||||
@@ -512,13 +667,12 @@ class Client:
|
||||
Discover all attributes, regardless of type
|
||||
'''
|
||||
starting_handle = 0x0001
|
||||
ending_handle = 0xFFFF
|
||||
ending_handle = 0xFFFF
|
||||
attributes = []
|
||||
while True:
|
||||
response = await self.send_request(
|
||||
ATT_Find_Information_Request(
|
||||
starting_handle = starting_handle,
|
||||
ending_handle = ending_handle
|
||||
starting_handle=starting_handle, ending_handle=ending_handle
|
||||
)
|
||||
)
|
||||
if response is None:
|
||||
@@ -528,7 +682,10 @@ class Client:
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
# Unexpected end
|
||||
logger.warning(f'!!! unexpected error while discovering attributes: {HCI_Constant.error_name(response.error_code)}')
|
||||
logger.warning(
|
||||
'!!! unexpected error while discovering attributes: '
|
||||
f'{HCI_Constant.error_name(response.error_code)}'
|
||||
)
|
||||
return []
|
||||
break
|
||||
|
||||
@@ -538,7 +695,9 @@ class Client:
|
||||
logger.warning(f'bogus handle value: {attribute_handle}')
|
||||
return []
|
||||
|
||||
attribute = AttributeProxy(self, attribute_handle, 0, UUID.from_bytes(attribute_uuid))
|
||||
attribute = AttributeProxy(
|
||||
self, attribute_handle, 0, UUID.from_bytes(attribute_uuid)
|
||||
)
|
||||
attributes.append(attribute)
|
||||
|
||||
# Move on to the next attributes
|
||||
@@ -546,58 +705,77 @@ class Client:
|
||||
|
||||
return attributes
|
||||
|
||||
async def subscribe(self, characteristic, subscriber=None):
|
||||
# If we haven't already discovered the descriptors for this characteristic, do it now
|
||||
async def subscribe(self, characteristic, subscriber=None, prefer_notify=True):
|
||||
# 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)
|
||||
cccd = characteristic.get_descriptor(
|
||||
GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR
|
||||
)
|
||||
if not cccd:
|
||||
logger.warning('subscribing to characteristic with no CCCD descriptor')
|
||||
return
|
||||
|
||||
# Set the subscription bits and select the subscriber set
|
||||
bits = 0
|
||||
subscriber_sets = []
|
||||
if characteristic.properties & Characteristic.NOTIFY:
|
||||
bits |= 0x0001
|
||||
subscriber_sets.append(self.notification_subscribers.setdefault(characteristic.handle, set()))
|
||||
if characteristic.properties & Characteristic.INDICATE:
|
||||
bits |= 0x0002
|
||||
subscriber_sets.append(self.indication_subscribers.setdefault(characteristic.handle, set()))
|
||||
if (
|
||||
characteristic.properties & Characteristic.Properties.NOTIFY
|
||||
and characteristic.properties & Characteristic.Properties.INDICATE
|
||||
):
|
||||
if prefer_notify:
|
||||
bits = ClientCharacteristicConfigurationBits.NOTIFICATION
|
||||
subscribers = self.notification_subscribers
|
||||
else:
|
||||
bits = ClientCharacteristicConfigurationBits.INDICATION
|
||||
subscribers = self.indication_subscribers
|
||||
elif characteristic.properties & Characteristic.Properties.NOTIFY:
|
||||
bits = ClientCharacteristicConfigurationBits.NOTIFICATION
|
||||
subscribers = self.notification_subscribers
|
||||
elif characteristic.properties & Characteristic.Properties.INDICATE:
|
||||
bits = ClientCharacteristicConfigurationBits.INDICATION
|
||||
subscribers = self.indication_subscribers
|
||||
else:
|
||||
raise InvalidStateError("characteristic is not notify or indicate")
|
||||
|
||||
# Add subscribers to the sets
|
||||
for subscriber_set in subscriber_sets:
|
||||
if subscriber is not None:
|
||||
subscriber_set.add(subscriber)
|
||||
# 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)
|
||||
subscriber_set = subscribers.setdefault(characteristic.handle, set())
|
||||
if subscriber is not None:
|
||||
subscriber_set.add(subscriber)
|
||||
# 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)
|
||||
|
||||
async def unsubscribe(self, characteristic, subscriber=None):
|
||||
# If we haven't already discovered the descriptors for this characteristic, do it now
|
||||
# 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)
|
||||
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):
|
||||
for subscriber_set in (
|
||||
self.notification_subscribers,
|
||||
self.indication_subscribers,
|
||||
):
|
||||
subscribers = subscriber_set.get(characteristic.handle, [])
|
||||
if subscriber in subscribers:
|
||||
subscribers.remove(subscriber)
|
||||
|
||||
# Cleanup if we removed the last one
|
||||
if not subscribers:
|
||||
subscriber_set.remove(characteristic.handle)
|
||||
del subscriber_set[characteristic.handle]
|
||||
else:
|
||||
# Remove all subscribers for this attribute from the sets!
|
||||
self.notification_subscribers.pop(characteristic.handle, None)
|
||||
@@ -615,8 +793,10 @@ class Client:
|
||||
'''
|
||||
|
||||
# Send a request to read
|
||||
attribute_handle = attribute if type(attribute) is int else attribute.handle
|
||||
response = await self.send_request(ATT_Read_Request(attribute_handle = attribute_handle))
|
||||
attribute_handle = attribute if isinstance(attribute, int) else attribute.handle
|
||||
response = await self.send_request(
|
||||
ATT_Read_Request(attribute_handle=attribute_handle)
|
||||
)
|
||||
if response is None:
|
||||
raise TimeoutError('read timeout')
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
@@ -624,7 +804,7 @@ class Client:
|
||||
response.error_code,
|
||||
'att',
|
||||
ATT_PDU.error_name(response.error_code),
|
||||
response
|
||||
response,
|
||||
)
|
||||
|
||||
# If the value is the max size for the MTU, try to read more unless the caller
|
||||
@@ -635,18 +815,23 @@ class Client:
|
||||
offset = len(attribute_value)
|
||||
while True:
|
||||
response = await self.send_request(
|
||||
ATT_Read_Blob_Request(attribute_handle = attribute_handle, value_offset = offset)
|
||||
ATT_Read_Blob_Request(
|
||||
attribute_handle=attribute_handle, value_offset=offset
|
||||
)
|
||||
)
|
||||
if response is None:
|
||||
raise TimeoutError('read timeout')
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
if response.error_code == ATT_ATTRIBUTE_NOT_LONG_ERROR or response.error_code == ATT_INVALID_OFFSET_ERROR:
|
||||
if response.error_code in (
|
||||
ATT_ATTRIBUTE_NOT_LONG_ERROR,
|
||||
ATT_INVALID_OFFSET_ERROR,
|
||||
):
|
||||
break
|
||||
raise ProtocolError(
|
||||
response.error_code,
|
||||
'att',
|
||||
ATT_PDU.error_name(response.error_code),
|
||||
response
|
||||
response,
|
||||
)
|
||||
|
||||
part = response.part_attribute_value
|
||||
@@ -657,6 +842,7 @@ class Client:
|
||||
|
||||
offset += len(part)
|
||||
|
||||
self.cache_value(attribute_handle, attribute_value)
|
||||
# Return the value as bytes
|
||||
return attribute_value
|
||||
|
||||
@@ -667,18 +853,18 @@ class Client:
|
||||
|
||||
if service is None:
|
||||
starting_handle = 0x0001
|
||||
ending_handle = 0xFFFF
|
||||
ending_handle = 0xFFFF
|
||||
else:
|
||||
starting_handle = service.handle
|
||||
ending_handle = service.end_group_handle
|
||||
ending_handle = service.end_group_handle
|
||||
|
||||
characteristics_values = []
|
||||
while starting_handle <= ending_handle:
|
||||
response = await self.send_request(
|
||||
ATT_Read_By_Type_Request(
|
||||
starting_handle = starting_handle,
|
||||
ending_handle = ending_handle,
|
||||
attribute_type = uuid
|
||||
starting_handle=starting_handle,
|
||||
ending_handle=ending_handle,
|
||||
attribute_type=uuid,
|
||||
)
|
||||
)
|
||||
if response is None:
|
||||
@@ -689,7 +875,10 @@ class Client:
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
# Unexpected end
|
||||
logger.warning(f'!!! unexpected error while reading characteristics: {HCI_Constant.error_name(response.error_code)}')
|
||||
logger.warning(
|
||||
'!!! unexpected error while reading characteristics: '
|
||||
f'{HCI_Constant.error_name(response.error_code)}'
|
||||
)
|
||||
# TODO raise appropriate exception
|
||||
return []
|
||||
break
|
||||
@@ -714,47 +903,54 @@ class Client:
|
||||
|
||||
async def write_value(self, attribute, value, with_response=False):
|
||||
'''
|
||||
See Vol 3, Part G - 4.9.1 Write Without Response & 4.9.3 Write Characteristic Value
|
||||
See Vol 3, Part G - 4.9.1 Write Without Response & 4.9.3 Write Characteristic
|
||||
Value
|
||||
|
||||
`attribute` can be an Attribute object, or a handle value
|
||||
'''
|
||||
|
||||
# Send a request or command to write
|
||||
attribute_handle = attribute if type(attribute) is int else attribute.handle
|
||||
attribute_handle = attribute if isinstance(attribute, int) else attribute.handle
|
||||
if with_response:
|
||||
response = await self.send_request(
|
||||
ATT_Write_Request(
|
||||
attribute_handle = attribute_handle,
|
||||
attribute_value = value
|
||||
attribute_handle=attribute_handle, attribute_value=value
|
||||
)
|
||||
)
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
raise ProtocolError(
|
||||
response.error_code,
|
||||
'att',
|
||||
ATT_PDU.error_name(response.error_code), response
|
||||
ATT_PDU.error_name(response.error_code),
|
||||
response,
|
||||
)
|
||||
else:
|
||||
await self.send_command(
|
||||
ATT_Write_Command(
|
||||
attribute_handle = attribute_handle,
|
||||
attribute_value = value
|
||||
attribute_handle=attribute_handle, attribute_value=value
|
||||
)
|
||||
)
|
||||
|
||||
def on_gatt_pdu(self, att_pdu):
|
||||
logger.debug(f'GATT Response to client: [0x{self.connection.handle:04X}] {att_pdu}')
|
||||
logger.debug(
|
||||
f'GATT Response to client: [0x{self.connection.handle:04X}] {att_pdu}'
|
||||
)
|
||||
if att_pdu.op_code in ATT_RESPONSES:
|
||||
if self.pending_request is None:
|
||||
# Not expected!
|
||||
logger.warning('!!! unexpected response, there is no pending request')
|
||||
return
|
||||
|
||||
# Sanity check: the response should match the pending request unless it is an error response
|
||||
# Sanity check: the response should match the pending request unless it is
|
||||
# an error response
|
||||
if att_pdu.op_code != ATT_ERROR_RESPONSE:
|
||||
expected_response_name = self.pending_request.name.replace('_REQUEST', '_RESPONSE')
|
||||
expected_response_name = self.pending_request.name.replace(
|
||||
'_REQUEST', '_RESPONSE'
|
||||
)
|
||||
if att_pdu.name != expected_response_name:
|
||||
logger.warning(f'!!! mismatched response: expected {expected_response_name}')
|
||||
logger.warning(
|
||||
f'!!! mismatched response: expected {expected_response_name}'
|
||||
)
|
||||
return
|
||||
|
||||
# Return the response to the coroutine that is waiting for it
|
||||
@@ -765,13 +961,24 @@ class Client:
|
||||
if handler is not None:
|
||||
handler(att_pdu)
|
||||
else:
|
||||
logger.warning(f'{color(f"--- Ignoring GATT Response from [0x{self.connection.handle:04X}]:", "red")} {att_pdu}')
|
||||
logger.warning(
|
||||
color(
|
||||
'--- Ignoring GATT Response from '
|
||||
f'[0x{self.connection.handle:04X}]: ',
|
||||
'red',
|
||||
)
|
||||
+ str(att_pdu)
|
||||
)
|
||||
|
||||
def on_att_handle_value_notification(self, notification):
|
||||
# Call all subscribers
|
||||
subscribers = self.notification_subscribers.get(notification.attribute_handle, [])
|
||||
subscribers = self.notification_subscribers.get(
|
||||
notification.attribute_handle, []
|
||||
)
|
||||
if not subscribers:
|
||||
logger.warning('!!! received notification with no subscriber')
|
||||
|
||||
self.cache_value(notification.attribute_handle, notification.attribute_value)
|
||||
for subscriber in subscribers:
|
||||
if callable(subscriber):
|
||||
subscriber(notification.attribute_value)
|
||||
@@ -783,6 +990,8 @@ class Client:
|
||||
subscribers = self.indication_subscribers.get(indication.attribute_handle, [])
|
||||
if not subscribers:
|
||||
logger.warning('!!! received indication with no subscriber')
|
||||
|
||||
self.cache_value(indication.attribute_handle, indication.attribute_value)
|
||||
for subscriber in subscribers:
|
||||
if callable(subscriber):
|
||||
subscriber(indication.attribute_value)
|
||||
@@ -791,3 +1000,9 @@ class Client:
|
||||
|
||||
# Confirm that we received the indication
|
||||
self.send_confirmation(ATT_Handle_Value_Confirmation())
|
||||
|
||||
def cache_value(self, attribute_handle: int, value: bytes):
|
||||
self.cached_values[attribute_handle] = (
|
||||
datetime.now(),
|
||||
value,
|
||||
)
|
||||
|
||||
@@ -26,13 +26,52 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
import struct
|
||||
from typing import List, Tuple, Optional, TypeVar, Type
|
||||
from pyee import EventEmitter
|
||||
from colors import color
|
||||
|
||||
from .core import *
|
||||
from .hci import *
|
||||
from .att import *
|
||||
from .gatt import *
|
||||
from .colors import color
|
||||
from .core import UUID
|
||||
from .att import (
|
||||
ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
||||
ATT_ATTRIBUTE_NOT_LONG_ERROR,
|
||||
ATT_CID,
|
||||
ATT_DEFAULT_MTU,
|
||||
ATT_INVALID_ATTRIBUTE_LENGTH_ERROR,
|
||||
ATT_INVALID_HANDLE_ERROR,
|
||||
ATT_INVALID_OFFSET_ERROR,
|
||||
ATT_REQUEST_NOT_SUPPORTED_ERROR,
|
||||
ATT_REQUESTS,
|
||||
ATT_UNLIKELY_ERROR_ERROR,
|
||||
ATT_UNSUPPORTED_GROUP_TYPE_ERROR,
|
||||
ATT_Error,
|
||||
ATT_Error_Response,
|
||||
ATT_Exchange_MTU_Response,
|
||||
ATT_Find_By_Type_Value_Response,
|
||||
ATT_Find_Information_Response,
|
||||
ATT_Handle_Value_Indication,
|
||||
ATT_Handle_Value_Notification,
|
||||
ATT_Read_Blob_Response,
|
||||
ATT_Read_By_Group_Type_Response,
|
||||
ATT_Read_By_Type_Response,
|
||||
ATT_Read_Response,
|
||||
ATT_Write_Response,
|
||||
Attribute,
|
||||
)
|
||||
from .gatt import (
|
||||
GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
|
||||
GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
|
||||
GATT_MAX_ATTRIBUTE_VALUE_SIZE,
|
||||
GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
|
||||
GATT_REQUEST_TIMEOUT,
|
||||
GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE,
|
||||
Characteristic,
|
||||
CharacteristicDeclaration,
|
||||
CharacteristicValue,
|
||||
Descriptor,
|
||||
Service,
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -50,22 +89,39 @@ GATT_SERVER_DEFAULT_MAX_MTU = 517
|
||||
# GATT Server
|
||||
# -----------------------------------------------------------------------------
|
||||
class Server(EventEmitter):
|
||||
attributes: List[Attribute]
|
||||
|
||||
def __init__(self, device):
|
||||
super().__init__()
|
||||
self.device = device
|
||||
self.attributes = [] # Attributes, ordered by increasing handle values
|
||||
self.attributes_by_handle = {} # Map for fast attribute access by handle
|
||||
self.max_mtu = GATT_SERVER_DEFAULT_MAX_MTU # The max MTU we're willing to negotiate
|
||||
self.subscribers = {} # Map of subscriber states by connection handle and attribute handle
|
||||
self.device = device
|
||||
self.attributes = [] # Attributes, ordered by increasing handle values
|
||||
self.attributes_by_handle = {} # Map for fast attribute access by handle
|
||||
self.max_mtu = (
|
||||
GATT_SERVER_DEFAULT_MAX_MTU # The max MTU we're willing to negotiate
|
||||
)
|
||||
self.subscribers = (
|
||||
{}
|
||||
) # Map of subscriber states by connection handle and attribute handle
|
||||
self.indication_semaphores = defaultdict(lambda: asyncio.Semaphore(1))
|
||||
self.pending_confirmations = defaultdict(lambda: None)
|
||||
|
||||
def __str__(self):
|
||||
return "\n".join(map(str, self.attributes))
|
||||
|
||||
def send_gatt_pdu(self, connection_handle, pdu):
|
||||
self.device.send_l2cap_pdu(connection_handle, ATT_CID, pdu)
|
||||
|
||||
def next_handle(self):
|
||||
return 1 + len(self.attributes)
|
||||
|
||||
def get_advertising_service_data(self):
|
||||
return {
|
||||
attribute: data
|
||||
for attribute in self.attributes
|
||||
if isinstance(attribute, Service)
|
||||
and (data := attribute.get_advertising_data())
|
||||
}
|
||||
|
||||
def get_attribute(self, handle):
|
||||
attribute = self.attributes_by_handle.get(handle)
|
||||
if attribute:
|
||||
@@ -79,15 +135,90 @@ class Server(EventEmitter):
|
||||
return attribute
|
||||
return None
|
||||
|
||||
AttributeGroupType = TypeVar('AttributeGroupType', Service, Characteristic)
|
||||
|
||||
def get_attribute_group(
|
||||
self, handle: int, group_type: Type[AttributeGroupType]
|
||||
) -> Optional[AttributeGroupType]:
|
||||
return next(
|
||||
(
|
||||
attribute
|
||||
for attribute in self.attributes
|
||||
if isinstance(attribute, group_type)
|
||||
and attribute.handle <= handle <= attribute.end_group_handle
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
def get_service_attribute(self, service_uuid: UUID) -> Optional[Service]:
|
||||
return next(
|
||||
(
|
||||
attribute
|
||||
for attribute in self.attributes
|
||||
if attribute.type == GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE
|
||||
and isinstance(attribute, Service)
|
||||
and attribute.uuid == service_uuid
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
def get_characteristic_attributes(
|
||||
self, service_uuid: UUID, characteristic_uuid: UUID
|
||||
) -> Optional[Tuple[CharacteristicDeclaration, Characteristic]]:
|
||||
service_handle = self.get_service_attribute(service_uuid)
|
||||
if not service_handle:
|
||||
return None
|
||||
|
||||
return next(
|
||||
(
|
||||
(attribute, self.get_attribute(attribute.characteristic.handle))
|
||||
for attribute in map(
|
||||
self.get_attribute,
|
||||
range(service_handle.handle, service_handle.end_group_handle + 1),
|
||||
)
|
||||
if attribute.type == GATT_CHARACTERISTIC_ATTRIBUTE_TYPE
|
||||
and attribute.characteristic.uuid == characteristic_uuid
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
def get_descriptor_attribute(
|
||||
self, service_uuid: UUID, characteristic_uuid: UUID, descriptor_uuid: UUID
|
||||
) -> Optional[Descriptor]:
|
||||
characteristics = self.get_characteristic_attributes(
|
||||
service_uuid, characteristic_uuid
|
||||
)
|
||||
if not characteristics:
|
||||
return None
|
||||
|
||||
(_, characteristic_value) = characteristics
|
||||
|
||||
return next(
|
||||
(
|
||||
attribute
|
||||
for attribute in map(
|
||||
self.get_attribute,
|
||||
range(
|
||||
characteristic_value.handle + 1,
|
||||
characteristic_value.end_group_handle + 1,
|
||||
),
|
||||
)
|
||||
if attribute.type == descriptor_uuid
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
def add_attribute(self, attribute):
|
||||
# Assign a handle to this attribute
|
||||
attribute.handle = self.next_handle()
|
||||
attribute.end_group_handle = attribute.handle # TODO: keep track of descriptors in the group
|
||||
attribute.end_group_handle = (
|
||||
attribute.handle
|
||||
) # TODO: keep track of descriptors in the group
|
||||
|
||||
# Add this attribute to the list
|
||||
self.attributes.append(attribute)
|
||||
|
||||
def add_service(self, service):
|
||||
def add_service(self, service: Service):
|
||||
# Add the service attribute to the DB
|
||||
self.add_attribute(service)
|
||||
|
||||
@@ -95,16 +226,9 @@ class Server(EventEmitter):
|
||||
|
||||
# Add all characteristics
|
||||
for characteristic in service.characteristics:
|
||||
# Add a Characteristic Declaration (Vol 3, Part G - 3.3.1 Characteristic Declaration)
|
||||
declaration_bytes = struct.pack(
|
||||
'<BH',
|
||||
characteristic.properties,
|
||||
self.next_handle() + 1, # The value will be the next attribute after this declaration
|
||||
) + characteristic.uuid.to_pdu_bytes()
|
||||
characteristic_declaration = Attribute(
|
||||
GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
|
||||
Attribute.READABLE,
|
||||
declaration_bytes
|
||||
# Add a Characteristic Declaration
|
||||
characteristic_declaration = CharacteristicDeclaration(
|
||||
characteristic, self.next_handle() + 1
|
||||
)
|
||||
self.add_attribute(characteristic_declaration)
|
||||
|
||||
@@ -118,17 +242,29 @@ class Server(EventEmitter):
|
||||
# If the characteristic supports subscriptions, add a CCCD descriptor
|
||||
# unless there is one already
|
||||
if (
|
||||
characteristic.properties & (Characteristic.NOTIFY | Characteristic.INDICATE) and
|
||||
characteristic.get_descriptor(GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR) is None
|
||||
characteristic.properties
|
||||
& (
|
||||
Characteristic.Properties.NOTIFY
|
||||
| Characteristic.Properties.INDICATE
|
||||
)
|
||||
and characteristic.get_descriptor(
|
||||
GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR
|
||||
)
|
||||
is None
|
||||
):
|
||||
self.add_attribute(
|
||||
# pylint: disable=line-too-long
|
||||
Descriptor(
|
||||
GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
|
||||
Attribute.READABLE | Attribute.WRITEABLE,
|
||||
CharacteristicValue(
|
||||
read=lambda connection, characteristic=characteristic: self.read_cccd(connection, characteristic),
|
||||
write=lambda connection, value, characteristic=characteristic: self.write_cccd(connection, characteristic, value)
|
||||
)
|
||||
read=lambda connection, characteristic=characteristic: self.read_cccd(
|
||||
connection, characteristic
|
||||
),
|
||||
write=lambda connection, value, characteristic=characteristic: self.write_cccd(
|
||||
connection, characteristic, value
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -155,23 +291,36 @@ class Server(EventEmitter):
|
||||
return cccd or bytes([0, 0])
|
||||
|
||||
def write_cccd(self, connection, characteristic, value):
|
||||
logger.debug(f'Subscription update for connection={connection.handle:04X}, handle={characteristic.handle:04X}: {value.hex()}')
|
||||
logger.debug(
|
||||
f'Subscription update for connection=0x{connection.handle:04X}, '
|
||||
f'handle=0x{characteristic.handle:04X}: {value.hex()}'
|
||||
)
|
||||
|
||||
# Sanity check
|
||||
if len(value) != 2:
|
||||
logger.warn('CCCD value not 2 bytes long')
|
||||
logger.warning('CCCD value not 2 bytes long')
|
||||
return
|
||||
|
||||
cccds = self.subscribers.setdefault(connection.handle, {})
|
||||
cccds[characteristic.handle] = value
|
||||
logger.debug(f'CCCDs: {cccds}')
|
||||
notify_enabled = (value[0] & 0x01 != 0)
|
||||
indicate_enabled = (value[0] & 0x02 != 0)
|
||||
characteristic.emit('subscription', connection, notify_enabled, indicate_enabled)
|
||||
self.emit('characteristic_subscription', connection, characteristic, notify_enabled, indicate_enabled)
|
||||
notify_enabled = value[0] & 0x01 != 0
|
||||
indicate_enabled = value[0] & 0x02 != 0
|
||||
characteristic.emit(
|
||||
'subscription', connection, notify_enabled, indicate_enabled
|
||||
)
|
||||
self.emit(
|
||||
'characteristic_subscription',
|
||||
connection,
|
||||
characteristic,
|
||||
notify_enabled,
|
||||
indicate_enabled,
|
||||
)
|
||||
|
||||
def send_response(self, connection, response):
|
||||
logger.debug(f'GATT Response from server: [0x{connection.handle:04X}] {response}')
|
||||
logger.debug(
|
||||
f'GATT Response from server: [0x{connection.handle:04X}] {response}'
|
||||
)
|
||||
self.send_gatt_pdu(connection.handle, response.to_bytes())
|
||||
|
||||
async def notify_subscriber(self, connection, attribute, value=None, force=False):
|
||||
@@ -183,25 +332,32 @@ class Server(EventEmitter):
|
||||
return
|
||||
cccd = subscribers.get(attribute.handle)
|
||||
if not cccd:
|
||||
logger.debug(f'not notifying, no subscribers for handle {attribute.handle:04X}')
|
||||
logger.debug(
|
||||
f'not notifying, no subscribers for handle {attribute.handle:04X}'
|
||||
)
|
||||
return
|
||||
if len(cccd) != 2 or (cccd[0] & 0x01 == 0):
|
||||
logger.debug(f'not notifying, cccd={cccd.hex()}')
|
||||
return
|
||||
|
||||
# Get or encode the value
|
||||
value = attribute.read_value(connection) if value is None else attribute.encode_value(value)
|
||||
value = (
|
||||
attribute.read_value(connection)
|
||||
if value is None
|
||||
else attribute.encode_value(value)
|
||||
)
|
||||
|
||||
# Truncate if needed
|
||||
if len(value) > connection.att_mtu - 3:
|
||||
value = value[:connection.att_mtu - 3]
|
||||
value = value[: connection.att_mtu - 3]
|
||||
|
||||
# Notify
|
||||
notification = ATT_Handle_Value_Notification(
|
||||
attribute_handle = attribute.handle,
|
||||
attribute_value = value
|
||||
attribute_handle=attribute.handle, attribute_value=value
|
||||
)
|
||||
logger.debug(
|
||||
f'GATT Notify from server: [0x{connection.handle:04X}] {notification}'
|
||||
)
|
||||
logger.debug(f'GATT Notify from server: [0x{connection.handle:04X}] {notification}')
|
||||
self.send_gatt_pdu(connection.handle, bytes(notification))
|
||||
|
||||
async def indicate_subscriber(self, connection, attribute, value=None, force=False):
|
||||
@@ -213,46 +369,60 @@ class Server(EventEmitter):
|
||||
return
|
||||
cccd = subscribers.get(attribute.handle)
|
||||
if not cccd:
|
||||
logger.debug(f'not indicating, no subscribers for handle {attribute.handle:04X}')
|
||||
logger.debug(
|
||||
f'not indicating, no subscribers for handle {attribute.handle:04X}'
|
||||
)
|
||||
return
|
||||
if len(cccd) != 2 or (cccd[0] & 0x02 == 0):
|
||||
logger.debug(f'not indicating, cccd={cccd.hex()}')
|
||||
return
|
||||
|
||||
# Get or encode the value
|
||||
value = attribute.read_value(connection) if value is None else attribute.encode_value(value)
|
||||
value = (
|
||||
attribute.read_value(connection)
|
||||
if value is None
|
||||
else attribute.encode_value(value)
|
||||
)
|
||||
|
||||
# Truncate if needed
|
||||
if len(value) > connection.att_mtu - 3:
|
||||
value = value[:connection.att_mtu - 3]
|
||||
value = value[: connection.att_mtu - 3]
|
||||
|
||||
# Indicate
|
||||
indication = ATT_Handle_Value_Indication(
|
||||
attribute_handle = attribute.handle,
|
||||
attribute_value = value
|
||||
attribute_handle=attribute.handle, attribute_value=value
|
||||
)
|
||||
logger.debug(
|
||||
f'GATT Indicate from server: [0x{connection.handle:04X}] {indication}'
|
||||
)
|
||||
logger.debug(f'GATT Indicate from server: [0x{connection.handle:04X}] {indication}')
|
||||
|
||||
# Wait until we can send (only one pending indication at a time per connection)
|
||||
async with self.indication_semaphores[connection.handle]:
|
||||
assert(self.pending_confirmations[connection.handle] is None)
|
||||
assert self.pending_confirmations[connection.handle] is None
|
||||
|
||||
# Create a future value to hold the eventual response
|
||||
self.pending_confirmations[connection.handle] = asyncio.get_running_loop().create_future()
|
||||
self.pending_confirmations[
|
||||
connection.handle
|
||||
] = asyncio.get_running_loop().create_future()
|
||||
|
||||
try:
|
||||
self.send_gatt_pdu(connection.handle, indication.to_bytes())
|
||||
await asyncio.wait_for(self.pending_confirmations[connection.handle], GATT_REQUEST_TIMEOUT)
|
||||
except asyncio.TimeoutError:
|
||||
await asyncio.wait_for(
|
||||
self.pending_confirmations[connection.handle], GATT_REQUEST_TIMEOUT
|
||||
)
|
||||
except asyncio.TimeoutError as error:
|
||||
logger.warning(color('!!! GATT Indicate timeout', 'red'))
|
||||
raise TimeoutError(f'GATT timeout for {indication.name}')
|
||||
raise TimeoutError(f'GATT timeout for {indication.name}') from error
|
||||
finally:
|
||||
self.pending_confirmations[connection.handle] = None
|
||||
|
||||
async def notify_or_indicate_subscribers(self, indicate, attribute, value=None, force=False):
|
||||
async def notify_or_indicate_subscribers(
|
||||
self, indicate, attribute, value=None, force=False
|
||||
):
|
||||
# Get all the connections for which there's at least one subscription
|
||||
connections = [
|
||||
connection for connection in [
|
||||
connection
|
||||
for connection in [
|
||||
self.device.lookup_connection(connection_handle)
|
||||
for (connection_handle, subscribers) in self.subscribers.items()
|
||||
if force or subscribers.get(attribute.handle)
|
||||
@@ -263,10 +433,12 @@ class Server(EventEmitter):
|
||||
# Indicate or notify for each connection
|
||||
if connections:
|
||||
coroutine = self.indicate_subscriber if indicate else self.notify_subscriber
|
||||
await asyncio.wait([
|
||||
asyncio.create_task(coroutine(connection, attribute, value, force))
|
||||
for connection in connections
|
||||
])
|
||||
await asyncio.wait(
|
||||
[
|
||||
asyncio.create_task(coroutine(connection, attribute, value, force))
|
||||
for connection in connections
|
||||
]
|
||||
)
|
||||
|
||||
async def notify_subscribers(self, attribute, value=None, force=False):
|
||||
return await self.notify_or_indicate_subscribers(False, attribute, value, force)
|
||||
@@ -292,17 +464,17 @@ class Server(EventEmitter):
|
||||
except ATT_Error as error:
|
||||
logger.debug(f'normal exception returned by handler: {error}')
|
||||
response = ATT_Error_Response(
|
||||
request_opcode_in_error = att_pdu.op_code,
|
||||
attribute_handle_in_error = error.att_handle,
|
||||
error_code = error.error_code
|
||||
request_opcode_in_error=att_pdu.op_code,
|
||||
attribute_handle_in_error=error.att_handle,
|
||||
error_code=error.error_code,
|
||||
)
|
||||
self.send_response(connection, response)
|
||||
except Exception as error:
|
||||
logger.warning(f'{color("!!! Exception in handler:", "red")} {error}')
|
||||
response = ATT_Error_Response(
|
||||
request_opcode_in_error = att_pdu.op_code,
|
||||
attribute_handle_in_error = 0x0000,
|
||||
error_code = ATT_UNLIKELY_ERROR_ERROR
|
||||
request_opcode_in_error=att_pdu.op_code,
|
||||
attribute_handle_in_error=0x0000,
|
||||
error_code=ATT_UNLIKELY_ERROR_ERROR,
|
||||
)
|
||||
self.send_response(connection, response)
|
||||
raise error
|
||||
@@ -313,7 +485,13 @@ class Server(EventEmitter):
|
||||
self.on_att_request(connection, att_pdu)
|
||||
else:
|
||||
# Just ignore
|
||||
logger.warning(f'{color("--- Ignoring GATT Request from [0x{connection.handle:04X}]:", "red")} {att_pdu}')
|
||||
logger.warning(
|
||||
color(
|
||||
f'--- Ignoring GATT Request from [0x{connection.handle:04X}]: ',
|
||||
'red',
|
||||
)
|
||||
+ str(att_pdu)
|
||||
)
|
||||
|
||||
#######################################################
|
||||
# ATT handlers
|
||||
@@ -322,11 +500,16 @@ class Server(EventEmitter):
|
||||
'''
|
||||
Handler for requests without a more specific handler
|
||||
'''
|
||||
logger.warning(f'{color(f"--- Unsupported ATT Request from [0x{connection.handle:04X}]:", "red")} {pdu}')
|
||||
logger.warning(
|
||||
color(
|
||||
f'--- Unsupported ATT Request from [0x{connection.handle:04X}]: ', 'red'
|
||||
)
|
||||
+ str(pdu)
|
||||
)
|
||||
response = ATT_Error_Response(
|
||||
request_opcode_in_error = pdu.op_code,
|
||||
attribute_handle_in_error = 0x0000,
|
||||
error_code = ATT_REQUEST_NOT_SUPPORTED_ERROR
|
||||
request_opcode_in_error=pdu.op_code,
|
||||
attribute_handle_in_error=0x0000,
|
||||
error_code=ATT_REQUEST_NOT_SUPPORTED_ERROR,
|
||||
)
|
||||
self.send_response(connection, response)
|
||||
|
||||
@@ -334,7 +517,9 @@ class Server(EventEmitter):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.2.1 Exchange MTU Request
|
||||
'''
|
||||
self.send_response(connection, ATT_Exchange_MTU_Response(server_rx_mtu = self.max_mtu))
|
||||
self.send_response(
|
||||
connection, ATT_Exchange_MTU_Response(server_rx_mtu=self.max_mtu)
|
||||
)
|
||||
|
||||
# Compute the final MTU
|
||||
if request.client_rx_mtu >= ATT_DEFAULT_MTU:
|
||||
@@ -351,12 +536,18 @@ class Server(EventEmitter):
|
||||
'''
|
||||
|
||||
# Check the request parameters
|
||||
if request.starting_handle == 0 or request.starting_handle > request.ending_handle:
|
||||
self.send_response(connection, ATT_Error_Response(
|
||||
request_opcode_in_error = request.op_code,
|
||||
attribute_handle_in_error = request.starting_handle,
|
||||
error_code = ATT_INVALID_HANDLE_ERROR
|
||||
))
|
||||
if (
|
||||
request.starting_handle == 0
|
||||
or request.starting_handle > request.ending_handle
|
||||
):
|
||||
self.send_response(
|
||||
connection,
|
||||
ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.starting_handle,
|
||||
error_code=ATT_INVALID_HANDLE_ERROR,
|
||||
),
|
||||
)
|
||||
return
|
||||
|
||||
# Build list of returned attributes
|
||||
@@ -364,12 +555,11 @@ class Server(EventEmitter):
|
||||
attributes = []
|
||||
uuid_size = 0
|
||||
for attribute in (
|
||||
attribute for attribute in self.attributes if
|
||||
attribute.handle >= request.starting_handle and
|
||||
attribute.handle <= request.ending_handle
|
||||
attribute
|
||||
for attribute in self.attributes
|
||||
if attribute.handle >= request.starting_handle
|
||||
and attribute.handle <= request.ending_handle
|
||||
):
|
||||
# TODO: check permissions
|
||||
|
||||
this_uuid_size = len(attribute.type.to_pdu_bytes())
|
||||
|
||||
if attributes:
|
||||
@@ -393,14 +583,14 @@ class Server(EventEmitter):
|
||||
for attribute in attributes
|
||||
]
|
||||
response = ATT_Find_Information_Response(
|
||||
format = 1 if len(attributes[0].type.to_pdu_bytes()) == 2 else 2,
|
||||
information_data = b''.join(information_data_list)
|
||||
format=1 if len(attributes[0].type.to_pdu_bytes()) == 2 else 2,
|
||||
information_data=b''.join(information_data_list),
|
||||
)
|
||||
else:
|
||||
response = ATT_Error_Response(
|
||||
request_opcode_in_error = request.op_code,
|
||||
attribute_handle_in_error = request.starting_handle,
|
||||
error_code = ATT_ATTRIBUTE_NOT_FOUND_ERROR
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.starting_handle,
|
||||
error_code=ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
||||
)
|
||||
|
||||
self.send_response(connection, response)
|
||||
@@ -414,12 +604,13 @@ class Server(EventEmitter):
|
||||
pdu_space_available = connection.att_mtu - 2
|
||||
attributes = []
|
||||
for attribute in (
|
||||
attribute for attribute in self.attributes if
|
||||
attribute.handle >= request.starting_handle and
|
||||
attribute.handle <= request.ending_handle and
|
||||
attribute.type == request.attribute_type and
|
||||
attribute.read_value(connection) == request.attribute_value and
|
||||
pdu_space_available >= 4
|
||||
attribute
|
||||
for attribute in self.attributes
|
||||
if attribute.handle >= request.starting_handle
|
||||
and attribute.handle <= request.ending_handle
|
||||
and attribute.type == request.attribute_type
|
||||
and attribute.read_value(connection) == request.attribute_value
|
||||
and pdu_space_available >= 4
|
||||
):
|
||||
# TODO: check permissions
|
||||
|
||||
@@ -431,25 +622,27 @@ class Server(EventEmitter):
|
||||
if attributes:
|
||||
handles_information_list = []
|
||||
for attribute in attributes:
|
||||
if attribute.type in {
|
||||
if attribute.type in (
|
||||
GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
|
||||
GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE,
|
||||
GATT_CHARACTERISTIC_ATTRIBUTE_TYPE
|
||||
}:
|
||||
GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
|
||||
):
|
||||
# Part of a group
|
||||
group_end_handle = attribute.end_group_handle
|
||||
else:
|
||||
# Not part of a group
|
||||
group_end_handle = attribute.handle
|
||||
handles_information_list.append(struct.pack('<HH', attribute.handle, group_end_handle))
|
||||
handles_information_list.append(
|
||||
struct.pack('<HH', attribute.handle, group_end_handle)
|
||||
)
|
||||
response = ATT_Find_By_Type_Value_Response(
|
||||
handles_information_list = b''.join(handles_information_list)
|
||||
handles_information_list=b''.join(handles_information_list)
|
||||
)
|
||||
else:
|
||||
response = ATT_Error_Response(
|
||||
request_opcode_in_error = request.op_code,
|
||||
attribute_handle_in_error = request.starting_handle,
|
||||
error_code = ATT_ATTRIBUTE_NOT_FOUND_ERROR
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.starting_handle,
|
||||
error_code=ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
||||
)
|
||||
|
||||
self.send_response(connection, response)
|
||||
@@ -460,18 +653,37 @@ class Server(EventEmitter):
|
||||
'''
|
||||
|
||||
pdu_space_available = connection.att_mtu - 2
|
||||
|
||||
response = ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.starting_handle,
|
||||
error_code=ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
||||
)
|
||||
|
||||
attributes = []
|
||||
for attribute in (
|
||||
attribute for attribute in self.attributes if
|
||||
attribute.type == request.attribute_type and
|
||||
attribute.handle >= request.starting_handle and
|
||||
attribute.handle <= request.ending_handle and
|
||||
pdu_space_available
|
||||
attribute
|
||||
for attribute in self.attributes
|
||||
if attribute.type == request.attribute_type
|
||||
and attribute.handle >= request.starting_handle
|
||||
and attribute.handle <= request.ending_handle
|
||||
and pdu_space_available
|
||||
):
|
||||
# TODO: check permissions
|
||||
|
||||
try:
|
||||
attribute_value = attribute.read_value(connection)
|
||||
except ATT_Error as error:
|
||||
# If the first attribute is unreadable, return an error
|
||||
# Otherwise return attributes up to this point
|
||||
if not attributes:
|
||||
response = ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=attribute.handle,
|
||||
error_code=error.error_code,
|
||||
)
|
||||
break
|
||||
|
||||
# Check the attribute value size
|
||||
attribute_value = attribute.read_value(connection)
|
||||
max_attribute_size = min(connection.att_mtu - 4, 253)
|
||||
if len(attribute_value) > max_attribute_size:
|
||||
# We need to truncate
|
||||
@@ -490,17 +702,14 @@ class Server(EventEmitter):
|
||||
pdu_space_available -= entry_size
|
||||
|
||||
if attributes:
|
||||
attribute_data_list = [struct.pack('<H', handle) + value for handle, value in attributes]
|
||||
attribute_data_list = [
|
||||
struct.pack('<H', handle) + value for handle, value in attributes
|
||||
]
|
||||
response = ATT_Read_By_Type_Response(
|
||||
length = entry_size,
|
||||
attribute_data_list = b''.join(attribute_data_list)
|
||||
length=entry_size, attribute_data_list=b''.join(attribute_data_list)
|
||||
)
|
||||
else:
|
||||
response = ATT_Error_Response(
|
||||
request_opcode_in_error = request.op_code,
|
||||
attribute_handle_in_error = request.starting_handle,
|
||||
error_code = ATT_ATTRIBUTE_NOT_FOUND_ERROR
|
||||
)
|
||||
logging.debug(f"not found {request}")
|
||||
|
||||
self.send_response(connection, response)
|
||||
|
||||
@@ -510,17 +719,22 @@ class Server(EventEmitter):
|
||||
'''
|
||||
|
||||
if attribute := self.get_attribute(request.attribute_handle):
|
||||
# TODO: check permissions
|
||||
value = attribute.read_value(connection)
|
||||
value_size = min(connection.att_mtu - 1, len(value))
|
||||
response = ATT_Read_Response(
|
||||
attribute_value = value[:value_size]
|
||||
)
|
||||
try:
|
||||
value = attribute.read_value(connection)
|
||||
except ATT_Error as error:
|
||||
response = ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.attribute_handle,
|
||||
error_code=error.error_code,
|
||||
)
|
||||
else:
|
||||
value_size = min(connection.att_mtu - 1, len(value))
|
||||
response = ATT_Read_Response(attribute_value=value[:value_size])
|
||||
else:
|
||||
response = ATT_Error_Response(
|
||||
request_opcode_in_error = request.op_code,
|
||||
attribute_handle_in_error = request.attribute_handle,
|
||||
error_code = ATT_INVALID_HANDLE_ERROR
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.attribute_handle,
|
||||
error_code=ATT_INVALID_HANDLE_ERROR,
|
||||
)
|
||||
self.send_response(connection, response)
|
||||
|
||||
@@ -530,30 +744,41 @@ class Server(EventEmitter):
|
||||
'''
|
||||
|
||||
if attribute := self.get_attribute(request.attribute_handle):
|
||||
# TODO: check permissions
|
||||
value = attribute.read_value(connection)
|
||||
if request.value_offset > len(value):
|
||||
try:
|
||||
value = attribute.read_value(connection)
|
||||
except ATT_Error as error:
|
||||
response = ATT_Error_Response(
|
||||
request_opcode_in_error = request.op_code,
|
||||
attribute_handle_in_error = request.attribute_handle,
|
||||
error_code = ATT_INVALID_OFFSET_ERROR
|
||||
)
|
||||
elif len(value) <= connection.att_mtu - 1:
|
||||
response = ATT_Error_Response(
|
||||
request_opcode_in_error = request.op_code,
|
||||
attribute_handle_in_error = request.attribute_handle,
|
||||
error_code = ATT_ATTRIBUTE_NOT_LONG_ERROR
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.attribute_handle,
|
||||
error_code=error.error_code,
|
||||
)
|
||||
else:
|
||||
part_size = min(connection.att_mtu - 1, len(value) - request.value_offset)
|
||||
response = ATT_Read_Blob_Response(
|
||||
part_attribute_value = value[request.value_offset:request.value_offset + part_size]
|
||||
)
|
||||
if request.value_offset > len(value):
|
||||
response = ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.attribute_handle,
|
||||
error_code=ATT_INVALID_OFFSET_ERROR,
|
||||
)
|
||||
elif len(value) <= connection.att_mtu - 1:
|
||||
response = ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.attribute_handle,
|
||||
error_code=ATT_ATTRIBUTE_NOT_LONG_ERROR,
|
||||
)
|
||||
else:
|
||||
part_size = min(
|
||||
connection.att_mtu - 1, len(value) - request.value_offset
|
||||
)
|
||||
response = ATT_Read_Blob_Response(
|
||||
part_attribute_value=value[
|
||||
request.value_offset : request.value_offset + part_size
|
||||
]
|
||||
)
|
||||
else:
|
||||
response = ATT_Error_Response(
|
||||
request_opcode_in_error = request.op_code,
|
||||
attribute_handle_in_error = request.attribute_handle,
|
||||
error_code = ATT_INVALID_HANDLE_ERROR
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.attribute_handle,
|
||||
error_code=ATT_INVALID_HANDLE_ERROR,
|
||||
)
|
||||
self.send_response(connection, response)
|
||||
|
||||
@@ -561,15 +786,14 @@ class Server(EventEmitter):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.4.9 Read by Group Type Request
|
||||
'''
|
||||
if request.attribute_group_type not in {
|
||||
if request.attribute_group_type not in (
|
||||
GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
|
||||
GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE,
|
||||
GATT_INCLUDE_ATTRIBUTE_TYPE
|
||||
}:
|
||||
):
|
||||
response = ATT_Error_Response(
|
||||
request_opcode_in_error = request.op_code,
|
||||
attribute_handle_in_error = request.starting_handle,
|
||||
error_code = ATT_UNSUPPORTED_GROUP_TYPE_ERROR
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.starting_handle,
|
||||
error_code=ATT_UNSUPPORTED_GROUP_TYPE_ERROR,
|
||||
)
|
||||
self.send_response(connection, response)
|
||||
return
|
||||
@@ -577,14 +801,17 @@ class Server(EventEmitter):
|
||||
pdu_space_available = connection.att_mtu - 2
|
||||
attributes = []
|
||||
for attribute in (
|
||||
attribute for attribute in self.attributes if
|
||||
attribute.type == request.attribute_group_type and
|
||||
attribute.handle >= request.starting_handle and
|
||||
attribute.handle <= request.ending_handle and
|
||||
pdu_space_available
|
||||
attribute
|
||||
for attribute in self.attributes
|
||||
if attribute.type == request.attribute_group_type
|
||||
and attribute.handle >= request.starting_handle
|
||||
and attribute.handle <= request.ending_handle
|
||||
and pdu_space_available
|
||||
):
|
||||
# Check the attribute value size
|
||||
# No need to catch permission errors here, since these attributes
|
||||
# must all be world-readable
|
||||
attribute_value = attribute.read_value(connection)
|
||||
# Check the attribute value size
|
||||
max_attribute_size = min(connection.att_mtu - 6, 251)
|
||||
if len(attribute_value) > max_attribute_size:
|
||||
# We need to truncate
|
||||
@@ -599,7 +826,9 @@ class Server(EventEmitter):
|
||||
break
|
||||
|
||||
# Add the attribute to the list
|
||||
attributes.append((attribute.handle, attribute.end_group_handle, attribute_value))
|
||||
attributes.append(
|
||||
(attribute.handle, attribute.end_group_handle, attribute_value)
|
||||
)
|
||||
pdu_space_available -= entry_size
|
||||
|
||||
if attributes:
|
||||
@@ -608,14 +837,14 @@ class Server(EventEmitter):
|
||||
for handle, end_group_handle, value in attributes
|
||||
]
|
||||
response = ATT_Read_By_Group_Type_Response(
|
||||
length = len(attribute_data_list[0]),
|
||||
attribute_data_list = b''.join(attribute_data_list)
|
||||
length=len(attribute_data_list[0]),
|
||||
attribute_data_list=b''.join(attribute_data_list),
|
||||
)
|
||||
else:
|
||||
response = ATT_Error_Response(
|
||||
request_opcode_in_error = request.op_code,
|
||||
attribute_handle_in_error = request.starting_handle,
|
||||
error_code = ATT_ATTRIBUTE_NOT_FOUND_ERROR
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.starting_handle,
|
||||
error_code=ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
||||
)
|
||||
|
||||
self.send_response(connection, response)
|
||||
@@ -628,22 +857,28 @@ class Server(EventEmitter):
|
||||
# Check that the attribute exists
|
||||
attribute = self.get_attribute(request.attribute_handle)
|
||||
if attribute is None:
|
||||
self.send_response(connection, ATT_Error_Response(
|
||||
request_opcode_in_error = request.op_code,
|
||||
attribute_handle_in_error = request.attribute_handle,
|
||||
error_code = ATT_INVALID_HANDLE_ERROR
|
||||
))
|
||||
self.send_response(
|
||||
connection,
|
||||
ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.attribute_handle,
|
||||
error_code=ATT_INVALID_HANDLE_ERROR,
|
||||
),
|
||||
)
|
||||
return
|
||||
|
||||
# TODO: check permissions
|
||||
|
||||
# Check the request parameters
|
||||
if len(request.attribute_value) > GATT_MAX_ATTRIBUTE_VALUE_SIZE:
|
||||
self.send_response(connection, ATT_Error_Response(
|
||||
request_opcode_in_error = request.op_code,
|
||||
attribute_handle_in_error = request.attribute_handle,
|
||||
error_code = ATT_INVALID_ATTRIBUTE_LENGTH_ERROR
|
||||
))
|
||||
self.send_response(
|
||||
connection,
|
||||
ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.attribute_handle,
|
||||
error_code=ATT_INVALID_ATTRIBUTE_LENGTH_ERROR,
|
||||
),
|
||||
)
|
||||
return
|
||||
|
||||
# Accept the value
|
||||
@@ -674,13 +909,15 @@ class Server(EventEmitter):
|
||||
except Exception as error:
|
||||
logger.warning(f'!!! ignoring exception: {error}')
|
||||
|
||||
def on_att_handle_value_confirmation(self, connection, confirmation):
|
||||
def on_att_handle_value_confirmation(self, connection, _confirmation):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.7.3 Handle Value Confirmation
|
||||
'''
|
||||
if self.pending_confirmations[connection.handle] is None:
|
||||
# Not expected!
|
||||
logger.warning('!!! unexpected confirmation, there is no pending indication')
|
||||
logger.warning(
|
||||
'!!! unexpected confirmation, there is no pending indication'
|
||||
)
|
||||
return
|
||||
|
||||
self.pending_confirmations[connection.handle].set_result(None)
|
||||
|
||||
3061
bumble/hci.py
3061
bumble/hci.py
File diff suppressed because it is too large
Load Diff
@@ -16,12 +16,11 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import logging
|
||||
from colors import color
|
||||
|
||||
from bumble.smp import SMP_CID, SMP_Command
|
||||
|
||||
from .colors import color
|
||||
from .att import ATT_CID, ATT_PDU
|
||||
from .smp import SMP_CID, SMP_Command
|
||||
from .core import name_or_number
|
||||
from .gatt import ATT_PDU, ATT_CID
|
||||
from .l2cap import (
|
||||
L2CAP_PDU,
|
||||
L2CAP_CONNECTION_REQUEST,
|
||||
@@ -29,20 +28,17 @@ from .l2cap import (
|
||||
L2CAP_SIGNALING_CID,
|
||||
L2CAP_LE_SIGNALING_CID,
|
||||
L2CAP_Control_Frame,
|
||||
L2CAP_Connection_Response
|
||||
L2CAP_Connection_Response,
|
||||
)
|
||||
from .hci import (
|
||||
HCI_EVENT_PACKET,
|
||||
HCI_ACL_DATA_PACKET,
|
||||
HCI_DISCONNECTION_COMPLETE_EVENT,
|
||||
HCI_AclDataPacketAssembler
|
||||
HCI_AclDataPacketAssembler,
|
||||
)
|
||||
from .rfcomm import RFCOMM_Frame, RFCOMM_PSM
|
||||
from .sdp import SDP_PDU, SDP_PSM
|
||||
from .avdtp import (
|
||||
MessageAssembler as AVDTP_MessageAssembler,
|
||||
AVDTP_PSM
|
||||
)
|
||||
from .avdtp import MessageAssembler as AVDTP_MessageAssembler, AVDTP_PSM
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -53,8 +49,8 @@ logger = logging.getLogger(__name__)
|
||||
# -----------------------------------------------------------------------------
|
||||
PSM_NAMES = {
|
||||
RFCOMM_PSM: 'RFCOMM',
|
||||
SDP_PSM: 'SDP',
|
||||
AVDTP_PSM: 'AVDTP'
|
||||
SDP_PSM: 'SDP',
|
||||
AVDTP_PSM: 'AVDTP'
|
||||
# TODO: add more PSM values
|
||||
}
|
||||
|
||||
@@ -63,12 +59,13 @@ PSM_NAMES = {
|
||||
class PacketTracer:
|
||||
class AclStream:
|
||||
def __init__(self, analyzer):
|
||||
self.analyzer = analyzer
|
||||
self.analyzer = analyzer
|
||||
self.packet_assembler = HCI_AclDataPacketAssembler(self.on_acl_pdu)
|
||||
self.avdtp_assemblers = {} # AVDTP assemblers, by source_cid
|
||||
self.psms = {} # PSM, by source_cid
|
||||
self.peer = None # ACL stream in the other direction
|
||||
self.avdtp_assemblers = {} # AVDTP assemblers, by source_cid
|
||||
self.psms = {} # PSM, by source_cid
|
||||
self.peer = None # ACL stream in the other direction
|
||||
|
||||
# pylint: disable=too-many-nested-blocks
|
||||
def on_acl_pdu(self, pdu):
|
||||
l2cap_pdu = L2CAP_PDU.from_bytes(pdu)
|
||||
|
||||
@@ -78,7 +75,7 @@ class PacketTracer:
|
||||
elif l2cap_pdu.cid == SMP_CID:
|
||||
smp_command = SMP_Command.from_bytes(l2cap_pdu.payload)
|
||||
self.analyzer.emit(smp_command)
|
||||
elif l2cap_pdu.cid == L2CAP_SIGNALING_CID or l2cap_pdu.cid == L2CAP_LE_SIGNALING_CID:
|
||||
elif l2cap_pdu.cid in (L2CAP_SIGNALING_CID, L2CAP_LE_SIGNALING_CID):
|
||||
control_frame = L2CAP_Control_Frame.from_bytes(l2cap_pdu.payload)
|
||||
self.analyzer.emit(control_frame)
|
||||
|
||||
@@ -86,16 +83,26 @@ class PacketTracer:
|
||||
if control_frame.code == L2CAP_CONNECTION_REQUEST:
|
||||
self.psms[control_frame.source_cid] = control_frame.psm
|
||||
elif control_frame.code == L2CAP_CONNECTION_RESPONSE:
|
||||
if control_frame.result == L2CAP_Connection_Response.CONNECTION_SUCCESSFUL:
|
||||
if (
|
||||
control_frame.result
|
||||
== L2CAP_Connection_Response.CONNECTION_SUCCESSFUL
|
||||
):
|
||||
if self.peer:
|
||||
if psm := self.peer.psms.get(control_frame.source_cid):
|
||||
# Found a pending connection
|
||||
self.psms[control_frame.destination_cid] = psm
|
||||
|
||||
# For AVDTP connections, create a packet assembler for each direction
|
||||
# For AVDTP connections, create a packet assembler for
|
||||
# each direction
|
||||
if psm == AVDTP_PSM:
|
||||
self.avdtp_assemblers[control_frame.source_cid] = AVDTP_MessageAssembler(self.on_avdtp_message)
|
||||
self.peer.avdtp_assemblers[control_frame.destination_cid] = AVDTP_MessageAssembler(self.peer.on_avdtp_message)
|
||||
self.avdtp_assemblers[
|
||||
control_frame.source_cid
|
||||
] = AVDTP_MessageAssembler(self.on_avdtp_message)
|
||||
self.peer.avdtp_assemblers[
|
||||
control_frame.destination_cid
|
||||
] = AVDTP_MessageAssembler(
|
||||
self.peer.on_avdtp_message
|
||||
)
|
||||
|
||||
else:
|
||||
# Try to find the PSM associated with this PDU
|
||||
@@ -107,31 +114,42 @@ class PacketTracer:
|
||||
rfcomm_frame = RFCOMM_Frame.from_bytes(l2cap_pdu.payload)
|
||||
self.analyzer.emit(rfcomm_frame)
|
||||
elif psm == AVDTP_PSM:
|
||||
self.analyzer.emit(f'{color("L2CAP", "green")} [CID={l2cap_pdu.cid}, PSM=AVDTP]: {l2cap_pdu.payload.hex()}')
|
||||
self.analyzer.emit(
|
||||
f'{color("L2CAP", "green")} [CID={l2cap_pdu.cid}, '
|
||||
f'PSM=AVDTP]: {l2cap_pdu.payload.hex()}'
|
||||
)
|
||||
assembler = self.avdtp_assemblers.get(l2cap_pdu.cid)
|
||||
if assembler:
|
||||
assembler.on_pdu(l2cap_pdu.payload)
|
||||
else:
|
||||
psm_string = name_or_number(PSM_NAMES, psm)
|
||||
self.analyzer.emit(f'{color("L2CAP", "green")} [CID={l2cap_pdu.cid}, PSM={psm_string}]: {l2cap_pdu.payload.hex()}')
|
||||
self.analyzer.emit(
|
||||
f'{color("L2CAP", "green")} [CID={l2cap_pdu.cid}, '
|
||||
f'PSM={psm_string}]: {l2cap_pdu.payload.hex()}'
|
||||
)
|
||||
else:
|
||||
self.analyzer.emit(l2cap_pdu)
|
||||
|
||||
def on_avdtp_message(self, transaction_label, message):
|
||||
self.analyzer.emit(f'{color("AVDTP", "green")} [{transaction_label}] {message}')
|
||||
self.analyzer.emit(
|
||||
f'{color("AVDTP", "green")} [{transaction_label}] {message}'
|
||||
)
|
||||
|
||||
def feed_packet(self, packet):
|
||||
self.packet_assembler.feed_packet(packet)
|
||||
|
||||
class Analyzer:
|
||||
def __init__(self, label, emit_message):
|
||||
self.label = label
|
||||
self.label = label
|
||||
self.emit_message = emit_message
|
||||
self.acl_streams = {} # ACL streams, by connection handle
|
||||
self.peer = None # Analyzer in the other direction
|
||||
self.acl_streams = {} # ACL streams, by connection handle
|
||||
self.peer = None # Analyzer in the other direction
|
||||
|
||||
def start_acl_stream(self, connection_handle):
|
||||
logger.info(f'[{self.label}] +++ Creating ACL stream for connection 0x{connection_handle:04X}')
|
||||
logger.info(
|
||||
f'[{self.label}] +++ Creating ACL stream for connection '
|
||||
f'0x{connection_handle:04X}'
|
||||
)
|
||||
stream = PacketTracer.AclStream(self)
|
||||
self.acl_streams[connection_handle] = stream
|
||||
|
||||
@@ -144,7 +162,10 @@ class PacketTracer:
|
||||
|
||||
def end_acl_stream(self, connection_handle):
|
||||
if connection_handle in self.acl_streams:
|
||||
logger.info(f'[{self.label}] --- Removing ACL stream for connection 0x{connection_handle:04X}')
|
||||
logger.info(
|
||||
f'[{self.label}] --- Removing ACL stream for connection '
|
||||
f'0x{connection_handle:04X}'
|
||||
)
|
||||
del self.acl_streams[connection_handle]
|
||||
|
||||
# Let the other forwarder know so it can cleanup its stream as well
|
||||
@@ -176,9 +197,13 @@ class PacketTracer:
|
||||
self,
|
||||
host_to_controller_label=color('HOST->CONTROLLER', 'blue'),
|
||||
controller_to_host_label=color('CONTROLLER->HOST', 'cyan'),
|
||||
emit_message=logger.info
|
||||
emit_message=logger.info,
|
||||
):
|
||||
self.host_to_controller_analyzer = PacketTracer.Analyzer(host_to_controller_label, emit_message)
|
||||
self.controller_to_host_analyzer = PacketTracer.Analyzer(controller_to_host_label, emit_message)
|
||||
self.host_to_controller_analyzer = PacketTracer.Analyzer(
|
||||
host_to_controller_label, emit_message
|
||||
)
|
||||
self.controller_to_host_analyzer = PacketTracer.Analyzer(
|
||||
controller_to_host_label, emit_message
|
||||
)
|
||||
self.host_to_controller_analyzer.peer = self.controller_to_host_analyzer
|
||||
self.controller_to_host_analyzer.peer = self.host_to_controller_analyzer
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
import logging
|
||||
import asyncio
|
||||
import collections
|
||||
from colors import color
|
||||
|
||||
from .colors import color
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -34,16 +35,16 @@ logger = logging.getLogger(__name__)
|
||||
# -----------------------------------------------------------------------------
|
||||
class HfpProtocol:
|
||||
def __init__(self, dlc):
|
||||
self.dlc = dlc
|
||||
self.buffer = ''
|
||||
self.lines = collections.deque()
|
||||
self.dlc = dlc
|
||||
self.buffer = ''
|
||||
self.lines = collections.deque()
|
||||
self.lines_available = asyncio.Event()
|
||||
|
||||
dlc.sink = self.feed
|
||||
|
||||
def feed(self, data):
|
||||
# Convert the data to a string if needed
|
||||
if type(data) == bytes:
|
||||
if isinstance(data, bytes):
|
||||
data = data.decode('utf-8')
|
||||
|
||||
logger.debug(f'<<< Data received: {data}')
|
||||
@@ -52,7 +53,7 @@ class HfpProtocol:
|
||||
self.buffer += data
|
||||
while (separator := self.buffer.find('\r')) >= 0:
|
||||
line = self.buffer[:separator].strip()
|
||||
self.buffer = self.buffer[separator + 1:]
|
||||
self.buffer = self.buffer[separator + 1 :]
|
||||
if len(line) > 0:
|
||||
self.on_line(line)
|
||||
|
||||
@@ -79,16 +80,16 @@ class HfpProtocol:
|
||||
async def initialize_service(self):
|
||||
# Perform Service Level Connection Initialization
|
||||
self.send_command_line('AT+BRSF=2072') # Retrieve Supported Features
|
||||
line = await(self.next_line())
|
||||
line = await(self.next_line())
|
||||
await (self.next_line())
|
||||
await (self.next_line())
|
||||
|
||||
self.send_command_line('AT+CIND=?')
|
||||
line = await(self.next_line())
|
||||
line = await(self.next_line())
|
||||
await (self.next_line())
|
||||
await (self.next_line())
|
||||
|
||||
self.send_command_line('AT+CIND?')
|
||||
line = await(self.next_line())
|
||||
line = await(self.next_line())
|
||||
await (self.next_line())
|
||||
await (self.next_line())
|
||||
|
||||
self.send_command_line('AT+CMER=3,0,0,1')
|
||||
line = await(self.next_line())
|
||||
await (self.next_line())
|
||||
|
||||
597
bumble/host.py
597
bumble/host.py
@@ -16,16 +16,62 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import collections
|
||||
import logging
|
||||
from pyee import EventEmitter
|
||||
from colors import color
|
||||
import struct
|
||||
|
||||
from bumble.colors import color
|
||||
from bumble.l2cap import L2CAP_PDU
|
||||
from bumble.snoop import Snooper
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from .hci import (
|
||||
Address,
|
||||
HCI_ACL_DATA_PACKET,
|
||||
HCI_COMMAND_COMPLETE_EVENT,
|
||||
HCI_COMMAND_PACKET,
|
||||
HCI_EVENT_PACKET,
|
||||
HCI_LE_READ_BUFFER_SIZE_COMMAND,
|
||||
HCI_LE_READ_LOCAL_SUPPORTED_FEATURES_COMMAND,
|
||||
HCI_LE_READ_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND,
|
||||
HCI_LE_WRITE_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND,
|
||||
HCI_READ_BUFFER_SIZE_COMMAND,
|
||||
HCI_READ_LOCAL_VERSION_INFORMATION_COMMAND,
|
||||
HCI_RESET_COMMAND,
|
||||
HCI_SUCCESS,
|
||||
HCI_SUPPORTED_COMMANDS_FLAGS,
|
||||
HCI_VERSION_BLUETOOTH_CORE_4_0,
|
||||
HCI_AclDataPacket,
|
||||
HCI_AclDataPacketAssembler,
|
||||
HCI_Constant,
|
||||
HCI_Error,
|
||||
HCI_LE_Long_Term_Key_Request_Negative_Reply_Command,
|
||||
HCI_LE_Long_Term_Key_Request_Reply_Command,
|
||||
HCI_LE_Read_Buffer_Size_Command,
|
||||
HCI_LE_Read_Local_Supported_Features_Command,
|
||||
HCI_LE_Read_Suggested_Default_Data_Length_Command,
|
||||
HCI_LE_Remote_Connection_Parameter_Request_Reply_Command,
|
||||
HCI_LE_Set_Event_Mask_Command,
|
||||
HCI_LE_Write_Suggested_Default_Data_Length_Command,
|
||||
HCI_Link_Key_Request_Negative_Reply_Command,
|
||||
HCI_Link_Key_Request_Reply_Command,
|
||||
HCI_Packet,
|
||||
HCI_Read_Buffer_Size_Command,
|
||||
HCI_Read_Local_Supported_Commands_Command,
|
||||
HCI_Read_Local_Version_Information_Command,
|
||||
HCI_Reset_Command,
|
||||
HCI_Set_Event_Mask_Command,
|
||||
)
|
||||
from .core import (
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
BT_CENTRAL_ROLE,
|
||||
BT_LE_TRANSPORT,
|
||||
ConnectionPHY,
|
||||
ConnectionParameters,
|
||||
)
|
||||
from .utils import AbortableEventEmitter
|
||||
|
||||
from .hci import *
|
||||
from .l2cap import *
|
||||
from .att import *
|
||||
from .gatt import *
|
||||
from .smp import *
|
||||
from .core import ConnectionParameters
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -36,21 +82,24 @@ logger = logging.getLogger(__name__)
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
# fmt: off
|
||||
|
||||
HOST_DEFAULT_HC_LE_ACL_DATA_PACKET_LENGTH = 27
|
||||
HOST_HC_TOTAL_NUM_LE_ACL_DATA_PACKETS = 1
|
||||
HOST_DEFAULT_HC_ACL_DATA_PACKET_LENGTH = 27
|
||||
HOST_HC_TOTAL_NUM_ACL_DATA_PACKETS = 1
|
||||
|
||||
# fmt: on
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Connection:
|
||||
def __init__(self, host, handle, role, peer_address, transport):
|
||||
self.host = host
|
||||
self.handle = handle
|
||||
self.role = role
|
||||
self.peer_address = peer_address
|
||||
self.assembler = HCI_AclDataPacketAssembler(self.on_acl_pdu)
|
||||
self.transport = transport
|
||||
def __init__(self, host, handle, peer_address, transport):
|
||||
self.host = host
|
||||
self.handle = handle
|
||||
self.peer_address = peer_address
|
||||
self.assembler = HCI_AclDataPacketAssembler(self.on_acl_pdu)
|
||||
self.transport = transport
|
||||
|
||||
def on_hci_acl_data_packet(self, packet):
|
||||
self.assembler.feed_packet(packet)
|
||||
@@ -61,28 +110,32 @@ class Connection:
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Host(EventEmitter):
|
||||
def __init__(self, controller_source = None, controller_sink = None):
|
||||
class Host(AbortableEventEmitter):
|
||||
def __init__(self, controller_source=None, controller_sink=None):
|
||||
super().__init__()
|
||||
|
||||
self.hci_sink = None
|
||||
self.ready = False # True when we can accept incoming packets
|
||||
self.connections = {} # Connections, by connection handle
|
||||
self.pending_command = None
|
||||
self.pending_response = None
|
||||
self.hc_le_acl_data_packet_length = HOST_DEFAULT_HC_LE_ACL_DATA_PACKET_LENGTH
|
||||
self.hci_sink = None
|
||||
self.ready = False # True when we can accept incoming packets
|
||||
self.reset_done = False
|
||||
self.connections = {} # Connections, by connection handle
|
||||
self.pending_command = None
|
||||
self.pending_response = None
|
||||
self.hc_le_acl_data_packet_length = HOST_DEFAULT_HC_LE_ACL_DATA_PACKET_LENGTH
|
||||
self.hc_total_num_le_acl_data_packets = HOST_HC_TOTAL_NUM_LE_ACL_DATA_PACKETS
|
||||
self.hc_acl_data_packet_length = HOST_DEFAULT_HC_ACL_DATA_PACKET_LENGTH
|
||||
self.hc_total_num_acl_data_packets = HOST_HC_TOTAL_NUM_ACL_DATA_PACKETS
|
||||
self.acl_packet_queue = collections.deque()
|
||||
self.acl_packets_in_flight = 0
|
||||
self.local_version = HCI_VERSION_BLUETOOTH_CORE_4_0
|
||||
self.local_supported_commands = bytes(64)
|
||||
self.local_le_features = 0
|
||||
self.command_semaphore = asyncio.Semaphore(1)
|
||||
self.long_term_key_provider = None
|
||||
self.link_key_provider = None
|
||||
self.pairing_io_capability_provider = None # Classic only
|
||||
self.hc_acl_data_packet_length = HOST_DEFAULT_HC_ACL_DATA_PACKET_LENGTH
|
||||
self.hc_total_num_acl_data_packets = HOST_HC_TOTAL_NUM_ACL_DATA_PACKETS
|
||||
self.acl_packet_queue = collections.deque()
|
||||
self.acl_packets_in_flight = 0
|
||||
self.local_version = None
|
||||
self.local_supported_commands = bytes(64)
|
||||
self.local_le_features = 0
|
||||
self.suggested_max_tx_octets = 251 # Max allowed
|
||||
self.suggested_max_tx_time = 2120 # Max allowed
|
||||
self.command_semaphore = asyncio.Semaphore(1)
|
||||
self.long_term_key_provider = None
|
||||
self.link_key_provider = None
|
||||
self.pairing_io_capability_provider = None # Classic only
|
||||
self.snooper = None
|
||||
|
||||
# Connect to the source and sink if specified
|
||||
if controller_source:
|
||||
@@ -90,68 +143,140 @@ class Host(EventEmitter):
|
||||
if controller_sink:
|
||||
self.set_packet_sink(controller_sink)
|
||||
|
||||
def find_connection_by_bd_addr(
|
||||
self,
|
||||
bd_addr: Address,
|
||||
transport: Optional[int] = None,
|
||||
check_address_type: bool = False,
|
||||
) -> Optional[Connection]:
|
||||
for connection in self.connections.values():
|
||||
if connection.peer_address.to_bytes() == bd_addr.to_bytes():
|
||||
if (
|
||||
check_address_type
|
||||
and connection.peer_address.address_type != bd_addr.address_type
|
||||
):
|
||||
continue
|
||||
if transport is None or connection.transport == transport:
|
||||
return connection
|
||||
|
||||
return None
|
||||
|
||||
async def flush(self) -> None:
|
||||
# Make sure no command is pending
|
||||
await self.command_semaphore.acquire()
|
||||
|
||||
# Flush current host state, then release command semaphore
|
||||
self.emit('flush')
|
||||
self.command_semaphore.release()
|
||||
|
||||
async def reset(self):
|
||||
await self.send_command(HCI_Reset_Command())
|
||||
if self.ready:
|
||||
self.ready = False
|
||||
await self.flush()
|
||||
|
||||
await self.send_command(HCI_Reset_Command(), check_result=True)
|
||||
self.ready = True
|
||||
|
||||
response = await self.send_command(HCI_Read_Local_Supported_Commands_Command())
|
||||
if response.return_parameters.status == HCI_SUCCESS:
|
||||
self.local_supported_commands = response.return_parameters.supported_commands
|
||||
else:
|
||||
logger.warn(f'HCI_Read_Local_Supported_Commands_Command failed: {response.return_parameters.status}')
|
||||
response = await self.send_command(
|
||||
HCI_Read_Local_Supported_Commands_Command(), check_result=True
|
||||
)
|
||||
self.local_supported_commands = response.return_parameters.supported_commands
|
||||
|
||||
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}')
|
||||
response = await self.send_command(
|
||||
HCI_LE_Read_Local_Supported_Features_Command(), check_result=True
|
||||
)
|
||||
self.local_le_features = struct.unpack(
|
||||
'<Q', response.return_parameters.le_features
|
||||
)[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:
|
||||
self.local_version = response.return_parameters
|
||||
else:
|
||||
logger.warn(f'HCI_Read_Local_Version_Information_Command failed: {response.return_parameters.status}')
|
||||
response = await self.send_command(
|
||||
HCI_Read_Local_Version_Information_Command(), check_result=True
|
||||
)
|
||||
self.local_version = response.return_parameters
|
||||
|
||||
await self.send_command(HCI_Set_Event_Mask_Command(event_mask = bytes.fromhex('FFFFFFFFFFFFFF3F')))
|
||||
await self.send_command(
|
||||
HCI_Set_Event_Mask_Command(event_mask=bytes.fromhex('FFFFFFFFFFFFFF3F'))
|
||||
)
|
||||
|
||||
if self.local_version.hci_version <= HCI_VERSION_BLUETOOTH_CORE_4_0:
|
||||
# Some older controllers don't like event masks with bits they don't understand
|
||||
if (
|
||||
self.local_version is not None
|
||||
and self.local_version.hci_version <= HCI_VERSION_BLUETOOTH_CORE_4_0
|
||||
):
|
||||
# Some older controllers don't like event masks with bits they don't
|
||||
# understand
|
||||
le_event_mask = bytes.fromhex('1F00000000000000')
|
||||
else:
|
||||
le_event_mask = bytes.fromhex('FFFFF00000000000')
|
||||
await self.send_command(HCI_LE_Set_Event_Mask_Command(le_event_mask = le_event_mask))
|
||||
|
||||
await self.send_command(
|
||||
HCI_LE_Set_Event_Mask_Command(le_event_mask=le_event_mask)
|
||||
)
|
||||
|
||||
if self.supports_command(HCI_READ_BUFFER_SIZE_COMMAND):
|
||||
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_acl_data_packet_length
|
||||
self.hc_total_num_acl_data_packets = response.return_parameters.hc_total_num_acl_data_packets
|
||||
else:
|
||||
logger.warn(f'HCI_Read_Buffer_Size_Command failed: {response.return_parameters.status}')
|
||||
response = await self.send_command(
|
||||
HCI_Read_Buffer_Size_Command(), check_result=True
|
||||
)
|
||||
self.hc_acl_data_packet_length = (
|
||||
response.return_parameters.hc_acl_data_packet_length
|
||||
)
|
||||
self.hc_total_num_acl_data_packets = (
|
||||
response.return_parameters.hc_total_num_acl_data_packets
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
'HCI ACL flow control: '
|
||||
f'hc_acl_data_packet_length={self.hc_acl_data_packet_length},'
|
||||
f'hc_total_num_acl_data_packets={self.hc_total_num_acl_data_packets}'
|
||||
)
|
||||
|
||||
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
|
||||
else:
|
||||
logger.warn(f'HCI_LE_Read_Buffer_Size_Command failed: {response.return_parameters.status}')
|
||||
response = await self.send_command(
|
||||
HCI_LE_Read_Buffer_Size_Command(), check_result=True
|
||||
)
|
||||
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
|
||||
)
|
||||
|
||||
if response.return_parameters.hc_le_acl_data_packet_length == 0 or response.return_parameters.hc_total_num_le_acl_data_packets == 0:
|
||||
# LE and Classic share the same values
|
||||
self.hc_le_acl_data_packet_length = self.hc_acl_data_packet_length
|
||||
self.hc_total_num_le_acl_data_packets = self.hc_total_num_acl_data_packets
|
||||
logger.debug(
|
||||
'HCI LE ACL flow control: '
|
||||
f'hc_le_acl_data_packet_length={self.hc_le_acl_data_packet_length},'
|
||||
'hc_total_num_le_acl_data_packets='
|
||||
f'{self.hc_total_num_le_acl_data_packets}'
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f'HCI ACL flow control: hc_acl_data_packet_length={self.hc_acl_data_packet_length},'
|
||||
f'hc_total_num_acl_data_packets={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},'
|
||||
f'hc_total_num_le_acl_data_packets={self.hc_total_num_le_acl_data_packets}'
|
||||
)
|
||||
if (
|
||||
response.return_parameters.hc_le_acl_data_packet_length == 0
|
||||
or response.return_parameters.hc_total_num_le_acl_data_packets == 0
|
||||
):
|
||||
# LE and Classic share the same values
|
||||
self.hc_le_acl_data_packet_length = self.hc_acl_data_packet_length
|
||||
self.hc_total_num_le_acl_data_packets = (
|
||||
self.hc_total_num_acl_data_packets
|
||||
)
|
||||
|
||||
if self.supports_command(
|
||||
HCI_LE_READ_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND
|
||||
) and self.supports_command(HCI_LE_WRITE_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND):
|
||||
response = await self.send_command(
|
||||
HCI_LE_Read_Suggested_Default_Data_Length_Command()
|
||||
)
|
||||
suggested_max_tx_octets = response.return_parameters.suggested_max_tx_octets
|
||||
suggested_max_tx_time = response.return_parameters.suggested_max_tx_time
|
||||
if (
|
||||
suggested_max_tx_octets != self.suggested_max_tx_octets
|
||||
or suggested_max_tx_time != self.suggested_max_tx_time
|
||||
):
|
||||
await self.send_command(
|
||||
HCI_LE_Write_Suggested_Default_Data_Length_Command(
|
||||
suggested_max_tx_octets=self.suggested_max_tx_octets,
|
||||
suggested_max_tx_time=self.suggested_max_tx_time,
|
||||
)
|
||||
)
|
||||
|
||||
self.reset_done = True
|
||||
|
||||
@@ -169,9 +294,12 @@ class Host(EventEmitter):
|
||||
self.hci_sink = sink
|
||||
|
||||
def send_hci_packet(self, packet):
|
||||
if self.snooper:
|
||||
self.snooper.snoop(bytes(packet), Snooper.Direction.HOST_TO_CONTROLLER)
|
||||
|
||||
self.hci_sink.on_packet(packet.to_bytes())
|
||||
|
||||
async def send_command(self, command):
|
||||
async def send_command(self, command, check_result=False):
|
||||
logger.debug(f'{color("### HOST -> CONTROLLER", "blue")}: {command}')
|
||||
|
||||
# Wait until we can send (only one pending command at a time)
|
||||
@@ -186,11 +314,29 @@ class Host(EventEmitter):
|
||||
try:
|
||||
self.send_hci_packet(command)
|
||||
response = await self.pending_response
|
||||
# TODO: check error values
|
||||
|
||||
# Check the return parameters if required
|
||||
if check_result:
|
||||
if isinstance(response.return_parameters, int):
|
||||
status = response.return_parameters
|
||||
elif isinstance(response.return_parameters, bytes):
|
||||
# return parameters first field is a one byte status code
|
||||
status = response.return_parameters[0]
|
||||
else:
|
||||
status = response.return_parameters.status
|
||||
|
||||
if status != HCI_SUCCESS:
|
||||
logger.warning(
|
||||
f'{command.name} failed ({HCI_Constant.error_name(status)})'
|
||||
)
|
||||
raise HCI_Error(status)
|
||||
|
||||
return response
|
||||
except Exception as error:
|
||||
logger.warning(f'{color("!!! Exception while sending HCI packet:", "red")} {error}')
|
||||
# raise error
|
||||
logger.warning(
|
||||
f'{color("!!! Exception while sending HCI packet:", "red")} {error}'
|
||||
)
|
||||
raise error
|
||||
finally:
|
||||
self.pending_command = None
|
||||
self.pending_response = None
|
||||
@@ -213,13 +359,15 @@ class Host(EventEmitter):
|
||||
# TODO: support different LE/Classic lengths
|
||||
data_total_length = min(bytes_remaining, self.hc_le_acl_data_packet_length)
|
||||
acl_packet = HCI_AclDataPacket(
|
||||
connection_handle = connection_handle,
|
||||
pb_flag = pb_flag,
|
||||
bc_flag = 0,
|
||||
data_total_length = data_total_length,
|
||||
data = l2cap_pdu[offset:offset + data_total_length]
|
||||
connection_handle=connection_handle,
|
||||
pb_flag=pb_flag,
|
||||
bc_flag=0,
|
||||
data_total_length=data_total_length,
|
||||
data=l2cap_pdu[offset : offset + data_total_length],
|
||||
)
|
||||
logger.debug(
|
||||
f'{color("### HOST -> CONTROLLER", "blue")}: (CID={cid}) {acl_packet}'
|
||||
)
|
||||
logger.debug(f'{color("### HOST -> CONTROLLER", "blue")}: (CID={cid}) {acl_packet}')
|
||||
self.queue_acl_packet(acl_packet)
|
||||
pb_flag = 1
|
||||
offset += data_total_length
|
||||
@@ -230,30 +378,38 @@ class Host(EventEmitter):
|
||||
self.check_acl_packet_queue()
|
||||
|
||||
if len(self.acl_packet_queue):
|
||||
logger.debug(f'{self.acl_packets_in_flight} ACL packets in flight, {len(self.acl_packet_queue)} in queue')
|
||||
logger.debug(
|
||||
f'{self.acl_packets_in_flight} ACL packets in flight, '
|
||||
f'{len(self.acl_packet_queue)} in queue'
|
||||
)
|
||||
|
||||
def check_acl_packet_queue(self):
|
||||
# Send all we can (TODO: support different LE/Classic limits)
|
||||
while len(self.acl_packet_queue) > 0 and self.acl_packets_in_flight < self.hc_total_num_le_acl_data_packets:
|
||||
while (
|
||||
len(self.acl_packet_queue) > 0
|
||||
and self.acl_packets_in_flight < self.hc_total_num_le_acl_data_packets
|
||||
):
|
||||
packet = self.acl_packet_queue.pop()
|
||||
self.send_hci_packet(packet)
|
||||
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):
|
||||
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 (
|
||||
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):
|
||||
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:
|
||||
@@ -268,15 +424,17 @@ class Host(EventEmitter):
|
||||
|
||||
@property
|
||||
def supported_le_features(self):
|
||||
return [feature for feature in range(64) if self.local_le_features & (1 << feature)]
|
||||
return [
|
||||
feature for feature in range(64) if self.local_le_features & (1 << feature)
|
||||
]
|
||||
|
||||
# Packet Sink protocol (packets coming from the controller via HCI)
|
||||
def on_packet(self, packet):
|
||||
hci_packet = HCI_Packet.from_bytes(packet)
|
||||
if self.ready or (
|
||||
hci_packet.hci_packet_type == HCI_EVENT_PACKET and
|
||||
hci_packet.event_code == HCI_COMMAND_COMPLETE_EVENT and
|
||||
hci_packet.command_opcode == HCI_RESET_COMMAND
|
||||
hci_packet.hci_packet_type == HCI_EVENT_PACKET
|
||||
and hci_packet.event_code == HCI_COMMAND_COMPLETE_EVENT
|
||||
and hci_packet.command_opcode == HCI_RESET_COMMAND
|
||||
):
|
||||
self.on_hci_packet(hci_packet)
|
||||
else:
|
||||
@@ -285,6 +443,9 @@ class Host(EventEmitter):
|
||||
def on_hci_packet(self, packet):
|
||||
logger.debug(f'{color("### CONTROLLER -> HOST", "green")}: {packet}')
|
||||
|
||||
if self.snooper:
|
||||
self.snooper.snoop(bytes(packet), Snooper.Direction.CONTROLLER_TO_HOST)
|
||||
|
||||
# If the packet is a command, invoke the handler for this packet
|
||||
if packet.hci_packet_type == HCI_COMMAND_PACKET:
|
||||
self.on_hci_command_packet(packet)
|
||||
@@ -315,7 +476,11 @@ class Host(EventEmitter):
|
||||
if self.pending_response:
|
||||
# Check that it is what we were expecting
|
||||
if self.pending_command.op_code != event.command_opcode:
|
||||
logger.warning(f'!!! command result mismatch, expected 0x{self.pending_command.op_code:X} but got 0x{event.command_opcode:X}')
|
||||
logger.warning(
|
||||
'!!! command result mismatch, expected '
|
||||
f'0x{self.pending_command.op_code:X} but got '
|
||||
f'0x{event.command_opcode:X}'
|
||||
)
|
||||
|
||||
self.pending_response.set_result(event)
|
||||
else:
|
||||
@@ -329,10 +494,12 @@ class Host(EventEmitter):
|
||||
|
||||
def on_hci_command_complete_event(self, event):
|
||||
if event.command_opcode == 0:
|
||||
# This is used just for the Num_HCI_Command_Packets field, not related to an actual command
|
||||
# This is used just for the Num_HCI_Command_Packets field, not related to
|
||||
# an actual command
|
||||
logger.debug('no-command event')
|
||||
else:
|
||||
return self.on_command_processed(event)
|
||||
return None
|
||||
|
||||
return self.on_command_processed(event)
|
||||
|
||||
def on_hci_command_status_event(self, event):
|
||||
return self.on_command_processed(event)
|
||||
@@ -343,51 +510,64 @@ class Host(EventEmitter):
|
||||
self.acl_packets_in_flight -= total_packets
|
||||
self.check_acl_packet_queue()
|
||||
else:
|
||||
logger.warning(color(f'!!! {total_packets} completed but only {self.acl_packets_in_flight} in flight'))
|
||||
logger.warning(
|
||||
color(
|
||||
'!!! {total_packets} completed but only '
|
||||
f'{self.acl_packets_in_flight} in flight'
|
||||
)
|
||||
)
|
||||
self.acl_packets_in_flight = 0
|
||||
|
||||
# Classic only
|
||||
def on_hci_connection_request_event(self, event):
|
||||
# For now, just accept everything
|
||||
# TODO: delegate the decision
|
||||
self.send_command_sync(
|
||||
HCI_Accept_Connection_Request_Command(
|
||||
bd_addr = event.bd_addr,
|
||||
role = 0x01 # Remain the peripheral
|
||||
)
|
||||
# Notify the listeners
|
||||
self.emit(
|
||||
'connection_request',
|
||||
event.bd_addr,
|
||||
event.class_of_device,
|
||||
event.link_type,
|
||||
)
|
||||
|
||||
def on_hci_le_connection_complete_event(self, event):
|
||||
# Check if this is a cancellation
|
||||
if event.status == HCI_SUCCESS:
|
||||
# Create/update the connection
|
||||
logger.debug(f'### CONNECTION: [0x{event.connection_handle:04X}] {event.peer_address} as {HCI_Constant.role_name(event.role)}')
|
||||
logger.debug(
|
||||
f'### LE CONNECTION: [0x{event.connection_handle:04X}] '
|
||||
f'{event.peer_address} as {HCI_Constant.role_name(event.role)}'
|
||||
)
|
||||
|
||||
connection = self.connections.get(event.connection_handle)
|
||||
if connection is None:
|
||||
connection = Connection(self, event.connection_handle, event.role, event.peer_address, BT_LE_TRANSPORT)
|
||||
connection = Connection(
|
||||
self,
|
||||
event.connection_handle,
|
||||
event.peer_address,
|
||||
BT_LE_TRANSPORT,
|
||||
)
|
||||
self.connections[event.connection_handle] = connection
|
||||
|
||||
# Notify the client
|
||||
connection_parameters = ConnectionParameters(
|
||||
event.conn_interval,
|
||||
event.conn_latency,
|
||||
event.supervision_timeout
|
||||
event.connection_interval,
|
||||
event.peripheral_latency,
|
||||
event.supervision_timeout,
|
||||
)
|
||||
self.emit(
|
||||
'connection',
|
||||
event.connection_handle,
|
||||
BT_LE_TRANSPORT,
|
||||
event.peer_address,
|
||||
None,
|
||||
event.role,
|
||||
connection_parameters
|
||||
connection_parameters,
|
||||
)
|
||||
else:
|
||||
logger.debug(f'### CONNECTION FAILED: {event.status}')
|
||||
|
||||
# Notify the listeners
|
||||
self.emit('connection_failure', event.status)
|
||||
self.emit(
|
||||
'connection_failure', BT_LE_TRANSPORT, event.peer_address, event.status
|
||||
)
|
||||
|
||||
def on_hci_le_enhanced_connection_complete_event(self, event):
|
||||
# Just use the same implementation as for the non-enhanced event for now
|
||||
@@ -396,11 +576,19 @@ class Host(EventEmitter):
|
||||
def on_hci_connection_complete_event(self, event):
|
||||
if event.status == HCI_SUCCESS:
|
||||
# Create/update the connection
|
||||
logger.debug(f'### BR/EDR CONNECTION: [0x{event.connection_handle:04X}] {event.bd_addr}')
|
||||
logger.debug(
|
||||
f'### BR/EDR CONNECTION: [0x{event.connection_handle:04X}] '
|
||||
f'{event.bd_addr}'
|
||||
)
|
||||
|
||||
connection = self.connections.get(event.connection_handle)
|
||||
if connection is None:
|
||||
connection = Connection(self, event.connection_handle, BT_CENTRAL_ROLE, event.bd_addr, BT_BR_EDR_TRANSPORT)
|
||||
connection = Connection(
|
||||
self,
|
||||
event.connection_handle,
|
||||
event.bd_addr,
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
)
|
||||
self.connections[event.connection_handle] = connection
|
||||
|
||||
# Notify the client
|
||||
@@ -410,14 +598,15 @@ class Host(EventEmitter):
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
event.bd_addr,
|
||||
None,
|
||||
BT_CENTRAL_ROLE,
|
||||
None
|
||||
None,
|
||||
)
|
||||
else:
|
||||
logger.debug(f'### BR/EDR CONNECTION FAILED: {event.status}')
|
||||
|
||||
# Notify the client
|
||||
self.emit('connection_failure', event.connection_handle, event.status)
|
||||
self.emit(
|
||||
'connection_failure', BT_BR_EDR_TRANSPORT, event.bd_addr, event.status
|
||||
)
|
||||
|
||||
def on_hci_disconnection_complete_event(self, event):
|
||||
# Find the connection
|
||||
@@ -426,7 +615,11 @@ class Host(EventEmitter):
|
||||
return
|
||||
|
||||
if event.status == HCI_SUCCESS:
|
||||
logger.debug(f'### DISCONNECTION: [0x{event.connection_handle:04X}] {connection.peer_address} as {HCI_Constant.role_name(connection.role)}, reason={event.reason}')
|
||||
logger.debug(
|
||||
f'### DISCONNECTION: [0x{event.connection_handle:04X}] '
|
||||
f'{connection.peer_address} '
|
||||
f'reason={event.reason}'
|
||||
)
|
||||
del self.connections[event.connection_handle]
|
||||
|
||||
# Notify the listeners
|
||||
@@ -435,7 +628,7 @@ class Host(EventEmitter):
|
||||
logger.debug(f'### DISCONNECTION FAILED: {event.status}')
|
||||
|
||||
# Notify the listeners
|
||||
self.emit('disconnection_failure', event.status)
|
||||
self.emit('disconnection_failure', event.connection_handle, event.status)
|
||||
|
||||
def on_hci_le_connection_update_complete_event(self, event):
|
||||
if (connection := self.connections.get(event.connection_handle)) is None:
|
||||
@@ -445,13 +638,17 @@ class Host(EventEmitter):
|
||||
# Notify the client
|
||||
if event.status == HCI_SUCCESS:
|
||||
connection_parameters = ConnectionParameters(
|
||||
event.conn_interval,
|
||||
event.conn_latency,
|
||||
event.supervision_timeout
|
||||
event.connection_interval,
|
||||
event.peripheral_latency,
|
||||
event.supervision_timeout,
|
||||
)
|
||||
self.emit(
|
||||
'connection_parameters_update', connection.handle, connection_parameters
|
||||
)
|
||||
self.emit('connection_parameters_update', connection.handle, connection_parameters)
|
||||
else:
|
||||
self.emit('connection_parameters_update_failure', connection.handle, event.status)
|
||||
self.emit(
|
||||
'connection_parameters_update_failure', connection.handle, event.status
|
||||
)
|
||||
|
||||
def on_hci_le_phy_update_complete_event(self, event):
|
||||
if (connection := self.connections.get(event.connection_handle)) is None:
|
||||
@@ -467,13 +664,10 @@ class Host(EventEmitter):
|
||||
|
||||
def on_hci_le_advertising_report_event(self, event):
|
||||
for report in event.reports:
|
||||
self.emit(
|
||||
'advertising_report',
|
||||
report.address,
|
||||
report.data,
|
||||
report.rssi,
|
||||
report.event_type
|
||||
)
|
||||
self.emit('advertising_report', report)
|
||||
|
||||
def on_hci_le_extended_advertising_report_event(self, event):
|
||||
self.on_hci_le_advertising_report_event(event)
|
||||
|
||||
def on_hci_le_remote_connection_parameter_request_event(self, event):
|
||||
if event.connection_handle not in self.connections:
|
||||
@@ -484,13 +678,13 @@ class Host(EventEmitter):
|
||||
# TODO: delegate the decision
|
||||
self.send_command_sync(
|
||||
HCI_LE_Remote_Connection_Parameter_Request_Reply_Command(
|
||||
connection_handle = event.connection_handle,
|
||||
interval_min = event.interval_min,
|
||||
interval_max = event.interval_max,
|
||||
latency = event.latency,
|
||||
timeout = event.timeout,
|
||||
minimum_ce_length = 0,
|
||||
maximum_ce_length = 0
|
||||
connection_handle=event.connection_handle,
|
||||
interval_min=event.interval_min,
|
||||
interval_max=event.interval_max,
|
||||
max_latency=event.max_latency,
|
||||
timeout=event.timeout,
|
||||
min_ce_length=0,
|
||||
max_ce_length=0,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -504,19 +698,23 @@ class Host(EventEmitter):
|
||||
logger.debug('no long term key provider')
|
||||
long_term_key = None
|
||||
else:
|
||||
long_term_key = await self.long_term_key_provider(
|
||||
connection.handle,
|
||||
event.random_number,
|
||||
event.encryption_diversifier
|
||||
long_term_key = await self.abort_on(
|
||||
'flush',
|
||||
# pylint: disable-next=not-callable
|
||||
self.long_term_key_provider(
|
||||
connection.handle,
|
||||
event.random_number,
|
||||
event.encryption_diversifier,
|
||||
),
|
||||
)
|
||||
if long_term_key:
|
||||
response = HCI_LE_Long_Term_Key_Request_Reply_Command(
|
||||
connection_handle = event.connection_handle,
|
||||
long_term_key = long_term_key
|
||||
connection_handle=event.connection_handle,
|
||||
long_term_key=long_term_key,
|
||||
)
|
||||
else:
|
||||
response = HCI_LE_Long_Term_Key_Request_Negative_Reply_Command(
|
||||
connection_handle = event.connection_handle
|
||||
connection_handle=event.connection_handle
|
||||
)
|
||||
|
||||
await self.send_command(response)
|
||||
@@ -531,10 +729,17 @@ class Host(EventEmitter):
|
||||
|
||||
def on_hci_role_change_event(self, event):
|
||||
if event.status == HCI_SUCCESS:
|
||||
logger.debug(f'role change for {event.bd_addr}: {HCI_Constant.role_name(event.new_role)}')
|
||||
# TODO: lookup the connection and update the role
|
||||
logger.debug(
|
||||
f'role change for {event.bd_addr}: '
|
||||
f'{HCI_Constant.role_name(event.new_role)}'
|
||||
)
|
||||
self.emit('role_change', event.bd_addr, event.new_role)
|
||||
else:
|
||||
logger.debug(f'role change for {event.bd_addr} failed: {HCI_Constant.error_name(event.status)}')
|
||||
logger.debug(
|
||||
f'role change for {event.bd_addr} failed: '
|
||||
f'{HCI_Constant.error_name(event.status)}'
|
||||
)
|
||||
self.emit('role_change_failure', event.bd_addr, event.status)
|
||||
|
||||
def on_hci_le_data_length_change_event(self, event):
|
||||
self.emit(
|
||||
@@ -543,7 +748,7 @@ class Host(EventEmitter):
|
||||
event.max_tx_octets,
|
||||
event.max_tx_time,
|
||||
event.max_rx_octets,
|
||||
event.max_rx_time
|
||||
event.max_rx_time,
|
||||
)
|
||||
|
||||
def on_hci_authentication_complete_event(self, event):
|
||||
@@ -551,21 +756,35 @@ class Host(EventEmitter):
|
||||
if event.status == HCI_SUCCESS:
|
||||
self.emit('connection_authentication', event.connection_handle)
|
||||
else:
|
||||
self.emit('connection_authentication_failure', event.connection_handle, event.status)
|
||||
self.emit(
|
||||
'connection_authentication_failure',
|
||||
event.connection_handle,
|
||||
event.status,
|
||||
)
|
||||
|
||||
def on_hci_encryption_change_event(self, event):
|
||||
# Notify the client
|
||||
if event.status == HCI_SUCCESS:
|
||||
self.emit('connection_encryption_change', event.connection_handle, event.encryption_enabled)
|
||||
self.emit(
|
||||
'connection_encryption_change',
|
||||
event.connection_handle,
|
||||
event.encryption_enabled,
|
||||
)
|
||||
else:
|
||||
self.emit('connection_encryption_failure', event.connection_handle, event.status)
|
||||
self.emit(
|
||||
'connection_encryption_failure', event.connection_handle, event.status
|
||||
)
|
||||
|
||||
def on_hci_encryption_key_refresh_complete_event(self, event):
|
||||
# Notify the client
|
||||
if event.status == HCI_SUCCESS:
|
||||
self.emit('connection_encryption_key_refresh', event.connection_handle)
|
||||
else:
|
||||
self.emit('connection_encryption_key_refresh_failure', event.connection_handle, event.status)
|
||||
self.emit(
|
||||
'connection_encryption_key_refresh_failure',
|
||||
event.connection_handle,
|
||||
event.status,
|
||||
)
|
||||
|
||||
def on_hci_link_supervision_timeout_changed_event(self, event):
|
||||
pass
|
||||
@@ -577,20 +796,20 @@ class Host(EventEmitter):
|
||||
pass
|
||||
|
||||
def on_hci_link_key_notification_event(self, event):
|
||||
logger.debug(f'link key for {event.bd_addr}: {event.link_key.hex()}, type={HCI_Constant.link_key_type_name(event.key_type)}')
|
||||
logger.debug(
|
||||
f'link key for {event.bd_addr}: {event.link_key.hex()}, '
|
||||
f'type={HCI_Constant.link_key_type_name(event.key_type)}'
|
||||
)
|
||||
self.emit('link_key', event.bd_addr, event.link_key, event.key_type)
|
||||
|
||||
def on_hci_simple_pairing_complete_event(self, event):
|
||||
logger.debug(f'simple pairing complete for {event.bd_addr}: status={HCI_Constant.status_name(event.status)}')
|
||||
logger.debug(
|
||||
f'simple pairing complete for {event.bd_addr}: '
|
||||
f'status={HCI_Constant.status_name(event.status)}'
|
||||
)
|
||||
|
||||
def on_hci_pin_code_request_event(self, event):
|
||||
# For now, just refuse all requests
|
||||
# TODO: delegate the decision
|
||||
self.send_command_sync(
|
||||
HCI_PIN_Code_Request_Negative_Reply_Command(
|
||||
bd_addr = event.bd_addr
|
||||
)
|
||||
)
|
||||
self.emit('pin_code_request', event.bd_addr)
|
||||
|
||||
def on_hci_link_key_request_event(self, event):
|
||||
async def send_link_key():
|
||||
@@ -598,15 +817,18 @@ class Host(EventEmitter):
|
||||
logger.debug('no link key provider')
|
||||
link_key = None
|
||||
else:
|
||||
link_key = await self.link_key_provider(event.bd_addr)
|
||||
link_key = await self.abort_on(
|
||||
'flush',
|
||||
# pylint: disable-next=not-callable
|
||||
self.link_key_provider(event.bd_addr),
|
||||
)
|
||||
if link_key:
|
||||
response = HCI_Link_Key_Request_Reply_Command(
|
||||
bd_addr = event.bd_addr,
|
||||
link_key = link_key
|
||||
bd_addr=event.bd_addr, link_key=link_key
|
||||
)
|
||||
else:
|
||||
response = HCI_Link_Key_Request_Negative_Reply_Command(
|
||||
bd_addr = event.bd_addr
|
||||
bd_addr=event.bd_addr
|
||||
)
|
||||
|
||||
await self.send_command(response)
|
||||
@@ -617,15 +839,29 @@ class Host(EventEmitter):
|
||||
self.emit('authentication_io_capability_request', event.bd_addr)
|
||||
|
||||
def on_hci_io_capability_response_event(self, event):
|
||||
pass
|
||||
self.emit(
|
||||
'authentication_io_capability_response',
|
||||
event.bd_addr,
|
||||
event.io_capability,
|
||||
event.authentication_requirements,
|
||||
)
|
||||
|
||||
def on_hci_user_confirmation_request_event(self, event):
|
||||
self.emit('authentication_user_confirmation_request', event.bd_addr, event.numeric_value)
|
||||
self.emit(
|
||||
'authentication_user_confirmation_request',
|
||||
event.bd_addr,
|
||||
event.numeric_value,
|
||||
)
|
||||
|
||||
def on_hci_user_passkey_request_event(self, event):
|
||||
self.emit('authentication_user_passkey_request', event.bd_addr)
|
||||
|
||||
def on_hci_inquiry_complete_event(self, event):
|
||||
def on_hci_user_passkey_notification_event(self, event):
|
||||
self.emit(
|
||||
'authentication_user_passkey_notification', event.bd_addr, event.passkey
|
||||
)
|
||||
|
||||
def on_hci_inquiry_complete_event(self, _event):
|
||||
self.emit('inquiry_complete')
|
||||
|
||||
def on_hci_inquiry_result_with_rssi_event(self, event):
|
||||
@@ -635,7 +871,7 @@ class Host(EventEmitter):
|
||||
response.bd_addr,
|
||||
response.class_of_device,
|
||||
b'',
|
||||
response.rssi
|
||||
response.rssi,
|
||||
)
|
||||
|
||||
def on_hci_extended_inquiry_result_event(self, event):
|
||||
@@ -644,7 +880,7 @@ class Host(EventEmitter):
|
||||
event.bd_addr,
|
||||
event.class_of_device,
|
||||
event.extended_inquiry_response,
|
||||
event.rssi
|
||||
event.rssi,
|
||||
)
|
||||
|
||||
def on_hci_remote_name_request_complete_event(self, event):
|
||||
@@ -652,3 +888,10 @@ class Host(EventEmitter):
|
||||
self.emit('remote_name_failure', event.bd_addr, event.status)
|
||||
else:
|
||||
self.emit('remote_name', event.bd_addr, event.remote_name)
|
||||
|
||||
def on_hci_remote_host_supported_features_notification_event(self, event):
|
||||
self.emit(
|
||||
'remote_host_supported_features',
|
||||
event.bd_addr,
|
||||
event.host_supported_features,
|
||||
)
|
||||
|
||||
126
bumble/keys.py
126
bumble/keys.py
@@ -20,13 +20,19 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import json
|
||||
from colors import color
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from .colors import color
|
||||
from .hci import Address
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .device import Device
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -38,10 +44,10 @@ logger = logging.getLogger(__name__)
|
||||
class PairingKeys:
|
||||
class Key:
|
||||
def __init__(self, value, authenticated=False, ediv=None, rand=None):
|
||||
self.value = value
|
||||
self.value = value
|
||||
self.authenticated = authenticated
|
||||
self.ediv = ediv
|
||||
self.rand = rand
|
||||
self.ediv = ediv
|
||||
self.rand = rand
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, key_dict):
|
||||
@@ -64,31 +70,33 @@ class PairingKeys:
|
||||
return key_dict
|
||||
|
||||
def __init__(self):
|
||||
self.address_type = None
|
||||
self.ltk = None
|
||||
self.ltk_central = None
|
||||
self.address_type = None
|
||||
self.ltk = None
|
||||
self.ltk_central = None
|
||||
self.ltk_peripheral = None
|
||||
self.irk = None
|
||||
self.csrk = None
|
||||
self.link_key = None # Classic
|
||||
self.irk = None
|
||||
self.csrk = None
|
||||
self.link_key = None # Classic
|
||||
|
||||
@staticmethod
|
||||
def key_from_dict(keys_dict, key_name):
|
||||
key_dict = keys_dict.get(key_name)
|
||||
if key_dict is not None:
|
||||
return PairingKeys.Key.from_dict(key_dict)
|
||||
if key_dict is None:
|
||||
return None
|
||||
|
||||
return PairingKeys.Key.from_dict(key_dict)
|
||||
|
||||
@staticmethod
|
||||
def from_dict(keys_dict):
|
||||
keys = PairingKeys()
|
||||
|
||||
keys.address_type = keys_dict.get('address_type')
|
||||
keys.ltk = PairingKeys.key_from_dict(keys_dict, 'ltk')
|
||||
keys.ltk_central = PairingKeys.key_from_dict(keys_dict, 'ltk_central')
|
||||
keys.address_type = keys_dict.get('address_type')
|
||||
keys.ltk = PairingKeys.key_from_dict(keys_dict, 'ltk')
|
||||
keys.ltk_central = PairingKeys.key_from_dict(keys_dict, 'ltk_central')
|
||||
keys.ltk_peripheral = PairingKeys.key_from_dict(keys_dict, 'ltk_peripheral')
|
||||
keys.irk = PairingKeys.key_from_dict(keys_dict, 'irk')
|
||||
keys.csrk = PairingKeys.key_from_dict(keys_dict, 'csrk')
|
||||
keys.link_key = PairingKeys.key_from_dict(keys_dict, 'link_key')
|
||||
keys.irk = PairingKeys.key_from_dict(keys_dict, 'irk')
|
||||
keys.csrk = PairingKeys.key_from_dict(keys_dict, 'csrk')
|
||||
keys.link_key = PairingKeys.key_from_dict(keys_dict, 'link_key')
|
||||
|
||||
return keys
|
||||
|
||||
@@ -120,13 +128,13 @@ class PairingKeys:
|
||||
|
||||
def print(self, prefix=''):
|
||||
keys_dict = self.to_dict()
|
||||
for (property, value) in keys_dict.items():
|
||||
if type(value) is dict:
|
||||
print(f'{prefix}{color(property, "cyan")}:')
|
||||
for (container_property, value) in keys_dict.items():
|
||||
if isinstance(value, dict):
|
||||
print(f'{prefix}{color(container_property, "cyan")}:')
|
||||
for (key_property, key_value) in value.items():
|
||||
print(f'{prefix} {color(key_property, "green")}: {key_value}')
|
||||
else:
|
||||
print(f'{prefix}{color(property, "cyan")}: {value}')
|
||||
print(f'{prefix}{color(container_property, "cyan")}: {value}')
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -137,12 +145,16 @@ class KeyStore:
|
||||
async def update(self, name, keys):
|
||||
pass
|
||||
|
||||
async def get(self, name):
|
||||
async def get(self, _name):
|
||||
return PairingKeys()
|
||||
|
||||
async def get_all(self):
|
||||
return []
|
||||
|
||||
async def delete_all(self):
|
||||
all_keys = await self.get_all()
|
||||
await asyncio.gather(*(self.delete(name) for (name, _) in all_keys))
|
||||
|
||||
async def get_resolving_keys(self):
|
||||
all_keys = await self.get_all()
|
||||
resolving_keys = []
|
||||
@@ -161,26 +173,26 @@ class KeyStore:
|
||||
separator = ''
|
||||
for (name, keys) in entries:
|
||||
print(separator + prefix + color(name, 'yellow'))
|
||||
keys.print(prefix = prefix + ' ')
|
||||
keys.print(prefix=prefix + ' ')
|
||||
separator = '\n'
|
||||
|
||||
@staticmethod
|
||||
def create_for_device(device_config):
|
||||
if device_config.keystore is None:
|
||||
def create_for_device(device: Device) -> Optional[KeyStore]:
|
||||
if device.config.keystore is None:
|
||||
return None
|
||||
|
||||
keystore_type = device_config.keystore.split(':', 1)[0]
|
||||
keystore_type = device.config.keystore.split(':', 1)[0]
|
||||
if keystore_type == 'JsonKeyStore':
|
||||
return JsonKeyStore.from_device_config(device_config)
|
||||
return JsonKeyStore.from_device(device)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class JsonKeyStore(KeyStore):
|
||||
APP_NAME = 'Bumble'
|
||||
APP_AUTHOR = 'Google'
|
||||
KEYS_DIR = 'Pairing'
|
||||
APP_NAME = 'Bumble'
|
||||
APP_AUTHOR = 'Google'
|
||||
KEYS_DIR = 'Pairing'
|
||||
DEFAULT_NAMESPACE = '__DEFAULT__'
|
||||
|
||||
def __init__(self, namespace, filename=None):
|
||||
@@ -188,12 +200,17 @@ class JsonKeyStore(KeyStore):
|
||||
|
||||
if filename is None:
|
||||
# Use a default for the current user
|
||||
|
||||
# Import here because this may not exist on all platforms
|
||||
# pylint: disable=import-outside-toplevel
|
||||
import appdirs
|
||||
|
||||
self.directory_name = os.path.join(
|
||||
appdirs.user_data_dir(self.APP_NAME, self.APP_AUTHOR),
|
||||
self.KEYS_DIR
|
||||
appdirs.user_data_dir(self.APP_NAME, self.APP_AUTHOR), self.KEYS_DIR
|
||||
)
|
||||
json_filename = (
|
||||
f'{self.namespace}.json'.lower().replace(':', '-').replace('/p', '-p')
|
||||
)
|
||||
json_filename = f'{self.namespace}.json'.lower().replace(':', '-')
|
||||
self.filename = os.path.join(self.directory_name, json_filename)
|
||||
else:
|
||||
self.filename = filename
|
||||
@@ -202,11 +219,21 @@ class JsonKeyStore(KeyStore):
|
||||
logger.debug(f'JSON keystore: {self.filename}')
|
||||
|
||||
@staticmethod
|
||||
def from_device_config(device_config):
|
||||
params = device_config.keystore.split(':', 1)[1:]
|
||||
namespace = str(device_config.address)
|
||||
def from_device(device: Device) -> Optional[JsonKeyStore]:
|
||||
if not device.config.keystore:
|
||||
return None
|
||||
|
||||
params = device.config.keystore.split(':', 1)[1:]
|
||||
|
||||
# Use a namespace based on the device address
|
||||
if device.public_address not in (Address.ANY, Address.ANY_RANDOM):
|
||||
namespace = str(device.public_address)
|
||||
elif device.random_address != Address.ANY_RANDOM:
|
||||
namespace = str(device.random_address)
|
||||
else:
|
||||
namespace = JsonKeyStore.DEFAULT_NAMESPACE
|
||||
if params:
|
||||
filename = params[1]
|
||||
filename = params[0]
|
||||
else:
|
||||
filename = None
|
||||
|
||||
@@ -214,7 +241,7 @@ class JsonKeyStore(KeyStore):
|
||||
|
||||
async def load(self):
|
||||
try:
|
||||
with open(self.filename, 'r') as json_file:
|
||||
with open(self.filename, 'r', encoding='utf-8') as json_file:
|
||||
return json.load(json_file)
|
||||
except FileNotFoundError:
|
||||
return {}
|
||||
@@ -226,13 +253,13 @@ class JsonKeyStore(KeyStore):
|
||||
|
||||
# Save to a temporary file
|
||||
temp_filename = self.filename + '.tmp'
|
||||
with open(temp_filename, 'w') as output:
|
||||
with open(temp_filename, 'w', encoding='utf-8') as output:
|
||||
json.dump(db, output, sort_keys=True, indent=4)
|
||||
|
||||
# Atomically replace the previous file
|
||||
os.rename(temp_filename, self.filename)
|
||||
os.replace(temp_filename, self.filename)
|
||||
|
||||
async def delete(self, name):
|
||||
async def delete(self, name: str) -> None:
|
||||
db = await self.load()
|
||||
|
||||
namespace = db.get(self.namespace)
|
||||
@@ -246,7 +273,7 @@ class JsonKeyStore(KeyStore):
|
||||
db = await self.load()
|
||||
|
||||
namespace = db.setdefault(self.namespace, {})
|
||||
namespace[name] = keys.to_dict()
|
||||
namespace.setdefault(name, {}).update(keys.to_dict())
|
||||
|
||||
await self.save(db)
|
||||
|
||||
@@ -257,9 +284,18 @@ class JsonKeyStore(KeyStore):
|
||||
if namespace is None:
|
||||
return []
|
||||
|
||||
return [(name, PairingKeys.from_dict(keys)) for (name, keys) in namespace.items()]
|
||||
return [
|
||||
(name, PairingKeys.from_dict(keys)) for (name, keys) in namespace.items()
|
||||
]
|
||||
|
||||
async def get(self, name):
|
||||
async def delete_all(self):
|
||||
db = await self.load()
|
||||
|
||||
db.pop(self.namespace, None)
|
||||
|
||||
await self.save(db)
|
||||
|
||||
async def get(self, name: str) -> Optional[PairingKeys]:
|
||||
db = await self.load()
|
||||
|
||||
namespace = db.get(self.namespace)
|
||||
|
||||
1593
bumble/l2cap.py
1593
bumble/l2cap.py
File diff suppressed because it is too large
Load Diff
274
bumble/link.py
274
bumble/link.py
@@ -17,15 +17,17 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
import logging
|
||||
import asyncio
|
||||
import websockets
|
||||
from functools import partial
|
||||
from colors import color
|
||||
|
||||
from bumble.core import BT_PERIPHERAL_ROLE, BT_BR_EDR_TRANSPORT, BT_LE_TRANSPORT
|
||||
from bumble.colors import color
|
||||
from bumble.hci import (
|
||||
Address,
|
||||
HCI_SUCCESS,
|
||||
HCI_CONNECTION_ACCEPT_TIMEOUT_ERROR,
|
||||
HCI_CONNECTION_TIMEOUT_ERROR
|
||||
HCI_CONNECTION_TIMEOUT_ERROR,
|
||||
HCI_PAGE_TIMEOUT_ERROR,
|
||||
HCI_Connection_Complete_Event,
|
||||
)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -47,7 +49,8 @@ def parse_parameters(params_str):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# TODO: add more support for various LL exchanges (see Vol 6, Part B - 2.4 DATA CHANNEL PDU)
|
||||
# TODO: add more support for various LL exchanges
|
||||
# (see Vol 6, Part B - 2.4 DATA CHANNEL PDU)
|
||||
# -----------------------------------------------------------------------------
|
||||
class LocalLink:
|
||||
'''
|
||||
@@ -55,8 +58,13 @@ class LocalLink:
|
||||
'''
|
||||
|
||||
def __init__(self):
|
||||
self.controllers = set()
|
||||
self.controllers = set()
|
||||
self.pending_connection = None
|
||||
self.pending_classic_connection = None
|
||||
|
||||
############################################################
|
||||
# Common utils
|
||||
############################################################
|
||||
|
||||
def add_controller(self, controller):
|
||||
logger.debug(f'new controller: {controller}')
|
||||
@@ -71,22 +79,39 @@ class LocalLink:
|
||||
return controller
|
||||
return None
|
||||
|
||||
def on_address_changed(self, controller):
|
||||
pass
|
||||
def find_classic_controller(self, address):
|
||||
for controller in self.controllers:
|
||||
if controller.public_address == address:
|
||||
return controller
|
||||
return None
|
||||
|
||||
def get_pending_connection(self):
|
||||
return self.pending_connection
|
||||
|
||||
############################################################
|
||||
# LE handlers
|
||||
############################################################
|
||||
|
||||
def on_address_changed(self, controller):
|
||||
pass
|
||||
|
||||
def send_advertising_data(self, sender_address, data):
|
||||
# Send the advertising data to all controllers, except the sender
|
||||
for controller in self.controllers:
|
||||
if controller.random_address != sender_address:
|
||||
controller.on_link_advertising_data(sender_address, data)
|
||||
|
||||
def send_acl_data(self, sender_address, destination_address, data):
|
||||
def send_acl_data(self, sender_controller, destination_address, transport, data):
|
||||
# Send the data to the first controller with a matching address
|
||||
if controller := self.find_controller(destination_address):
|
||||
controller.on_link_acl_data(sender_address, data)
|
||||
if transport == BT_LE_TRANSPORT:
|
||||
destination_controller = self.find_controller(destination_address)
|
||||
source_address = sender_controller.random_address
|
||||
elif transport == BT_BR_EDR_TRANSPORT:
|
||||
destination_controller = self.find_classic_controller(destination_address)
|
||||
source_address = sender_controller.public_address
|
||||
|
||||
if destination_controller is not None:
|
||||
destination_controller.on_link_acl_data(source_address, transport, data)
|
||||
|
||||
def on_connection_complete(self):
|
||||
# Check that we expect this call
|
||||
@@ -103,23 +128,31 @@ class LocalLink:
|
||||
return
|
||||
|
||||
# Connect to the first controller with a matching address
|
||||
if peripheral_controller := self.find_controller(le_create_connection_command.peer_address):
|
||||
central_controller.on_link_peripheral_connection_complete(le_create_connection_command, HCI_SUCCESS)
|
||||
if peripheral_controller := self.find_controller(
|
||||
le_create_connection_command.peer_address
|
||||
):
|
||||
central_controller.on_link_peripheral_connection_complete(
|
||||
le_create_connection_command, HCI_SUCCESS
|
||||
)
|
||||
peripheral_controller.on_link_central_connected(central_address)
|
||||
return
|
||||
|
||||
# No peripheral found
|
||||
central_controller.on_link_peripheral_connection_complete(
|
||||
le_create_connection_command,
|
||||
HCI_CONNECTION_ACCEPT_TIMEOUT_ERROR
|
||||
le_create_connection_command, HCI_CONNECTION_ACCEPT_TIMEOUT_ERROR
|
||||
)
|
||||
|
||||
def connect(self, central_address, le_create_connection_command):
|
||||
logger.debug(f'$$$ CONNECTION {central_address} -> {le_create_connection_command.peer_address}')
|
||||
logger.debug(
|
||||
f'$$$ CONNECTION {central_address} -> '
|
||||
f'{le_create_connection_command.peer_address}'
|
||||
)
|
||||
self.pending_connection = (central_address, le_create_connection_command)
|
||||
asyncio.get_running_loop().call_soon(self.on_connection_complete)
|
||||
|
||||
def on_disconnection_complete(self, central_address, peripheral_address, disconnect_command):
|
||||
def on_disconnection_complete(
|
||||
self, central_address, peripheral_address, disconnect_command
|
||||
):
|
||||
# Find the controller that initiated the disconnection
|
||||
if not (central_controller := self.find_controller(central_address)):
|
||||
logger.warning('!!! Initiating controller not found')
|
||||
@@ -127,16 +160,26 @@ class LocalLink:
|
||||
|
||||
# Disconnect from the first controller with a matching address
|
||||
if peripheral_controller := self.find_controller(peripheral_address):
|
||||
peripheral_controller.on_link_central_disconnected(central_address, disconnect_command.reason)
|
||||
peripheral_controller.on_link_central_disconnected(
|
||||
central_address, disconnect_command.reason
|
||||
)
|
||||
|
||||
central_controller.on_link_peripheral_disconnection_complete(disconnect_command, HCI_SUCCESS)
|
||||
central_controller.on_link_peripheral_disconnection_complete(
|
||||
disconnect_command, HCI_SUCCESS
|
||||
)
|
||||
|
||||
def disconnect(self, central_address, peripheral_address, disconnect_command):
|
||||
logger.debug(f'$$$ DISCONNECTION {central_address} -> {peripheral_address}: reason = {disconnect_command.reason}')
|
||||
logger.debug(
|
||||
f'$$$ DISCONNECTION {central_address} -> '
|
||||
f'{peripheral_address}: reason = {disconnect_command.reason}'
|
||||
)
|
||||
args = [central_address, peripheral_address, disconnect_command]
|
||||
asyncio.get_running_loop().call_soon(self.on_disconnection_complete, *args)
|
||||
|
||||
def on_connection_encrypted(self, central_address, peripheral_address, rand, ediv, ltk):
|
||||
# pylint: disable=too-many-arguments
|
||||
def on_connection_encrypted(
|
||||
self, central_address, peripheral_address, rand, ediv, ltk
|
||||
):
|
||||
logger.debug(f'*** ENCRYPTION {central_address} -> {peripheral_address}')
|
||||
|
||||
if central_controller := self.find_controller(central_address):
|
||||
@@ -145,6 +188,89 @@ class LocalLink:
|
||||
if peripheral_controller := self.find_controller(peripheral_address):
|
||||
peripheral_controller.on_link_encrypted(central_address, rand, ediv, ltk)
|
||||
|
||||
############################################################
|
||||
# Classic handlers
|
||||
############################################################
|
||||
|
||||
def classic_connect(self, initiator_controller, responder_address):
|
||||
logger.debug(
|
||||
f'[Classic] {initiator_controller.public_address} connects to {responder_address}'
|
||||
)
|
||||
responder_controller = self.find_classic_controller(responder_address)
|
||||
if responder_controller is None:
|
||||
initiator_controller.on_classic_connection_complete(
|
||||
responder_address, HCI_PAGE_TIMEOUT_ERROR
|
||||
)
|
||||
return
|
||||
self.pending_classic_connection = (initiator_controller, responder_controller)
|
||||
|
||||
responder_controller.on_classic_connection_request(
|
||||
initiator_controller.public_address,
|
||||
HCI_Connection_Complete_Event.ACL_LINK_TYPE,
|
||||
)
|
||||
|
||||
def classic_accept_connection(
|
||||
self, responder_controller, initiator_address, responder_role
|
||||
):
|
||||
logger.debug(
|
||||
f'[Classic] {responder_controller.public_address} accepts to connect {initiator_address}'
|
||||
)
|
||||
initiator_controller = self.find_classic_controller(initiator_address)
|
||||
if initiator_controller is None:
|
||||
responder_controller.on_classic_connection_complete(
|
||||
responder_controller.public_address, HCI_PAGE_TIMEOUT_ERROR
|
||||
)
|
||||
return
|
||||
|
||||
async def task():
|
||||
if responder_role != BT_PERIPHERAL_ROLE:
|
||||
initiator_controller.on_classic_role_change(
|
||||
responder_controller.public_address, int(not (responder_role))
|
||||
)
|
||||
initiator_controller.on_classic_connection_complete(
|
||||
responder_controller.public_address, HCI_SUCCESS
|
||||
)
|
||||
|
||||
asyncio.create_task(task())
|
||||
responder_controller.on_classic_role_change(
|
||||
initiator_controller.public_address, responder_role
|
||||
)
|
||||
responder_controller.on_classic_connection_complete(
|
||||
initiator_controller.public_address, HCI_SUCCESS
|
||||
)
|
||||
self.pending_classic_connection = None
|
||||
|
||||
def classic_disconnect(self, initiator_controller, responder_address, reason):
|
||||
logger.debug(
|
||||
f'[Classic] {initiator_controller.public_address} disconnects {responder_address}'
|
||||
)
|
||||
responder_controller = self.find_classic_controller(responder_address)
|
||||
|
||||
async def task():
|
||||
initiator_controller.on_classic_disconnected(responder_address, reason)
|
||||
|
||||
asyncio.create_task(task())
|
||||
responder_controller.on_classic_disconnected(
|
||||
initiator_controller.public_address, reason
|
||||
)
|
||||
|
||||
def classic_switch_role(
|
||||
self, initiator_controller, responder_address, initiator_new_role
|
||||
):
|
||||
responder_controller = self.find_classic_controller(responder_address)
|
||||
if responder_controller is None:
|
||||
return
|
||||
|
||||
async def task():
|
||||
initiator_controller.on_classic_role_change(
|
||||
responder_address, initiator_new_role
|
||||
)
|
||||
|
||||
asyncio.create_task(task())
|
||||
responder_controller.on_classic_role_change(
|
||||
initiator_controller.public_address, int(not (initiator_new_role))
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class RemoteLink:
|
||||
@@ -152,15 +278,18 @@ class RemoteLink:
|
||||
A Link implementation that communicates with other virtual controllers via a
|
||||
WebSocket relay
|
||||
'''
|
||||
|
||||
def __init__(self, uri):
|
||||
self.controller = None
|
||||
self.uri = uri
|
||||
self.execution_queue = asyncio.Queue()
|
||||
self.websocket = asyncio.get_running_loop().create_future()
|
||||
self.rpc_result = None
|
||||
self.pending_connection = None
|
||||
self.central_connections = set() # List of addresses that we have connected to
|
||||
self.peripheral_connections = set() # List of addresses that have connected to us
|
||||
self.controller = None
|
||||
self.uri = uri
|
||||
self.execution_queue = asyncio.Queue()
|
||||
self.websocket = asyncio.get_running_loop().create_future()
|
||||
self.rpc_result = None
|
||||
self.pending_connection = None
|
||||
self.central_connections = set() # List of addresses that we have connected to
|
||||
self.peripheral_connections = (
|
||||
set()
|
||||
) # List of addresses that have connected to us
|
||||
|
||||
# Connect and run asynchronously
|
||||
asyncio.create_task(self.run_connection())
|
||||
@@ -179,6 +308,9 @@ class RemoteLink:
|
||||
def get_pending_connection(self):
|
||||
return self.pending_connection
|
||||
|
||||
def get_pending_classic_connection(self):
|
||||
return self.pending_classic_connection
|
||||
|
||||
async def wait_until_connected(self):
|
||||
await self.websocket
|
||||
|
||||
@@ -192,11 +324,16 @@ class RemoteLink:
|
||||
try:
|
||||
await item
|
||||
except Exception as error:
|
||||
logger.warning(f'{color("!!! Exception in async handler:", "red")} {error}')
|
||||
logger.warning(
|
||||
f'{color("!!! Exception in async handler:", "red")} {error}'
|
||||
)
|
||||
|
||||
async def run_connection(self):
|
||||
import websockets # lazy import
|
||||
|
||||
# Connect to the relay
|
||||
logger.debug(f'connecting to {self.uri}')
|
||||
# pylint: disable-next=no-member
|
||||
websocket = await websockets.connect(self.uri)
|
||||
self.websocket.set_result(websocket)
|
||||
logger.debug(f'connected to {self.uri}')
|
||||
@@ -227,7 +364,9 @@ class RemoteLink:
|
||||
self.central_connections.remove(address)
|
||||
|
||||
if address in self.peripheral_connections:
|
||||
self.controller.on_link_central_disconnected(address, HCI_CONNECTION_TIMEOUT_ERROR)
|
||||
self.controller.on_link_central_disconnected(
|
||||
address, HCI_CONNECTION_TIMEOUT_ERROR
|
||||
)
|
||||
self.peripheral_connections.remove(address)
|
||||
|
||||
async def on_unreachable_received(self, target):
|
||||
@@ -244,7 +383,9 @@ class RemoteLink:
|
||||
|
||||
async def on_advertisement_message_received(self, sender, advertisement):
|
||||
try:
|
||||
self.controller.on_link_advertising_data(Address(sender), bytes.fromhex(advertisement))
|
||||
self.controller.on_link_advertising_data(
|
||||
Address(sender), bytes.fromhex(advertisement)
|
||||
)
|
||||
except Exception:
|
||||
logger.exception('exception')
|
||||
|
||||
@@ -263,11 +404,11 @@ class RemoteLink:
|
||||
self.controller.on_link_central_connected(Address(sender))
|
||||
|
||||
# Accept the connection by responding to it
|
||||
await self.send_targetted_message(sender, 'connected')
|
||||
await self.send_targeted_message(sender, 'connected')
|
||||
|
||||
async def on_connected_message_received(self, sender, _):
|
||||
if not self.pending_connection:
|
||||
logger.warn('received a connection ack, but no connection is pending')
|
||||
logger.warning('received a connection ack, but no connection is pending')
|
||||
return
|
||||
|
||||
# Remember the connection
|
||||
@@ -275,7 +416,9 @@ class RemoteLink:
|
||||
|
||||
# Notify the controller
|
||||
logger.debug(f'connected to peripheral {self.pending_connection.peer_address}')
|
||||
self.controller.on_link_peripheral_connection_complete(self.pending_connection, HCI_SUCCESS)
|
||||
self.controller.on_link_peripheral_connection_complete(
|
||||
self.pending_connection, HCI_SUCCESS
|
||||
)
|
||||
|
||||
async def on_disconnect_message_received(self, sender, message):
|
||||
# Notify the controller
|
||||
@@ -287,7 +430,7 @@ class RemoteLink:
|
||||
if sender in self.peripheral_connections:
|
||||
self.peripheral_connections.remove(sender)
|
||||
|
||||
async def on_encrypted_message_received(self, sender, message):
|
||||
async def on_encrypted_message_received(self, sender, _):
|
||||
# TODO parse params to get real args
|
||||
self.controller.on_link_encrypted(Address(sender), bytes(8), 0, bytes(16))
|
||||
|
||||
@@ -296,7 +439,7 @@ class RemoteLink:
|
||||
websocket = await self.websocket
|
||||
|
||||
# Create a future value to hold the eventual result
|
||||
assert(self.rpc_result is None)
|
||||
assert self.rpc_result is None
|
||||
self.rpc_result = asyncio.get_running_loop().create_future()
|
||||
|
||||
# Send the command
|
||||
@@ -309,7 +452,7 @@ class RemoteLink:
|
||||
|
||||
# TODO: parse the result
|
||||
|
||||
async def send_targetted_message(self, target, message):
|
||||
async def send_targeted_message(self, target, message):
|
||||
# Ensure we have a connection
|
||||
websocket = await self.websocket
|
||||
|
||||
@@ -326,35 +469,62 @@ class RemoteLink:
|
||||
self.execute(self.notify_address_changed)
|
||||
|
||||
async def send_advertising_data_to_relay(self, data):
|
||||
await self.send_targetted_message('*', f'advertisement:{data.hex()}')
|
||||
await self.send_targeted_message('*', f'advertisement:{data.hex()}')
|
||||
|
||||
def send_advertising_data(self, sender_address, data):
|
||||
def send_advertising_data(self, _, data):
|
||||
self.execute(partial(self.send_advertising_data_to_relay, data))
|
||||
|
||||
async def send_acl_data_to_relay(self, peer_address, data):
|
||||
await self.send_targetted_message(peer_address, f'acl:{data.hex()}')
|
||||
await self.send_targeted_message(peer_address, f'acl:{data.hex()}')
|
||||
|
||||
def send_acl_data(self, sender_address, peer_address, data):
|
||||
def send_acl_data(self, _, peer_address, _transport, data):
|
||||
# TODO: handle different transport
|
||||
self.execute(partial(self.send_acl_data_to_relay, peer_address, data))
|
||||
|
||||
async def send_connection_request_to_relay(self, peer_address):
|
||||
await self.send_targetted_message(peer_address, 'connect')
|
||||
await self.send_targeted_message(peer_address, 'connect')
|
||||
|
||||
def connect(self, central_address, le_create_connection_command):
|
||||
def connect(self, _, le_create_connection_command):
|
||||
if self.pending_connection:
|
||||
logger.warn('connection already pending')
|
||||
logger.warning('connection already pending')
|
||||
return
|
||||
self.pending_connection = le_create_connection_command
|
||||
self.execute(partial(self.send_connection_request_to_relay, str(le_create_connection_command.peer_address)))
|
||||
self.execute(
|
||||
partial(
|
||||
self.send_connection_request_to_relay,
|
||||
str(le_create_connection_command.peer_address),
|
||||
)
|
||||
)
|
||||
|
||||
def on_disconnection_complete(self, disconnect_command):
|
||||
self.controller.on_link_peripheral_disconnection_complete(disconnect_command, HCI_SUCCESS)
|
||||
self.controller.on_link_peripheral_disconnection_complete(
|
||||
disconnect_command, HCI_SUCCESS
|
||||
)
|
||||
|
||||
def disconnect(self, central_address, peripheral_address, disconnect_command):
|
||||
logger.debug(f'disconnect {central_address} -> {peripheral_address}: reason = {disconnect_command.reason}')
|
||||
self.execute(partial(self.send_targetted_message, peripheral_address, f'disconnect:reason={disconnect_command.reason}'))
|
||||
asyncio.get_running_loop().call_soon(self.on_disconnection_complete, disconnect_command)
|
||||
logger.debug(
|
||||
f'disconnect {central_address} -> '
|
||||
f'{peripheral_address}: reason = {disconnect_command.reason}'
|
||||
)
|
||||
self.execute(
|
||||
partial(
|
||||
self.send_targeted_message,
|
||||
peripheral_address,
|
||||
f'disconnect:reason={disconnect_command.reason}',
|
||||
)
|
||||
)
|
||||
asyncio.get_running_loop().call_soon(
|
||||
self.on_disconnection_complete, disconnect_command
|
||||
)
|
||||
|
||||
def on_connection_encrypted(self, central_address, peripheral_address, rand, ediv, ltk):
|
||||
asyncio.get_running_loop().call_soon(self.controller.on_link_encrypted, peripheral_address, rand, ediv, ltk)
|
||||
self.execute(partial(self.send_targetted_message, peripheral_address, f'encrypted:ltk={ltk.hex()}'))
|
||||
def on_connection_encrypted(self, _, peripheral_address, rand, ediv, ltk):
|
||||
asyncio.get_running_loop().call_soon(
|
||||
self.controller.on_link_encrypted, peripheral_address, rand, ediv, ltk
|
||||
)
|
||||
self.execute(
|
||||
partial(
|
||||
self.send_targeted_message,
|
||||
peripheral_address,
|
||||
f'encrypted:ltk={ltk.hex()}',
|
||||
)
|
||||
)
|
||||
|
||||
184
bumble/pairing.py
Normal file
184
bumble/pairing.py
Normal file
@@ -0,0 +1,184 @@
|
||||
# Copyright 2021-2023 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import enum
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from .hci import (
|
||||
HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
|
||||
HCI_DISPLAY_ONLY_IO_CAPABILITY,
|
||||
HCI_DISPLAY_YES_NO_IO_CAPABILITY,
|
||||
HCI_KEYBOARD_ONLY_IO_CAPABILITY,
|
||||
)
|
||||
from .smp import (
|
||||
SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
|
||||
SMP_KEYBOARD_ONLY_IO_CAPABILITY,
|
||||
SMP_DISPLAY_ONLY_IO_CAPABILITY,
|
||||
SMP_DISPLAY_YES_NO_IO_CAPABILITY,
|
||||
SMP_KEYBOARD_DISPLAY_IO_CAPABILITY,
|
||||
SMP_ENC_KEY_DISTRIBUTION_FLAG,
|
||||
SMP_ID_KEY_DISTRIBUTION_FLAG,
|
||||
SMP_SIGN_KEY_DISTRIBUTION_FLAG,
|
||||
SMP_LINK_KEY_DISTRIBUTION_FLAG,
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class PairingDelegate:
|
||||
"""Abstract base class for Pairing Delegates."""
|
||||
|
||||
# I/O Capabilities.
|
||||
# These are defined abstractly, and can be mapped to specific Classic pairing
|
||||
# and/or SMP constants.
|
||||
class IoCapability(enum.IntEnum):
|
||||
NO_OUTPUT_NO_INPUT = SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY
|
||||
KEYBOARD_INPUT_ONLY = SMP_KEYBOARD_ONLY_IO_CAPABILITY
|
||||
DISPLAY_OUTPUT_ONLY = SMP_DISPLAY_ONLY_IO_CAPABILITY
|
||||
DISPLAY_OUTPUT_AND_YES_NO_INPUT = SMP_DISPLAY_YES_NO_IO_CAPABILITY
|
||||
DISPLAY_OUTPUT_AND_KEYBOARD_INPUT = SMP_KEYBOARD_DISPLAY_IO_CAPABILITY
|
||||
|
||||
# Direct names for backward compatibility.
|
||||
NO_OUTPUT_NO_INPUT = IoCapability.NO_OUTPUT_NO_INPUT
|
||||
KEYBOARD_INPUT_ONLY = IoCapability.KEYBOARD_INPUT_ONLY
|
||||
DISPLAY_OUTPUT_ONLY = IoCapability.DISPLAY_OUTPUT_ONLY
|
||||
DISPLAY_OUTPUT_AND_YES_NO_INPUT = IoCapability.DISPLAY_OUTPUT_AND_YES_NO_INPUT
|
||||
DISPLAY_OUTPUT_AND_KEYBOARD_INPUT = IoCapability.DISPLAY_OUTPUT_AND_KEYBOARD_INPUT
|
||||
|
||||
# Key Distribution [LE only]
|
||||
class KeyDistribution(enum.IntFlag):
|
||||
DISTRIBUTE_ENCRYPTION_KEY = SMP_ENC_KEY_DISTRIBUTION_FLAG
|
||||
DISTRIBUTE_IDENTITY_KEY = SMP_ID_KEY_DISTRIBUTION_FLAG
|
||||
DISTRIBUTE_SIGNING_KEY = SMP_SIGN_KEY_DISTRIBUTION_FLAG
|
||||
DISTRIBUTE_LINK_KEY = SMP_LINK_KEY_DISTRIBUTION_FLAG
|
||||
|
||||
DEFAULT_KEY_DISTRIBUTION: int = (
|
||||
SMP_ENC_KEY_DISTRIBUTION_FLAG | SMP_ID_KEY_DISTRIBUTION_FLAG
|
||||
)
|
||||
|
||||
# Default mapping from abstract to Classic I/O capabilities.
|
||||
# Subclasses may override this if they prefer a different mapping.
|
||||
CLASSIC_IO_CAPABILITIES_MAP = {
|
||||
NO_OUTPUT_NO_INPUT: HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
|
||||
KEYBOARD_INPUT_ONLY: HCI_KEYBOARD_ONLY_IO_CAPABILITY,
|
||||
DISPLAY_OUTPUT_ONLY: HCI_DISPLAY_ONLY_IO_CAPABILITY,
|
||||
DISPLAY_OUTPUT_AND_YES_NO_INPUT: HCI_DISPLAY_YES_NO_IO_CAPABILITY,
|
||||
DISPLAY_OUTPUT_AND_KEYBOARD_INPUT: HCI_DISPLAY_YES_NO_IO_CAPABILITY,
|
||||
}
|
||||
|
||||
io_capability: IoCapability
|
||||
local_initiator_key_distribution: KeyDistribution
|
||||
local_responder_key_distribution: KeyDistribution
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
io_capability=NO_OUTPUT_NO_INPUT,
|
||||
local_initiator_key_distribution=DEFAULT_KEY_DISTRIBUTION,
|
||||
local_responder_key_distribution=DEFAULT_KEY_DISTRIBUTION,
|
||||
) -> None:
|
||||
self.io_capability = io_capability
|
||||
self.local_initiator_key_distribution = local_initiator_key_distribution
|
||||
self.local_responder_key_distribution = local_responder_key_distribution
|
||||
|
||||
@property
|
||||
def classic_io_capability(self) -> int:
|
||||
"""Map the abstract I/O capability to a Classic constant."""
|
||||
|
||||
# pylint: disable=line-too-long
|
||||
return self.CLASSIC_IO_CAPABILITIES_MAP.get(
|
||||
self.io_capability, HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY
|
||||
)
|
||||
|
||||
@property
|
||||
def smp_io_capability(self) -> int:
|
||||
"""Map the abstract I/O capability to an SMP constant."""
|
||||
|
||||
# This is just a 1-1 direct mapping
|
||||
return self.io_capability
|
||||
|
||||
async def accept(self) -> bool:
|
||||
"""Accept or reject a Pairing request."""
|
||||
return True
|
||||
|
||||
async def confirm(self) -> bool:
|
||||
"""Respond yes or no to a Pairing confirmation question."""
|
||||
return True
|
||||
|
||||
# pylint: disable-next=unused-argument
|
||||
async def compare_numbers(self, number: int, digits: int) -> bool:
|
||||
"""Compare two numbers."""
|
||||
return True
|
||||
|
||||
async def get_number(self) -> Optional[int]:
|
||||
"""
|
||||
Return an optional number as an answer to a passkey request.
|
||||
Returning `None` will result in a negative reply.
|
||||
"""
|
||||
return 0
|
||||
|
||||
async def get_string(self, max_length) -> Optional[str]:
|
||||
"""
|
||||
Return a string whose utf-8 encoding is up to max_length bytes.
|
||||
"""
|
||||
return None
|
||||
|
||||
# pylint: disable-next=unused-argument
|
||||
async def display_number(self, number: int, digits: int) -> None:
|
||||
"""Display a number."""
|
||||
|
||||
# [LE only]
|
||||
async def key_distribution_response(
|
||||
self, peer_initiator_key_distribution: int, peer_responder_key_distribution: int
|
||||
) -> Tuple[int, int]:
|
||||
"""
|
||||
Return the key distribution response in an SMP protocol context.
|
||||
|
||||
NOTE: since it is only used by the SMP protocol, this method's input and output
|
||||
are directly as integers, using the SMP constants, rather than the abstract
|
||||
KeyDistribution enums.
|
||||
"""
|
||||
return (
|
||||
int(
|
||||
peer_initiator_key_distribution & self.local_initiator_key_distribution
|
||||
),
|
||||
int(
|
||||
peer_responder_key_distribution & self.local_responder_key_distribution
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class PairingConfig:
|
||||
"""Configuration for the Pairing protocol."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
sc: bool = True,
|
||||
mitm: bool = True,
|
||||
bonding: bool = True,
|
||||
delegate: Optional[PairingDelegate] = None,
|
||||
) -> None:
|
||||
self.sc = sc
|
||||
self.mitm = mitm
|
||||
self.bonding = bonding
|
||||
self.delegate = delegate or PairingDelegate()
|
||||
|
||||
def __str__(self) -> str:
|
||||
return (
|
||||
f'PairingConfig(sc={self.sc}, '
|
||||
f'mitm={self.mitm}, bonding={self.bonding}, '
|
||||
f'delegate[{self.delegate.io_capability}])'
|
||||
)
|
||||
188
bumble/profiles/asha_service.py
Normal file
188
bumble/profiles/asha_service.py
Normal file
@@ -0,0 +1,188 @@
|
||||
# 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
|
||||
import logging
|
||||
from typing import List
|
||||
from ..core import AdvertisingData
|
||||
from ..device import Device, Connection
|
||||
from ..gatt import (
|
||||
GATT_ASHA_SERVICE,
|
||||
GATT_ASHA_READ_ONLY_PROPERTIES_CHARACTERISTIC,
|
||||
GATT_ASHA_AUDIO_CONTROL_POINT_CHARACTERISTIC,
|
||||
GATT_ASHA_AUDIO_STATUS_CHARACTERISTIC,
|
||||
GATT_ASHA_VOLUME_CHARACTERISTIC,
|
||||
GATT_ASHA_LE_PSM_OUT_CHARACTERISTIC,
|
||||
TemplateService,
|
||||
Characteristic,
|
||||
CharacteristicValue,
|
||||
)
|
||||
from ..utils import AsyncRunner
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class AshaService(TemplateService):
|
||||
UUID = GATT_ASHA_SERVICE
|
||||
OPCODE_START = 1
|
||||
OPCODE_STOP = 2
|
||||
OPCODE_STATUS = 3
|
||||
PROTOCOL_VERSION = 0x01
|
||||
RESERVED_FOR_FUTURE_USE = [00, 00]
|
||||
FEATURE_MAP = [0x01] # [LE CoC audio output streaming supported]
|
||||
SUPPORTED_CODEC_ID = [0x02, 0x01] # Codec IDs [G.722 at 16 kHz]
|
||||
RENDER_DELAY = [00, 00]
|
||||
|
||||
def __init__(self, capability: int, hisyncid: List[int], device: Device, psm=0):
|
||||
self.hisyncid = hisyncid
|
||||
self.capability = capability # Device Capabilities [Left, Monaural]
|
||||
self.device = device
|
||||
self.audio_out_data = b''
|
||||
self.psm = psm # a non-zero psm is mainly for testing purpose
|
||||
|
||||
# Handler for volume control
|
||||
def on_volume_write(connection, value):
|
||||
logger.info(f'--- VOLUME Write:{value[0]}')
|
||||
self.emit('volume', connection, value[0])
|
||||
|
||||
# Handler for audio control commands
|
||||
def on_audio_control_point_write(connection: Connection, value):
|
||||
logger.info(f'--- AUDIO CONTROL POINT Write:{value.hex()}')
|
||||
opcode = value[0]
|
||||
if opcode == AshaService.OPCODE_START:
|
||||
# Start
|
||||
audio_type = ('Unknown', 'Ringtone', 'Phone Call', 'Media')[value[2]]
|
||||
logger.info(
|
||||
f'### START: codec={value[1]}, '
|
||||
f'audio_type={audio_type}, '
|
||||
f'volume={value[3]}, '
|
||||
f'otherstate={value[4]}'
|
||||
)
|
||||
self.emit(
|
||||
'start',
|
||||
connection,
|
||||
{
|
||||
'codec': value[1],
|
||||
'audiotype': value[2],
|
||||
'volume': value[3],
|
||||
'otherstate': value[4],
|
||||
},
|
||||
)
|
||||
elif opcode == AshaService.OPCODE_STOP:
|
||||
logger.info('### STOP')
|
||||
self.emit('stop', connection)
|
||||
elif opcode == AshaService.OPCODE_STATUS:
|
||||
logger.info(f'### STATUS: connected={value[1]}')
|
||||
|
||||
# OPCODE_STATUS does not need audio status point update
|
||||
if opcode != AshaService.OPCODE_STATUS:
|
||||
AsyncRunner.spawn(
|
||||
device.notify_subscribers(
|
||||
self.audio_status_characteristic, force=True
|
||||
)
|
||||
)
|
||||
|
||||
self.read_only_properties_characteristic = Characteristic(
|
||||
GATT_ASHA_READ_ONLY_PROPERTIES_CHARACTERISTIC,
|
||||
Characteristic.Properties.READ,
|
||||
Characteristic.READABLE,
|
||||
bytes(
|
||||
[
|
||||
AshaService.PROTOCOL_VERSION, # Version
|
||||
self.capability,
|
||||
]
|
||||
)
|
||||
+ bytes(self.hisyncid)
|
||||
+ bytes(AshaService.FEATURE_MAP)
|
||||
+ bytes(AshaService.RENDER_DELAY)
|
||||
+ bytes(AshaService.RESERVED_FOR_FUTURE_USE)
|
||||
+ bytes(AshaService.SUPPORTED_CODEC_ID),
|
||||
)
|
||||
|
||||
self.audio_control_point_characteristic = Characteristic(
|
||||
GATT_ASHA_AUDIO_CONTROL_POINT_CHARACTERISTIC,
|
||||
Characteristic.Properties.WRITE
|
||||
| Characteristic.Properties.WRITE_WITHOUT_RESPONSE,
|
||||
Characteristic.WRITEABLE,
|
||||
CharacteristicValue(write=on_audio_control_point_write),
|
||||
)
|
||||
self.audio_status_characteristic = Characteristic(
|
||||
GATT_ASHA_AUDIO_STATUS_CHARACTERISTIC,
|
||||
Characteristic.Properties.READ | Characteristic.Properties.NOTIFY,
|
||||
Characteristic.READABLE,
|
||||
bytes([0]),
|
||||
)
|
||||
self.volume_characteristic = Characteristic(
|
||||
GATT_ASHA_VOLUME_CHARACTERISTIC,
|
||||
Characteristic.Properties.WRITE_WITHOUT_RESPONSE,
|
||||
Characteristic.WRITEABLE,
|
||||
CharacteristicValue(write=on_volume_write),
|
||||
)
|
||||
|
||||
# Register an L2CAP CoC server
|
||||
def on_coc(channel):
|
||||
def on_data(data):
|
||||
logging.debug(f'<<< data received:{data}')
|
||||
|
||||
self.emit('data', channel.connection, data)
|
||||
self.audio_out_data += data
|
||||
|
||||
channel.sink = on_data
|
||||
|
||||
# let the server find a free PSM
|
||||
self.psm = self.device.register_l2cap_channel_server(self.psm, on_coc, 8)
|
||||
self.le_psm_out_characteristic = Characteristic(
|
||||
GATT_ASHA_LE_PSM_OUT_CHARACTERISTIC,
|
||||
Characteristic.Properties.READ,
|
||||
Characteristic.READABLE,
|
||||
struct.pack('<H', self.psm),
|
||||
)
|
||||
|
||||
characteristics = [
|
||||
self.read_only_properties_characteristic,
|
||||
self.audio_control_point_characteristic,
|
||||
self.audio_status_characteristic,
|
||||
self.volume_characteristic,
|
||||
self.le_psm_out_characteristic,
|
||||
]
|
||||
|
||||
super().__init__(characteristics)
|
||||
|
||||
def get_advertising_data(self):
|
||||
# Advertisement only uses 4 least significant bytes of the HiSyncId.
|
||||
return bytes(
|
||||
AdvertisingData(
|
||||
[
|
||||
(
|
||||
AdvertisingData.SERVICE_DATA_16_BIT_UUID,
|
||||
bytes(GATT_ASHA_SERVICE)
|
||||
+ bytes(
|
||||
[
|
||||
AshaService.PROTOCOL_VERSION,
|
||||
self.capability,
|
||||
]
|
||||
)
|
||||
+ bytes(self.hisyncid[:4]),
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
@@ -23,7 +23,7 @@ from ..gatt import (
|
||||
TemplateService,
|
||||
Characteristic,
|
||||
CharacteristicValue,
|
||||
PackedCharacteristicAdapter
|
||||
PackedCharacteristicAdapter,
|
||||
)
|
||||
|
||||
|
||||
@@ -36,11 +36,11 @@ class BatteryService(TemplateService):
|
||||
self.battery_level_characteristic = PackedCharacteristicAdapter(
|
||||
Characteristic(
|
||||
GATT_BATTERY_LEVEL_CHARACTERISTIC,
|
||||
Characteristic.READ | Characteristic.NOTIFY,
|
||||
Characteristic.Properties.READ | Characteristic.Properties.NOTIFY,
|
||||
Characteristic.READABLE,
|
||||
CharacteristicValue(read=read_battery_level)
|
||||
CharacteristicValue(read=read_battery_level),
|
||||
),
|
||||
format=BatteryService.BATTERY_LEVEL_FORMAT
|
||||
pack_format=BatteryService.BATTERY_LEVEL_FORMAT,
|
||||
)
|
||||
super().__init__([self.battery_level_characteristic])
|
||||
|
||||
@@ -52,10 +52,11 @@ class BatteryServiceProxy(ProfileServiceProxy):
|
||||
def __init__(self, service_proxy):
|
||||
self.service_proxy = service_proxy
|
||||
|
||||
if characteristics := service_proxy.get_characteristics_by_uuid(GATT_BATTERY_LEVEL_CHARACTERISTIC):
|
||||
if characteristics := service_proxy.get_characteristics_by_uuid(
|
||||
GATT_BATTERY_LEVEL_CHARACTERISTIC
|
||||
):
|
||||
self.battery_level = PackedCharacteristicAdapter(
|
||||
characteristics[0],
|
||||
format=BatteryService.BATTERY_LEVEL_FORMAT
|
||||
characteristics[0], pack_format=BatteryService.BATTERY_LEVEL_FORMAT
|
||||
)
|
||||
else:
|
||||
self.battery_level = None
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import struct
|
||||
from typing import Tuple
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from ..gatt_client import ProfileServiceProxy
|
||||
from ..gatt import (
|
||||
@@ -33,7 +33,7 @@ from ..gatt import (
|
||||
TemplateService,
|
||||
Characteristic,
|
||||
DelegatedCharacteristicAdapter,
|
||||
UTF8CharacteristicAdapter
|
||||
UTF8CharacteristicAdapter,
|
||||
)
|
||||
|
||||
|
||||
@@ -52,49 +52,50 @@ class DeviceInformationService(TemplateService):
|
||||
|
||||
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
|
||||
manufacturer_name: Optional[str] = None,
|
||||
model_number: Optional[str] = None,
|
||||
serial_number: Optional[str] = None,
|
||||
hardware_revision: Optional[str] = None,
|
||||
firmware_revision: Optional[str] = None,
|
||||
software_revision: Optional[str] = None,
|
||||
system_id: Optional[Tuple[int, int]] = None, # (OUI, Manufacturer ID)
|
||||
ieee_regulatory_certification_data_list: Optional[bytes] = None
|
||||
# TODO: pnp_id
|
||||
):
|
||||
characteristics = [
|
||||
Characteristic(
|
||||
uuid,
|
||||
Characteristic.READ,
|
||||
Characteristic.READABLE,
|
||||
field
|
||||
uuid, Characteristic.Properties.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),
|
||||
(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)
|
||||
(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)
|
||||
))
|
||||
characteristics.append(
|
||||
Characteristic(
|
||||
GATT_SYSTEM_ID_CHARACTERISTIC,
|
||||
Characteristic.Properties.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
|
||||
))
|
||||
characteristics.append(
|
||||
Characteristic(
|
||||
GATT_REGULATORY_CERTIFICATION_DATA_LIST_CHARACTERISTIC,
|
||||
Characteristic.Properties.READ,
|
||||
Characteristic.READABLE,
|
||||
ieee_regulatory_certification_data_list,
|
||||
)
|
||||
)
|
||||
|
||||
super().__init__(characteristics)
|
||||
|
||||
@@ -108,11 +109,11 @@ class DeviceInformationServiceProxy(ProfileServiceProxy):
|
||||
|
||||
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),
|
||||
('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)
|
||||
('software_revision', GATT_SOFTWARE_REVISION_STRING_CHARACTERISTIC),
|
||||
):
|
||||
if characteristics := service_proxy.get_characteristics_by_uuid(uuid):
|
||||
characteristic = UTF8CharacteristicAdapter(characteristics[0])
|
||||
@@ -120,16 +121,20 @@ class DeviceInformationServiceProxy(ProfileServiceProxy):
|
||||
characteristic = None
|
||||
self.__setattr__(field, characteristic)
|
||||
|
||||
if characteristics := service_proxy.get_characteristics_by_uuid(GATT_SYSTEM_ID_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
|
||||
decode=DeviceInformationService.unpack_system_id,
|
||||
)
|
||||
else:
|
||||
self.system_id = None
|
||||
|
||||
if characteristics := service_proxy.get_characteristics_by_uuid(GATT_REGULATORY_CERTIFICATION_DATA_LIST_CHARACTERISTIC):
|
||||
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
|
||||
|
||||
@@ -30,25 +30,25 @@ from ..gatt import (
|
||||
Characteristic,
|
||||
CharacteristicValue,
|
||||
DelegatedCharacteristicAdapter,
|
||||
PackedCharacteristicAdapter
|
||||
PackedCharacteristicAdapter,
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class HeartRateService(TemplateService):
|
||||
UUID = GATT_HEART_RATE_SERVICE
|
||||
UUID = GATT_HEART_RATE_SERVICE
|
||||
HEART_RATE_CONTROL_POINT_FORMAT = 'B'
|
||||
CONTROL_POINT_NOT_SUPPORTED = 0x80
|
||||
RESET_ENERGY_EXPENDED = 0x01
|
||||
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
|
||||
OTHER = (0,)
|
||||
CHEST = (1,)
|
||||
WRIST = (2,)
|
||||
FINGER = (3,)
|
||||
HAND = (4,)
|
||||
EAR_LOBE = (5,)
|
||||
FOOT = 6
|
||||
|
||||
class HeartRateMeasurement:
|
||||
def __init__(
|
||||
@@ -56,12 +56,14 @@ class HeartRateService(TemplateService):
|
||||
heart_rate,
|
||||
sensor_contact_detected=None,
|
||||
energy_expended=None,
|
||||
rr_intervals=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):
|
||||
if energy_expended is not None and (
|
||||
energy_expended < 0 or energy_expended > 0xFFFF
|
||||
):
|
||||
raise ValueError('energy_expended out of range')
|
||||
|
||||
if rr_intervals:
|
||||
@@ -69,10 +71,10 @@ class HeartRateService(TemplateService):
|
||||
if rr_interval < 0 or rr_interval * 1024 > 0xFFFF:
|
||||
raise ValueError('rr_intervals out of range')
|
||||
|
||||
self.heart_rate = heart_rate
|
||||
self.heart_rate = heart_rate
|
||||
self.sensor_contact_detected = sensor_contact_detected
|
||||
self.energy_expended = energy_expended
|
||||
self.rr_intervals = rr_intervals
|
||||
self.energy_expended = energy_expended
|
||||
self.rr_intervals = rr_intervals
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data):
|
||||
@@ -87,7 +89,7 @@ class HeartRateService(TemplateService):
|
||||
offset += 1
|
||||
|
||||
if flags & (1 << 2):
|
||||
sensor_contact_detected = (flags & (1 << 1) != 0)
|
||||
sensor_contact_detected = flags & (1 << 1) != 0
|
||||
else:
|
||||
sensor_contact_detected = None
|
||||
|
||||
@@ -119,51 +121,57 @@ class HeartRateService(TemplateService):
|
||||
flags |= ((1 if self.sensor_contact_detected else 0) << 1) | (1 << 2)
|
||||
|
||||
if self.energy_expended is not None:
|
||||
flags |= (1 << 3)
|
||||
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
|
||||
])
|
||||
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},'\
|
||||
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
|
||||
reset_energy_expended=None,
|
||||
):
|
||||
self.heart_rate_measurement_characteristic = DelegatedCharacteristicAdapter(
|
||||
Characteristic(
|
||||
GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC,
|
||||
Characteristic.NOTIFY,
|
||||
Characteristic.Properties.NOTIFY,
|
||||
0,
|
||||
CharacteristicValue(read=read_heart_rate_measurement)
|
||||
CharacteristicValue(read=read_heart_rate_measurement),
|
||||
),
|
||||
encode=lambda value: bytes(value)
|
||||
# pylint: disable=unnecessary-lambda
|
||||
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.Properties.READ,
|
||||
Characteristic.READABLE,
|
||||
bytes([int(body_sensor_location)])
|
||||
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:
|
||||
@@ -174,11 +182,11 @@ class HeartRateService(TemplateService):
|
||||
self.heart_rate_control_point_characteristic = PackedCharacteristicAdapter(
|
||||
Characteristic(
|
||||
GATT_HEART_RATE_CONTROL_POINT_CHARACTERISTIC,
|
||||
Characteristic.WRITE,
|
||||
Characteristic.Properties.WRITE,
|
||||
Characteristic.WRITEABLE,
|
||||
CharacteristicValue(write=write_heart_rate_control_point_value)
|
||||
CharacteristicValue(write=write_heart_rate_control_point_value),
|
||||
),
|
||||
format=HeartRateService.HEART_RATE_CONTROL_POINT_FORMAT
|
||||
pack_format=HeartRateService.HEART_RATE_CONTROL_POINT_FORMAT,
|
||||
)
|
||||
characteristics.append(self.heart_rate_control_point_characteristic)
|
||||
|
||||
@@ -192,30 +200,38 @@ class HeartRateServiceProxy(ProfileServiceProxy):
|
||||
def __init__(self, service_proxy):
|
||||
self.service_proxy = service_proxy
|
||||
|
||||
if characteristics := service_proxy.get_characteristics_by_uuid(GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC):
|
||||
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
|
||||
decode=HeartRateService.HeartRateMeasurement.from_bytes,
|
||||
)
|
||||
else:
|
||||
self.heart_rate_measurement = None
|
||||
|
||||
if characteristics := service_proxy.get_characteristics_by_uuid(GATT_BODY_SENSOR_LOCATION_CHARACTERISTIC):
|
||||
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])
|
||||
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):
|
||||
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
|
||||
pack_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)
|
||||
return await self.heart_rate_control_point.write_value(
|
||||
HeartRateService.RESET_ENERGY_EXPENDED
|
||||
)
|
||||
|
||||
0
bumble/profiles/py.typed
Normal file
0
bumble/profiles/py.typed
Normal file
0
bumble/py.typed
Normal file
0
bumble/py.typed
Normal file
570
bumble/rfcomm.py
570
bumble/rfcomm.py
@@ -18,10 +18,11 @@
|
||||
import logging
|
||||
import asyncio
|
||||
|
||||
from colors import color
|
||||
from pyee import EventEmitter
|
||||
|
||||
from .core import InvalidStateError, ProtocolError, ConnectionError
|
||||
from . import core
|
||||
from .colors import color
|
||||
from .core import BT_BR_EDR_TRANSPORT, InvalidStateError, ProtocolError
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -32,6 +33,8 @@ logger = logging.getLogger(__name__)
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
# fmt: off
|
||||
|
||||
RFCOMM_PSM = 0x0003
|
||||
|
||||
|
||||
@@ -98,22 +101,24 @@ RFCOMM_DEFAULT_PREFERRED_MTU = 1280
|
||||
RFCOMM_DYNAMIC_CHANNEL_NUMBER_START = 1
|
||||
RFCOMM_DYNAMIC_CHANNEL_NUMBER_END = 30
|
||||
|
||||
# fmt: on
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def fcs(buffer):
|
||||
fcs = 0xFF
|
||||
def compute_fcs(buffer):
|
||||
result = 0xFF
|
||||
for byte in buffer:
|
||||
fcs = CRC_TABLE[fcs ^ byte]
|
||||
return 0xFF - fcs
|
||||
result = CRC_TABLE[result ^ byte]
|
||||
return 0xFF - result
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class RFCOMM_Frame:
|
||||
def __init__(self, type, c_r, dlci, p_f, information = b'', with_credits = False):
|
||||
self.type = type
|
||||
self.c_r = c_r
|
||||
self.dlci = dlci
|
||||
self.p_f = p_f
|
||||
def __init__(self, frame_type, c_r, dlci, p_f, information=b'', with_credits=False):
|
||||
self.type = frame_type
|
||||
self.c_r = c_r
|
||||
self.dlci = dlci
|
||||
self.p_f = p_f
|
||||
self.information = information
|
||||
length = len(information)
|
||||
if with_credits:
|
||||
@@ -124,19 +129,19 @@ class RFCOMM_Frame:
|
||||
else:
|
||||
# 1-byte length indicator
|
||||
self.length = bytes([(length << 1) | 1])
|
||||
self.address = (dlci << 2) | (c_r << 1) | 1
|
||||
self.control = type | (p_f << 4)
|
||||
if type == RFCOMM_UIH_FRAME:
|
||||
self.fcs = fcs(bytes([self.address, self.control]))
|
||||
self.address = (dlci << 2) | (c_r << 1) | 1
|
||||
self.control = frame_type | (p_f << 4)
|
||||
if frame_type == RFCOMM_UIH_FRAME:
|
||||
self.fcs = compute_fcs(bytes([self.address, self.control]))
|
||||
else:
|
||||
self.fcs = fcs(bytes([self.address, self.control]) + self.length)
|
||||
self.fcs = compute_fcs(bytes([self.address, self.control]) + self.length)
|
||||
|
||||
def type_name(self):
|
||||
return RFCOMM_FRAME_TYPE_NAMES[self.type]
|
||||
|
||||
@staticmethod
|
||||
def parse_mcc(data):
|
||||
type = data[0] >> 2
|
||||
mcc_type = data[0] >> 2
|
||||
c_r = (data[0] >> 1) & 1
|
||||
length = data[1]
|
||||
if data[1] & 1:
|
||||
@@ -144,13 +149,16 @@ class RFCOMM_Frame:
|
||||
value = data[2:]
|
||||
else:
|
||||
length = (data[3] << 7) & (length >> 1)
|
||||
value = data[3:3 + length]
|
||||
value = data[3 : 3 + length]
|
||||
|
||||
return (type, c_r, value)
|
||||
return (mcc_type, c_r, value)
|
||||
|
||||
@staticmethod
|
||||
def make_mcc(type, c_r, data):
|
||||
return bytes([(type << 2 | c_r << 1 | 1) & 0xFF, (len(data) & 0x7F) << 1 | 1]) + data
|
||||
def make_mcc(mcc_type, c_r, data):
|
||||
return (
|
||||
bytes([(mcc_type << 2 | c_r << 1 | 1) & 0xFF, (len(data) & 0x7F) << 1 | 1])
|
||||
+ data
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def sabm(c_r, dlci):
|
||||
@@ -169,15 +177,17 @@ class RFCOMM_Frame:
|
||||
return RFCOMM_Frame(RFCOMM_DISC_FRAME, c_r, dlci, 1)
|
||||
|
||||
@staticmethod
|
||||
def uih(c_r, dlci, information, p_f = 0):
|
||||
return RFCOMM_Frame(RFCOMM_UIH_FRAME, c_r, dlci, p_f, information, with_credits = (p_f == 1))
|
||||
def uih(c_r, dlci, information, p_f=0):
|
||||
return RFCOMM_Frame(
|
||||
RFCOMM_UIH_FRAME, c_r, dlci, p_f, information, with_credits=(p_f == 1)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(data):
|
||||
# Extract fields
|
||||
dlci = (data[0] >> 2) & 0x3F
|
||||
c_r = (data[0] >> 1) & 0x01
|
||||
type = data[1] & 0xEF
|
||||
frame_type = data[1] & 0xEF
|
||||
p_f = (data[1] >> 4) & 0x01
|
||||
length = data[2]
|
||||
if length & 0x01:
|
||||
@@ -189,132 +199,182 @@ class RFCOMM_Frame:
|
||||
fcs = data[-1]
|
||||
|
||||
# Construct the frame and check the CRC
|
||||
frame = RFCOMM_Frame(type, c_r, dlci, p_f, information)
|
||||
frame = RFCOMM_Frame(frame_type, c_r, dlci, p_f, information)
|
||||
if frame.fcs != fcs:
|
||||
logger.warn(f'FCS mismatch: got {fcs:02X}, expected {frame.fcs:02X}')
|
||||
logger.warning(f'FCS mismatch: got {fcs:02X}, expected {frame.fcs:02X}')
|
||||
raise ValueError('fcs mismatch')
|
||||
|
||||
return frame
|
||||
|
||||
def __bytes__(self):
|
||||
return bytes([self.address, self.control]) + self.length + self.information + bytes([self.fcs])
|
||||
return (
|
||||
bytes([self.address, self.control])
|
||||
+ self.length
|
||||
+ self.information
|
||||
+ bytes([self.fcs])
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f'{color(self.type_name(), "yellow")}(c/r={self.c_r},dlci={self.dlci},p/f={self.p_f},length={len(self.information)},fcs=0x{self.fcs:02X})'
|
||||
return (
|
||||
f'{color(self.type_name(), "yellow")}'
|
||||
f'(c/r={self.c_r},'
|
||||
f'dlci={self.dlci},'
|
||||
f'p/f={self.p_f},'
|
||||
f'length={len(self.information)},'
|
||||
f'fcs=0x{self.fcs:02X})'
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class RFCOMM_MCC_PN:
|
||||
def __init__(self, dlci, cl, priority, ack_timer, max_frame_size, max_retransmissions, window_size):
|
||||
self.dlci = dlci
|
||||
self.cl = cl
|
||||
self.priority = priority
|
||||
self.ack_timer = ack_timer
|
||||
self.max_frame_size = max_frame_size
|
||||
def __init__(
|
||||
self,
|
||||
dlci,
|
||||
cl,
|
||||
priority,
|
||||
ack_timer,
|
||||
max_frame_size,
|
||||
max_retransmissions,
|
||||
window_size,
|
||||
):
|
||||
self.dlci = dlci
|
||||
self.cl = cl
|
||||
self.priority = priority
|
||||
self.ack_timer = ack_timer
|
||||
self.max_frame_size = max_frame_size
|
||||
self.max_retransmissions = max_retransmissions
|
||||
self.window_size = window_size
|
||||
self.window_size = window_size
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(data):
|
||||
return RFCOMM_MCC_PN(
|
||||
dlci = data[0],
|
||||
cl = data[1],
|
||||
priority = data[2],
|
||||
ack_timer = data[3],
|
||||
max_frame_size = data[4] | data[5] << 8,
|
||||
max_retransmissions = data[6],
|
||||
window_size = data[7]
|
||||
dlci=data[0],
|
||||
cl=data[1],
|
||||
priority=data[2],
|
||||
ack_timer=data[3],
|
||||
max_frame_size=data[4] | data[5] << 8,
|
||||
max_retransmissions=data[6],
|
||||
window_size=data[7],
|
||||
)
|
||||
|
||||
def __bytes__(self):
|
||||
return bytes([
|
||||
self.dlci & 0xFF,
|
||||
self.cl & 0xFF,
|
||||
self.priority & 0xFF,
|
||||
self.ack_timer & 0xFF,
|
||||
self.max_frame_size & 0xFF,
|
||||
(self.max_frame_size >> 8) & 0xFF,
|
||||
self.max_retransmissions & 0xFF,
|
||||
self.window_size & 0xFF
|
||||
])
|
||||
return bytes(
|
||||
[
|
||||
self.dlci & 0xFF,
|
||||
self.cl & 0xFF,
|
||||
self.priority & 0xFF,
|
||||
self.ack_timer & 0xFF,
|
||||
self.max_frame_size & 0xFF,
|
||||
(self.max_frame_size >> 8) & 0xFF,
|
||||
self.max_retransmissions & 0xFF,
|
||||
self.window_size & 0xFF,
|
||||
]
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f'PN(dlci={self.dlci},cl={self.cl},priority={self.priority},ack_timer={self.ack_timer},max_frame_size={self.max_frame_size},max_retransmissions={self.max_retransmissions},window_size={self.window_size})'
|
||||
return (
|
||||
f'PN(dlci={self.dlci},'
|
||||
f'cl={self.cl},'
|
||||
f'priority={self.priority},'
|
||||
f'ack_timer={self.ack_timer},'
|
||||
f'max_frame_size={self.max_frame_size},'
|
||||
f'max_retransmissions={self.max_retransmissions},'
|
||||
f'window_size={self.window_size})'
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class RFCOMM_MCC_MSC:
|
||||
def __init__(self, dlci, fc, rtc, rtr, ic, dv):
|
||||
self.dlci = dlci
|
||||
self.fc = fc
|
||||
self.rtc = rtc
|
||||
self.rtr = rtr
|
||||
self.ic = ic
|
||||
self.dv = dv
|
||||
self.fc = fc
|
||||
self.rtc = rtc
|
||||
self.rtr = rtr
|
||||
self.ic = ic
|
||||
self.dv = dv
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(data):
|
||||
return RFCOMM_MCC_MSC(
|
||||
dlci = data[0] >> 2,
|
||||
fc = data[1] >> 1 & 1,
|
||||
rtc = data[1] >> 2 & 1,
|
||||
rtr = data[1] >> 3 & 1,
|
||||
ic = data[1] >> 6 & 1,
|
||||
dv = data[1] >> 7 & 1
|
||||
dlci=data[0] >> 2,
|
||||
fc=data[1] >> 1 & 1,
|
||||
rtc=data[1] >> 2 & 1,
|
||||
rtr=data[1] >> 3 & 1,
|
||||
ic=data[1] >> 6 & 1,
|
||||
dv=data[1] >> 7 & 1,
|
||||
)
|
||||
|
||||
def __bytes__(self):
|
||||
return bytes([
|
||||
(self.dlci << 2) | 3,
|
||||
1 | self.fc << 1 | self.rtc << 2 | self.rtr << 3 | self.ic << 6 | self.dv << 7
|
||||
])
|
||||
return bytes(
|
||||
[
|
||||
(self.dlci << 2) | 3,
|
||||
1
|
||||
| self.fc << 1
|
||||
| self.rtc << 2
|
||||
| self.rtr << 3
|
||||
| self.ic << 6
|
||||
| self.dv << 7,
|
||||
]
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f'MSC(dlci={self.dlci},fc={self.fc},rtc={self.rtc},rtr={self.rtr},ic={self.ic},dv={self.dv})'
|
||||
return (
|
||||
f'MSC(dlci={self.dlci},'
|
||||
f'fc={self.fc},'
|
||||
f'rtc={self.rtc},'
|
||||
f'rtr={self.rtr},'
|
||||
f'ic={self.ic},'
|
||||
f'dv={self.dv})'
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class DLC(EventEmitter):
|
||||
# States
|
||||
INIT = 0x00
|
||||
CONNECTING = 0x01
|
||||
CONNECTED = 0x02
|
||||
INIT = 0x00
|
||||
CONNECTING = 0x01
|
||||
CONNECTED = 0x02
|
||||
DISCONNECTING = 0x03
|
||||
DISCONNECTED = 0x04
|
||||
RESET = 0x05
|
||||
DISCONNECTED = 0x04
|
||||
RESET = 0x05
|
||||
|
||||
STATE_NAMES = {
|
||||
INIT: 'INIT',
|
||||
CONNECTING: 'CONNECTING',
|
||||
CONNECTED: 'CONNECTED',
|
||||
INIT: 'INIT',
|
||||
CONNECTING: 'CONNECTING',
|
||||
CONNECTED: 'CONNECTED',
|
||||
DISCONNECTING: 'DISCONNECTING',
|
||||
DISCONNECTED: 'DISCONNECTED',
|
||||
RESET: 'RESET'
|
||||
DISCONNECTED: 'DISCONNECTED',
|
||||
RESET: 'RESET',
|
||||
}
|
||||
|
||||
def __init__(self, multiplexer, dlci, max_frame_size, initial_tx_credits):
|
||||
super().__init__()
|
||||
self.multiplexer = multiplexer
|
||||
self.dlci = dlci
|
||||
self.rx_credits = RFCOMM_DEFAULT_INITIAL_RX_CREDITS
|
||||
self.multiplexer = multiplexer
|
||||
self.dlci = dlci
|
||||
self.rx_credits = RFCOMM_DEFAULT_INITIAL_RX_CREDITS
|
||||
self.rx_threshold = self.rx_credits // 2
|
||||
self.tx_credits = initial_tx_credits
|
||||
self.tx_buffer = b''
|
||||
self.state = DLC.INIT
|
||||
self.role = multiplexer.role
|
||||
self.c_r = 1 if self.role == Multiplexer.INITIATOR else 0
|
||||
self.sink = None
|
||||
self.tx_credits = initial_tx_credits
|
||||
self.tx_buffer = b''
|
||||
self.state = DLC.INIT
|
||||
self.role = multiplexer.role
|
||||
self.c_r = 1 if self.role == Multiplexer.INITIATOR else 0
|
||||
self.sink = None
|
||||
self.connection_result = None
|
||||
|
||||
# Compute the MTU
|
||||
max_overhead = 4 + 1 # header with 2-byte length + fcs
|
||||
self.mtu = min(max_frame_size, self.multiplexer.l2cap_channel.mtu - max_overhead)
|
||||
self.mtu = min(
|
||||
max_frame_size, self.multiplexer.l2cap_channel.mtu - max_overhead
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def state_name(state):
|
||||
return DLC.STATE_NAMES[state]
|
||||
|
||||
def change_state(self, new_state):
|
||||
logger.debug(f'{self} state change -> {color(self.state_name(new_state), "magenta")}')
|
||||
logger.debug(
|
||||
f'{self} state change -> {color(self.state_name(new_state), "magenta")}'
|
||||
)
|
||||
self.state = new_state
|
||||
|
||||
def send_frame(self, frame):
|
||||
@@ -324,58 +384,40 @@ class DLC(EventEmitter):
|
||||
handler = getattr(self, f'on_{frame.type_name()}_frame'.lower())
|
||||
handler(frame)
|
||||
|
||||
def on_sabm_frame(self, frame):
|
||||
def on_sabm_frame(self, _frame):
|
||||
if self.state != DLC.CONNECTING:
|
||||
logger.warn(color('!!! received SABM when not in CONNECTING state', 'red'))
|
||||
logger.warning(
|
||||
color('!!! received SABM when not in CONNECTING state', 'red')
|
||||
)
|
||||
return
|
||||
|
||||
self.send_frame(RFCOMM_Frame.ua(c_r = 1 - self.c_r, dlci = self.dlci))
|
||||
self.send_frame(RFCOMM_Frame.ua(c_r=1 - self.c_r, dlci=self.dlci))
|
||||
|
||||
# Exchange the modem status with the peer
|
||||
msc = RFCOMM_MCC_MSC(
|
||||
dlci = self.dlci,
|
||||
fc = 0,
|
||||
rtc = 1,
|
||||
rtr = 1,
|
||||
ic = 0,
|
||||
dv = 1
|
||||
msc = RFCOMM_MCC_MSC(dlci=self.dlci, fc=0, rtc=1, rtr=1, ic=0, dv=1)
|
||||
mcc = RFCOMM_Frame.make_mcc(
|
||||
mcc_type=RFCOMM_MCC_MSC_TYPE, c_r=1, data=bytes(msc)
|
||||
)
|
||||
mcc = RFCOMM_Frame.make_mcc(type = RFCOMM_MCC_MSC_TYPE, c_r = 1, data = bytes(msc))
|
||||
logger.debug(f'>>> MCC MSC Command: {msc}')
|
||||
self.send_frame(
|
||||
RFCOMM_Frame.uih(
|
||||
c_r = self.c_r,
|
||||
dlci = 0,
|
||||
information = mcc
|
||||
)
|
||||
)
|
||||
self.send_frame(RFCOMM_Frame.uih(c_r=self.c_r, dlci=0, information=mcc))
|
||||
|
||||
self.change_state(DLC.CONNECTED)
|
||||
self.emit('open')
|
||||
|
||||
def on_ua_frame(self, frame):
|
||||
def on_ua_frame(self, _frame):
|
||||
if self.state != DLC.CONNECTING:
|
||||
logger.warn(color('!!! received SABM when not in CONNECTING state', 'red'))
|
||||
logger.warning(
|
||||
color('!!! received SABM when not in CONNECTING state', 'red')
|
||||
)
|
||||
return
|
||||
|
||||
# Exchange the modem status with the peer
|
||||
msc = RFCOMM_MCC_MSC(
|
||||
dlci = self.dlci,
|
||||
fc = 0,
|
||||
rtc = 1,
|
||||
rtr = 1,
|
||||
ic = 0,
|
||||
dv = 1
|
||||
msc = RFCOMM_MCC_MSC(dlci=self.dlci, fc=0, rtc=1, rtr=1, ic=0, dv=1)
|
||||
mcc = RFCOMM_Frame.make_mcc(
|
||||
mcc_type=RFCOMM_MCC_MSC_TYPE, c_r=1, data=bytes(msc)
|
||||
)
|
||||
mcc = RFCOMM_Frame.make_mcc(type = RFCOMM_MCC_MSC_TYPE, c_r = 1, data = bytes(msc))
|
||||
logger.debug(f'>>> MCC MSC Command: {msc}')
|
||||
self.send_frame(
|
||||
RFCOMM_Frame.uih(
|
||||
c_r = self.c_r,
|
||||
dlci = 0,
|
||||
information = mcc
|
||||
)
|
||||
)
|
||||
self.send_frame(RFCOMM_Frame.uih(c_r=self.c_r, dlci=0, information=mcc))
|
||||
|
||||
self.change_state(DLC.CONNECTED)
|
||||
self.multiplexer.on_dlc_open_complete(self)
|
||||
@@ -384,29 +426,36 @@ class DLC(EventEmitter):
|
||||
# TODO: handle all states
|
||||
pass
|
||||
|
||||
def on_disc_frame(self, frame):
|
||||
def on_disc_frame(self, _frame):
|
||||
# TODO: handle all states
|
||||
self.send_frame(RFCOMM_Frame.ua(c_r = 1 - self.c_r, dlci = self.dlci))
|
||||
self.send_frame(RFCOMM_Frame.ua(c_r=1 - self.c_r, dlci=self.dlci))
|
||||
|
||||
def on_uih_frame(self, frame):
|
||||
data = frame.information
|
||||
if frame.p_f == 1:
|
||||
# With credits
|
||||
credits = frame.information[0]
|
||||
self.tx_credits += credits
|
||||
received_credits = frame.information[0]
|
||||
self.tx_credits += received_credits
|
||||
|
||||
logger.debug(f'<<< Credits [{self.dlci}]: received {credits}, total={self.tx_credits}')
|
||||
logger.debug(
|
||||
f'<<< Credits [{self.dlci}]: '
|
||||
f'received {received_credits}, total={self.tx_credits}'
|
||||
)
|
||||
data = data[1:]
|
||||
|
||||
logger.debug(f'{color("<<< Data", "yellow")} [{self.dlci}] {len(data)} bytes, rx_credits={self.rx_credits}: {data.hex()}')
|
||||
logger.debug(
|
||||
f'{color("<<< Data", "yellow")} '
|
||||
f'[{self.dlci}] {len(data)} bytes, '
|
||||
f'rx_credits={self.rx_credits}: {data.hex()}'
|
||||
)
|
||||
if len(data) and self.sink:
|
||||
self.sink(data)
|
||||
self.sink(data) # pylint: disable=not-callable
|
||||
|
||||
# Update the credits
|
||||
if self.rx_credits > 0:
|
||||
self.rx_credits -= 1
|
||||
else:
|
||||
logger.warn(color('!!! received frame with no rx credits', 'red'))
|
||||
logger.warning(color('!!! received frame with no rx credits', 'red'))
|
||||
|
||||
# Check if there's anything to send (including credits)
|
||||
self.process_tx()
|
||||
@@ -418,69 +467,47 @@ class DLC(EventEmitter):
|
||||
if c_r:
|
||||
# Command
|
||||
logger.debug(f'<<< MCC MSC Command: {msc}')
|
||||
msc = RFCOMM_MCC_MSC(
|
||||
dlci = self.dlci,
|
||||
fc = 0,
|
||||
rtc = 1,
|
||||
rtr = 1,
|
||||
ic = 0,
|
||||
dv = 1
|
||||
msc = RFCOMM_MCC_MSC(dlci=self.dlci, fc=0, rtc=1, rtr=1, ic=0, dv=1)
|
||||
mcc = RFCOMM_Frame.make_mcc(
|
||||
mcc_type=RFCOMM_MCC_MSC_TYPE, c_r=0, data=bytes(msc)
|
||||
)
|
||||
mcc = RFCOMM_Frame.make_mcc(type = RFCOMM_MCC_MSC_TYPE, c_r = 0, data = bytes(msc))
|
||||
logger.debug(f'>>> MCC MSC Response: {msc}')
|
||||
self.send_frame(
|
||||
RFCOMM_Frame.uih(
|
||||
c_r = self.c_r,
|
||||
dlci = 0,
|
||||
information = mcc
|
||||
)
|
||||
)
|
||||
self.send_frame(RFCOMM_Frame.uih(c_r=self.c_r, dlci=0, information=mcc))
|
||||
else:
|
||||
# Response
|
||||
logger.debug(f'<<< MCC MSC Response: {msc}')
|
||||
|
||||
def connect(self):
|
||||
if not self.state == DLC.INIT:
|
||||
if self.state != DLC.INIT:
|
||||
raise InvalidStateError('invalid state')
|
||||
|
||||
self.change_state(DLC.CONNECTING)
|
||||
self.connection_result = asyncio.get_running_loop().create_future()
|
||||
self.send_frame(
|
||||
RFCOMM_Frame.sabm(
|
||||
c_r = self.c_r,
|
||||
dlci = self.dlci
|
||||
)
|
||||
)
|
||||
self.send_frame(RFCOMM_Frame.sabm(c_r=self.c_r, dlci=self.dlci))
|
||||
|
||||
def accept(self):
|
||||
if not self.state == DLC.INIT:
|
||||
if self.state != DLC.INIT:
|
||||
raise InvalidStateError('invalid state')
|
||||
|
||||
pn = RFCOMM_MCC_PN(
|
||||
dlci = self.dlci,
|
||||
cl = 0xE0,
|
||||
priority = 7,
|
||||
ack_timer = 0,
|
||||
max_frame_size = RFCOMM_DEFAULT_PREFERRED_MTU,
|
||||
max_retransmissions = 0,
|
||||
window_size = RFCOMM_DEFAULT_INITIAL_RX_CREDITS
|
||||
dlci=self.dlci,
|
||||
cl=0xE0,
|
||||
priority=7,
|
||||
ack_timer=0,
|
||||
max_frame_size=RFCOMM_DEFAULT_PREFERRED_MTU,
|
||||
max_retransmissions=0,
|
||||
window_size=RFCOMM_DEFAULT_INITIAL_RX_CREDITS,
|
||||
)
|
||||
mcc = RFCOMM_Frame.make_mcc(type = RFCOMM_MCC_PN_TYPE, c_r = 0, data = bytes(pn))
|
||||
mcc = RFCOMM_Frame.make_mcc(mcc_type=RFCOMM_MCC_PN_TYPE, c_r=0, data=bytes(pn))
|
||||
logger.debug(f'>>> PN Response: {pn}')
|
||||
self.send_frame(
|
||||
RFCOMM_Frame.uih(
|
||||
c_r = self.c_r,
|
||||
dlci = 0,
|
||||
information = mcc
|
||||
)
|
||||
)
|
||||
self.send_frame(RFCOMM_Frame.uih(c_r=self.c_r, dlci=0, information=mcc))
|
||||
self.change_state(DLC.CONNECTING)
|
||||
|
||||
def rx_credits_needed(self):
|
||||
if self.rx_credits <= self.rx_threshold:
|
||||
return RFCOMM_DEFAULT_INITIAL_RX_CREDITS - self.rx_credits
|
||||
else:
|
||||
return 0
|
||||
|
||||
return 0
|
||||
|
||||
def process_tx(self):
|
||||
# Send anything we can (or an empty frame if we need to send rx credits)
|
||||
@@ -488,13 +515,13 @@ class DLC(EventEmitter):
|
||||
while (self.tx_buffer and self.tx_credits > 0) or rx_credits_needed > 0:
|
||||
# Get the next chunk, up to MTU size
|
||||
if rx_credits_needed > 0:
|
||||
chunk = bytes([rx_credits_needed]) + self.tx_buffer[:self.mtu - 1]
|
||||
self.tx_buffer = self.tx_buffer[len(chunk) - 1:]
|
||||
chunk = bytes([rx_credits_needed]) + self.tx_buffer[: self.mtu - 1]
|
||||
self.tx_buffer = self.tx_buffer[len(chunk) - 1 :]
|
||||
self.rx_credits += rx_credits_needed
|
||||
tx_credit_spent = (len(chunk) > 1)
|
||||
tx_credit_spent = len(chunk) > 1
|
||||
else:
|
||||
chunk = self.tx_buffer[:self.mtu]
|
||||
self.tx_buffer = self.tx_buffer[len(chunk):]
|
||||
chunk = self.tx_buffer[: self.mtu]
|
||||
self.tx_buffer = self.tx_buffer[len(chunk) :]
|
||||
tx_credit_spent = True
|
||||
|
||||
# Update the tx credits
|
||||
@@ -503,13 +530,17 @@ class DLC(EventEmitter):
|
||||
self.tx_credits -= 1
|
||||
|
||||
# Send the frame
|
||||
logger.debug(f'>>> sending {len(chunk)} bytes with {rx_credits_needed} credits, rx_credits={self.rx_credits}, tx_credits={self.tx_credits}')
|
||||
logger.debug(
|
||||
f'>>> sending {len(chunk)} bytes with {rx_credits_needed} credits, '
|
||||
f'rx_credits={self.rx_credits}, '
|
||||
f'tx_credits={self.tx_credits}'
|
||||
)
|
||||
self.send_frame(
|
||||
RFCOMM_Frame.uih(
|
||||
c_r = self.c_r,
|
||||
dlci = self.dlci,
|
||||
information = chunk,
|
||||
p_f = 1 if rx_credits_needed > 0 else 0
|
||||
c_r=self.c_r,
|
||||
dlci=self.dlci,
|
||||
information=chunk,
|
||||
p_f=1 if rx_credits_needed > 0 else 0,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -518,8 +549,8 @@ class DLC(EventEmitter):
|
||||
# Stream protocol
|
||||
def write(self, data):
|
||||
# We can only send bytes
|
||||
if type(data) != bytes:
|
||||
if type(data) == str:
|
||||
if not isinstance(data, bytes):
|
||||
if isinstance(data, str):
|
||||
# Automatically convert strings to bytes using UTF-8
|
||||
data = data.encode('utf-8')
|
||||
else:
|
||||
@@ -543,34 +574,34 @@ class Multiplexer(EventEmitter):
|
||||
RESPONDER = 0x01
|
||||
|
||||
# States
|
||||
INIT = 0x00
|
||||
CONNECTING = 0x01
|
||||
CONNECTED = 0x02
|
||||
OPENING = 0x03
|
||||
INIT = 0x00
|
||||
CONNECTING = 0x01
|
||||
CONNECTED = 0x02
|
||||
OPENING = 0x03
|
||||
DISCONNECTING = 0x04
|
||||
DISCONNECTED = 0x05
|
||||
RESET = 0x06
|
||||
DISCONNECTED = 0x05
|
||||
RESET = 0x06
|
||||
|
||||
STATE_NAMES = {
|
||||
INIT: 'INIT',
|
||||
CONNECTING: 'CONNECTING',
|
||||
CONNECTED: 'CONNECTED',
|
||||
OPENING: 'OPENING',
|
||||
INIT: 'INIT',
|
||||
CONNECTING: 'CONNECTING',
|
||||
CONNECTED: 'CONNECTED',
|
||||
OPENING: 'OPENING',
|
||||
DISCONNECTING: 'DISCONNECTING',
|
||||
DISCONNECTED: 'DISCONNECTED',
|
||||
RESET: 'RESET'
|
||||
DISCONNECTED: 'DISCONNECTED',
|
||||
RESET: 'RESET',
|
||||
}
|
||||
|
||||
def __init__(self, l2cap_channel, role):
|
||||
super().__init__()
|
||||
self.role = role
|
||||
self.l2cap_channel = l2cap_channel
|
||||
self.state = Multiplexer.INIT
|
||||
self.dlcs = {} # DLCs, by DLCI
|
||||
self.connection_result = None
|
||||
self.role = role
|
||||
self.l2cap_channel = l2cap_channel
|
||||
self.state = Multiplexer.INIT
|
||||
self.dlcs = {} # DLCs, by DLCI
|
||||
self.connection_result = None
|
||||
self.disconnection_result = None
|
||||
self.open_result = None
|
||||
self.acceptor = None
|
||||
self.open_result = None
|
||||
self.acceptor = None
|
||||
|
||||
# Become a sink for the L2CAP channel
|
||||
l2cap_channel.sink = self.on_pdu
|
||||
@@ -580,7 +611,9 @@ class Multiplexer(EventEmitter):
|
||||
return Multiplexer.STATE_NAMES[state]
|
||||
|
||||
def change_state(self, new_state):
|
||||
logger.debug(f'{self} state change -> {color(self.state_name(new_state), "cyan")}')
|
||||
logger.debug(
|
||||
f'{self} state change -> {color(self.state_name(new_state), "cyan")}'
|
||||
)
|
||||
self.state = new_state
|
||||
|
||||
def send_frame(self, frame):
|
||||
@@ -596,14 +629,14 @@ class Multiplexer(EventEmitter):
|
||||
self.on_frame(frame)
|
||||
else:
|
||||
if frame.type == RFCOMM_DM_FRAME:
|
||||
# DM responses are for a DLCI, but since we only create the dlc when we receive
|
||||
# a PN response (because we need the parameters), we handle DM frames at the Multiplexer
|
||||
# level
|
||||
# DM responses are for a DLCI, but since we only create the dlc when we
|
||||
# receive a PN response (because we need the parameters), we handle DM
|
||||
# frames at the Multiplexer level
|
||||
self.on_dm_frame(frame)
|
||||
else:
|
||||
dlc = self.dlcs.get(frame.dlci)
|
||||
if dlc is None:
|
||||
logger.warn(f'no dlc for DLCI {frame.dlci}')
|
||||
logger.warning(f'no dlc for DLCI {frame.dlci}')
|
||||
return
|
||||
dlc.on_frame(frame)
|
||||
|
||||
@@ -611,14 +644,14 @@ class Multiplexer(EventEmitter):
|
||||
handler = getattr(self, f'on_{frame.type_name()}_frame'.lower())
|
||||
handler(frame)
|
||||
|
||||
def on_sabm_frame(self, frame):
|
||||
def on_sabm_frame(self, _frame):
|
||||
if self.state != Multiplexer.INIT:
|
||||
logger.debug('not in INIT state, ignoring SABM')
|
||||
return
|
||||
self.change_state(Multiplexer.CONNECTED)
|
||||
self.send_frame(RFCOMM_Frame.ua(c_r = 1, dlci = 0))
|
||||
self.send_frame(RFCOMM_Frame.ua(c_r=1, dlci=0))
|
||||
|
||||
def on_ua_frame(self, frame):
|
||||
def on_ua_frame(self, _frame):
|
||||
if self.state == Multiplexer.CONNECTING:
|
||||
self.change_state(Multiplexer.CONNECTED)
|
||||
if self.connection_result:
|
||||
@@ -630,25 +663,34 @@ class Multiplexer(EventEmitter):
|
||||
self.disconnection_result.set_result(None)
|
||||
self.disconnection_result = None
|
||||
|
||||
def on_dm_frame(self, frame):
|
||||
def on_dm_frame(self, _frame):
|
||||
if self.state == Multiplexer.OPENING:
|
||||
self.change_state(Multiplexer.CONNECTED)
|
||||
if self.open_result:
|
||||
self.open_result.set_exception(ConnectionError(ConnectionError.CONNECTION_REFUSED))
|
||||
self.open_result.set_exception(
|
||||
core.ConnectionError(
|
||||
core.ConnectionError.CONNECTION_REFUSED,
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
self.l2cap_channel.connection.peer_address,
|
||||
'rfcomm',
|
||||
)
|
||||
)
|
||||
else:
|
||||
logger.warn(f'unexpected state for DM: {self}')
|
||||
logger.warning(f'unexpected state for DM: {self}')
|
||||
|
||||
def on_disc_frame(self, frame):
|
||||
def on_disc_frame(self, _frame):
|
||||
self.change_state(Multiplexer.DISCONNECTED)
|
||||
self.send_frame(RFCOMM_Frame.ua(c_r = 0 if self.role == Multiplexer.INITIATOR else 1, dlci = 0))
|
||||
self.send_frame(
|
||||
RFCOMM_Frame.ua(c_r=0 if self.role == Multiplexer.INITIATOR else 1, dlci=0)
|
||||
)
|
||||
|
||||
def on_uih_frame(self, frame):
|
||||
(type, c_r, value) = RFCOMM_Frame.parse_mcc(frame.information)
|
||||
(mcc_type, c_r, value) = RFCOMM_Frame.parse_mcc(frame.information)
|
||||
|
||||
if type == RFCOMM_MCC_PN_TYPE:
|
||||
if mcc_type == RFCOMM_MCC_PN_TYPE:
|
||||
pn = RFCOMM_MCC_PN.from_bytes(value)
|
||||
self.on_mcc_pn(c_r, pn)
|
||||
elif type == RFCOMM_MCC_MSC_TYPE:
|
||||
elif mcc_type == RFCOMM_MCC_MSC_TYPE:
|
||||
mcs = RFCOMM_MCC_MSC.from_bytes(value)
|
||||
self.on_mcc_msc(c_r, mcs)
|
||||
|
||||
@@ -664,7 +706,7 @@ class Multiplexer(EventEmitter):
|
||||
if pn.dlci & 1:
|
||||
# Not expected, this is an initiator-side number
|
||||
# TODO: error out
|
||||
logger.warn(f'invalid DLCI: {pn.dlci}')
|
||||
logger.warning(f'invalid DLCI: {pn.dlci}')
|
||||
else:
|
||||
if self.acceptor:
|
||||
channel_number = pn.dlci >> 1
|
||||
@@ -680,10 +722,10 @@ class Multiplexer(EventEmitter):
|
||||
dlc.accept()
|
||||
else:
|
||||
# No acceptor, we're in Disconnected Mode
|
||||
self.send_frame(RFCOMM_Frame.dm(c_r = 1, dlci = pn.dlci))
|
||||
self.send_frame(RFCOMM_Frame.dm(c_r=1, dlci=pn.dlci))
|
||||
else:
|
||||
# No acceptor?? shouldn't happen
|
||||
logger.warn(color('!!! no acceptor registered', 'red'))
|
||||
logger.warning(color('!!! no acceptor registered', 'red'))
|
||||
else:
|
||||
# Response
|
||||
logger.debug(f'>>> PN Response: {pn}')
|
||||
@@ -692,12 +734,12 @@ class Multiplexer(EventEmitter):
|
||||
self.dlcs[pn.dlci] = dlc
|
||||
dlc.connect()
|
||||
else:
|
||||
logger.warn('ignoring PN response')
|
||||
logger.warning('ignoring PN response')
|
||||
|
||||
def on_mcc_msc(self, c_r, msc):
|
||||
dlc = self.dlcs.get(msc.dlci)
|
||||
if dlc is None:
|
||||
logger.warn(f'no dlc for DLCI {msc.dlci}')
|
||||
logger.warning(f'no dlc for DLCI {msc.dlci}')
|
||||
return
|
||||
dlc.on_mcc_msc(c_r, msc)
|
||||
|
||||
@@ -707,7 +749,7 @@ class Multiplexer(EventEmitter):
|
||||
|
||||
self.change_state(Multiplexer.CONNECTING)
|
||||
self.connection_result = asyncio.get_running_loop().create_future()
|
||||
self.send_frame(RFCOMM_Frame.sabm(c_r = 1, dlci = 0))
|
||||
self.send_frame(RFCOMM_Frame.sabm(c_r=1, dlci=0))
|
||||
return await self.connection_result
|
||||
|
||||
async def disconnect(self):
|
||||
@@ -716,34 +758,38 @@ class Multiplexer(EventEmitter):
|
||||
|
||||
self.disconnection_result = asyncio.get_running_loop().create_future()
|
||||
self.change_state(Multiplexer.DISCONNECTING)
|
||||
self.send_frame(RFCOMM_Frame.disc(c_r = 1 if self.role == Multiplexer.INITIATOR else 0, dlci = 0))
|
||||
self.send_frame(
|
||||
RFCOMM_Frame.disc(
|
||||
c_r=1 if self.role == Multiplexer.INITIATOR else 0, dlci=0
|
||||
)
|
||||
)
|
||||
await self.disconnection_result
|
||||
|
||||
async def open_dlc(self, channel):
|
||||
if self.state != Multiplexer.CONNECTED:
|
||||
if self.state == Multiplexer.OPENING:
|
||||
raise InvalidStateError('open already in progress')
|
||||
else:
|
||||
raise InvalidStateError('not connected')
|
||||
|
||||
raise InvalidStateError('not connected')
|
||||
|
||||
pn = RFCOMM_MCC_PN(
|
||||
dlci = channel << 1,
|
||||
cl = 0xF0,
|
||||
priority = 7,
|
||||
ack_timer = 0,
|
||||
max_frame_size = RFCOMM_DEFAULT_PREFERRED_MTU,
|
||||
max_retransmissions = 0,
|
||||
window_size = RFCOMM_DEFAULT_INITIAL_RX_CREDITS
|
||||
dlci=channel << 1,
|
||||
cl=0xF0,
|
||||
priority=7,
|
||||
ack_timer=0,
|
||||
max_frame_size=RFCOMM_DEFAULT_PREFERRED_MTU,
|
||||
max_retransmissions=0,
|
||||
window_size=RFCOMM_DEFAULT_INITIAL_RX_CREDITS,
|
||||
)
|
||||
mcc = RFCOMM_Frame.make_mcc(type = RFCOMM_MCC_PN_TYPE, c_r = 1, data = bytes(pn))
|
||||
mcc = RFCOMM_Frame.make_mcc(mcc_type=RFCOMM_MCC_PN_TYPE, c_r=1, data=bytes(pn))
|
||||
logger.debug(f'>>> Sending MCC: {pn}')
|
||||
self.open_result = asyncio.get_running_loop().create_future()
|
||||
self.change_state(Multiplexer.OPENING)
|
||||
self.send_frame(
|
||||
RFCOMM_Frame.uih(
|
||||
c_r = 1 if self.role == Multiplexer.INITIATOR else 0,
|
||||
dlci = 0,
|
||||
information = mcc
|
||||
c_r=1 if self.role == Multiplexer.INITIATOR else 0,
|
||||
dlci=0,
|
||||
information=mcc,
|
||||
)
|
||||
)
|
||||
result = await self.open_result
|
||||
@@ -763,17 +809,19 @@ class Multiplexer(EventEmitter):
|
||||
# -----------------------------------------------------------------------------
|
||||
class Client:
|
||||
def __init__(self, device, connection):
|
||||
self.device = device
|
||||
self.connection = connection
|
||||
self.device = device
|
||||
self.connection = connection
|
||||
self.l2cap_channel = None
|
||||
self.multiplexer = None
|
||||
self.multiplexer = None
|
||||
|
||||
async def start(self):
|
||||
# Create a new L2CAP connection
|
||||
try:
|
||||
self.l2cap_channel = await self.device.l2cap_channel_manager.connect(self.connection, RFCOMM_PSM)
|
||||
self.l2cap_channel = await self.device.l2cap_channel_manager.connect(
|
||||
self.connection, RFCOMM_PSM
|
||||
)
|
||||
except ProtocolError as error:
|
||||
logger.warn(f'L2CAP connection failed: {error}')
|
||||
logger.warning(f'L2CAP connection failed: {error}')
|
||||
raise
|
||||
|
||||
# Create a mutliplexer to manage DLCs with the server
|
||||
@@ -797,22 +845,34 @@ class Client:
|
||||
class Server(EventEmitter):
|
||||
def __init__(self, device):
|
||||
super().__init__()
|
||||
self.device = device
|
||||
self.device = device
|
||||
self.multiplexer = None
|
||||
self.acceptors = {}
|
||||
self.acceptors = {}
|
||||
|
||||
# Register ourselves with the L2CAP channel manager
|
||||
device.register_l2cap_server(RFCOMM_PSM, self.on_connection)
|
||||
|
||||
def listen(self, acceptor):
|
||||
# Find a free channel number
|
||||
for channel in range(RFCOMM_DYNAMIC_CHANNEL_NUMBER_START, RFCOMM_DYNAMIC_CHANNEL_NUMBER_END + 1):
|
||||
if channel not in self.acceptors:
|
||||
self.acceptors[channel] = acceptor
|
||||
return channel
|
||||
def listen(self, acceptor, channel=0):
|
||||
if channel:
|
||||
if channel in self.acceptors:
|
||||
# Busy
|
||||
return 0
|
||||
else:
|
||||
# Find a free channel number
|
||||
for candidate in range(
|
||||
RFCOMM_DYNAMIC_CHANNEL_NUMBER_START,
|
||||
RFCOMM_DYNAMIC_CHANNEL_NUMBER_END + 1,
|
||||
):
|
||||
if candidate not in self.acceptors:
|
||||
channel = candidate
|
||||
break
|
||||
|
||||
# All channels used...
|
||||
return 0
|
||||
if channel == 0:
|
||||
# All channels used...
|
||||
return 0
|
||||
|
||||
self.acceptors[channel] = acceptor
|
||||
return channel
|
||||
|
||||
def on_connection(self, l2cap_channel):
|
||||
logger.debug(f'+++ new L2CAP connection: {l2cap_channel}')
|
||||
|
||||
516
bumble/sdp.py
516
bumble/sdp.py
@@ -15,12 +15,13 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
import logging
|
||||
import struct
|
||||
from colors import color
|
||||
import colors
|
||||
from typing import Dict, List, Type
|
||||
|
||||
from . import core
|
||||
from .colors import color
|
||||
from .core import InvalidStateError
|
||||
from .hci import HCI_Object, name_or_number, key_with_value
|
||||
|
||||
@@ -33,6 +34,9 @@ logger = logging.getLogger(__name__)
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
# fmt: off
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
SDP_CONTINUATION_WATCHDOG = 64 # Maximum number of continuations we're willing to do
|
||||
|
||||
SDP_PSM = 0x0001
|
||||
@@ -112,137 +116,162 @@ SDP_PUBLIC_BROWSE_ROOT = core.UUID.from_16_bits(0x1002, 'PublicBrowseRoot')
|
||||
# To be used in searches where an attribute ID list allows a range to be specified
|
||||
SDP_ALL_ATTRIBUTES_RANGE = (0x0000FFFF, 4) # Express this as tuple so we can convey the desired encoding size
|
||||
|
||||
# fmt: on
|
||||
# pylint: enable=line-too-long
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class DataElement:
|
||||
NIL = 0
|
||||
NIL = 0
|
||||
UNSIGNED_INTEGER = 1
|
||||
SIGNED_INTEGER = 2
|
||||
UUID = 3
|
||||
TEXT_STRING = 4
|
||||
BOOLEAN = 5
|
||||
SEQUENCE = 6
|
||||
ALTERNATIVE = 7
|
||||
URL = 8
|
||||
SIGNED_INTEGER = 2
|
||||
UUID = 3
|
||||
TEXT_STRING = 4
|
||||
BOOLEAN = 5
|
||||
SEQUENCE = 6
|
||||
ALTERNATIVE = 7
|
||||
URL = 8
|
||||
|
||||
TYPE_NAMES = {
|
||||
NIL: 'NIL',
|
||||
NIL: 'NIL',
|
||||
UNSIGNED_INTEGER: 'UNSIGNED_INTEGER',
|
||||
SIGNED_INTEGER: 'SIGNED_INTEGER',
|
||||
UUID: 'UUID',
|
||||
TEXT_STRING: 'TEXT_STRING',
|
||||
BOOLEAN: 'BOOLEAN',
|
||||
SEQUENCE: 'SEQUENCE',
|
||||
ALTERNATIVE: 'ALTERNATIVE',
|
||||
URL: 'URL'
|
||||
SIGNED_INTEGER: 'SIGNED_INTEGER',
|
||||
UUID: 'UUID',
|
||||
TEXT_STRING: 'TEXT_STRING',
|
||||
BOOLEAN: 'BOOLEAN',
|
||||
SEQUENCE: 'SEQUENCE',
|
||||
ALTERNATIVE: 'ALTERNATIVE',
|
||||
URL: 'URL',
|
||||
}
|
||||
|
||||
type_constructors = {
|
||||
NIL: lambda x: DataElement(DataElement.NIL, None),
|
||||
UNSIGNED_INTEGER: lambda x, y: DataElement(DataElement.UNSIGNED_INTEGER, DataElement.unsigned_integer_from_bytes(x), value_size=y),
|
||||
SIGNED_INTEGER: lambda x, y: DataElement(DataElement.SIGNED_INTEGER, DataElement.signed_integer_from_bytes(x), value_size=y),
|
||||
UUID: lambda x: DataElement(DataElement.UUID, core.UUID.from_bytes(bytes(reversed(x)))),
|
||||
TEXT_STRING: lambda x: DataElement(DataElement.TEXT_STRING, x.decode('utf8')),
|
||||
BOOLEAN: lambda x: DataElement(DataElement.BOOLEAN, x[0] == 1),
|
||||
SEQUENCE: lambda x: DataElement(DataElement.SEQUENCE, DataElement.list_from_bytes(x)),
|
||||
ALTERNATIVE: lambda x: DataElement(DataElement.ALTERNATIVE, DataElement.list_from_bytes(x)),
|
||||
URL: lambda x: DataElement(DataElement.URL, x.decode('utf8'))
|
||||
NIL: lambda x: DataElement(DataElement.NIL, None),
|
||||
UNSIGNED_INTEGER: lambda x, y: DataElement(
|
||||
DataElement.UNSIGNED_INTEGER,
|
||||
DataElement.unsigned_integer_from_bytes(x),
|
||||
value_size=y,
|
||||
),
|
||||
SIGNED_INTEGER: lambda x, y: DataElement(
|
||||
DataElement.SIGNED_INTEGER,
|
||||
DataElement.signed_integer_from_bytes(x),
|
||||
value_size=y,
|
||||
),
|
||||
UUID: lambda x: DataElement(
|
||||
DataElement.UUID, core.UUID.from_bytes(bytes(reversed(x)))
|
||||
),
|
||||
TEXT_STRING: lambda x: DataElement(DataElement.TEXT_STRING, x.decode('utf8')),
|
||||
BOOLEAN: lambda x: DataElement(DataElement.BOOLEAN, x[0] == 1),
|
||||
SEQUENCE: lambda x: DataElement(
|
||||
DataElement.SEQUENCE, DataElement.list_from_bytes(x)
|
||||
),
|
||||
ALTERNATIVE: lambda x: DataElement(
|
||||
DataElement.ALTERNATIVE, DataElement.list_from_bytes(x)
|
||||
),
|
||||
URL: lambda x: DataElement(DataElement.URL, x.decode('utf8')),
|
||||
}
|
||||
|
||||
def __init__(self, type, value, value_size=None):
|
||||
self.type = type
|
||||
self.value = value
|
||||
def __init__(self, element_type, value, value_size=None):
|
||||
self.type = element_type
|
||||
self.value = value
|
||||
self.value_size = value_size
|
||||
self.bytes = None # Used a cache when parsing from bytes so we can emit a byte-for-byte replica
|
||||
if type == DataElement.UNSIGNED_INTEGER or type == DataElement.SIGNED_INTEGER:
|
||||
# Used as a cache when parsing from bytes so we can emit a byte-for-byte replica
|
||||
self.bytes = None
|
||||
if element_type in (DataElement.UNSIGNED_INTEGER, DataElement.SIGNED_INTEGER):
|
||||
if value_size is None:
|
||||
raise ValueError('integer types must have a value size specified')
|
||||
|
||||
@staticmethod
|
||||
def nil():
|
||||
def nil() -> DataElement:
|
||||
return DataElement(DataElement.NIL, None)
|
||||
|
||||
@staticmethod
|
||||
def unsigned_integer(value, value_size):
|
||||
def unsigned_integer(value: int, value_size: int) -> DataElement:
|
||||
return DataElement(DataElement.UNSIGNED_INTEGER, value, value_size)
|
||||
|
||||
@staticmethod
|
||||
def unsigned_integer_8(value):
|
||||
def unsigned_integer_8(value: int) -> DataElement:
|
||||
return DataElement(DataElement.UNSIGNED_INTEGER, value, value_size=1)
|
||||
|
||||
@staticmethod
|
||||
def unsigned_integer_16(value):
|
||||
def unsigned_integer_16(value: int) -> DataElement:
|
||||
return DataElement(DataElement.UNSIGNED_INTEGER, value, value_size=2)
|
||||
|
||||
@staticmethod
|
||||
def unsigned_integer_32(value):
|
||||
def unsigned_integer_32(value: int) -> DataElement:
|
||||
return DataElement(DataElement.UNSIGNED_INTEGER, value, value_size=4)
|
||||
|
||||
@staticmethod
|
||||
def signed_integer(value, value_size):
|
||||
def signed_integer(value: int, value_size: int) -> DataElement:
|
||||
return DataElement(DataElement.SIGNED_INTEGER, value, value_size)
|
||||
|
||||
@staticmethod
|
||||
def signed_integer_8(value):
|
||||
def signed_integer_8(value: int) -> DataElement:
|
||||
return DataElement(DataElement.SIGNED_INTEGER, value, value_size=1)
|
||||
|
||||
@staticmethod
|
||||
def signed_integer_16(value):
|
||||
def signed_integer_16(value: int) -> DataElement:
|
||||
return DataElement(DataElement.SIGNED_INTEGER, value, value_size=2)
|
||||
|
||||
@staticmethod
|
||||
def signed_integer_32(value):
|
||||
def signed_integer_32(value: int) -> DataElement:
|
||||
return DataElement(DataElement.SIGNED_INTEGER, value, value_size=4)
|
||||
|
||||
@staticmethod
|
||||
def uuid(value):
|
||||
def uuid(value: core.UUID) -> DataElement:
|
||||
return DataElement(DataElement.UUID, value)
|
||||
|
||||
@staticmethod
|
||||
def text_string(value):
|
||||
def text_string(value: str) -> DataElement:
|
||||
return DataElement(DataElement.TEXT_STRING, value)
|
||||
|
||||
@staticmethod
|
||||
def boolean(value):
|
||||
def boolean(value: bool) -> DataElement:
|
||||
return DataElement(DataElement.BOOLEAN, value)
|
||||
|
||||
@staticmethod
|
||||
def sequence(value):
|
||||
def sequence(value: List[DataElement]) -> DataElement:
|
||||
return DataElement(DataElement.SEQUENCE, value)
|
||||
|
||||
@staticmethod
|
||||
def alternative(value):
|
||||
def alternative(value: List[DataElement]) -> DataElement:
|
||||
return DataElement(DataElement.ALTERNATIVE, value)
|
||||
|
||||
@staticmethod
|
||||
def url(value):
|
||||
def url(value: str) -> DataElement:
|
||||
return DataElement(DataElement.URL, value)
|
||||
|
||||
@staticmethod
|
||||
def unsigned_integer_from_bytes(data):
|
||||
if len(data) == 1:
|
||||
return data[0]
|
||||
elif len(data) == 2:
|
||||
|
||||
if len(data) == 2:
|
||||
return struct.unpack('>H', data)[0]
|
||||
elif len(data) == 4:
|
||||
|
||||
if len(data) == 4:
|
||||
return struct.unpack('>I', data)[0]
|
||||
elif len(data) == 8:
|
||||
|
||||
if len(data) == 8:
|
||||
return struct.unpack('>Q', data)[0]
|
||||
else:
|
||||
raise ValueError(f'invalid integer length {len(data)}')
|
||||
|
||||
raise ValueError(f'invalid integer length {len(data)}')
|
||||
|
||||
@staticmethod
|
||||
def signed_integer_from_bytes(data):
|
||||
if len(data) == 1:
|
||||
return struct.unpack('b', data)[0]
|
||||
elif len(data) == 2:
|
||||
|
||||
if len(data) == 2:
|
||||
return struct.unpack('>h', data)[0]
|
||||
elif len(data) == 4:
|
||||
|
||||
if len(data) == 4:
|
||||
return struct.unpack('>i', data)[0]
|
||||
elif len(data) == 8:
|
||||
|
||||
if len(data) == 8:
|
||||
return struct.unpack('>q', data)[0]
|
||||
else:
|
||||
raise ValueError(f'invalid integer length {len(data)}')
|
||||
|
||||
raise ValueError(f'invalid integer length {len(data)}')
|
||||
|
||||
@staticmethod
|
||||
def list_from_bytes(data):
|
||||
@@ -250,7 +279,7 @@ class DataElement:
|
||||
while data:
|
||||
element = DataElement.from_bytes(data)
|
||||
elements.append(element)
|
||||
data = data[len(bytes(element)):]
|
||||
data = data[len(bytes(element)) :]
|
||||
return elements
|
||||
|
||||
@staticmethod
|
||||
@@ -260,11 +289,11 @@ class DataElement:
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(data):
|
||||
type = data[0] >> 3
|
||||
size_index = data[0] & 7
|
||||
element_type = data[0] >> 3
|
||||
size_index = data[0] & 7
|
||||
value_offset = 0
|
||||
if size_index == 0:
|
||||
if type == DataElement.NIL:
|
||||
if element_type == DataElement.NIL:
|
||||
value_size = 0
|
||||
else:
|
||||
value_size = 1
|
||||
@@ -286,16 +315,21 @@ class DataElement:
|
||||
value_size = struct.unpack('>I', data[1:5])[0]
|
||||
value_offset = 4
|
||||
|
||||
value_data = data[1 + value_offset:1 + value_offset + value_size]
|
||||
constructor = DataElement.type_constructors.get(type)
|
||||
value_data = data[1 + value_offset : 1 + value_offset + value_size]
|
||||
constructor = DataElement.type_constructors.get(element_type)
|
||||
if constructor:
|
||||
if type == DataElement.UNSIGNED_INTEGER or type == DataElement.SIGNED_INTEGER:
|
||||
if element_type in (
|
||||
DataElement.UNSIGNED_INTEGER,
|
||||
DataElement.SIGNED_INTEGER,
|
||||
):
|
||||
result = constructor(value_data, value_size)
|
||||
else:
|
||||
result = constructor(value_data)
|
||||
else:
|
||||
result = DataElement(type, value_data)
|
||||
result.bytes = data[:1 + value_offset + value_size] # Keep a copy so we can re-serialize to an exact replica
|
||||
result = DataElement(element_type, value_data)
|
||||
result.bytes = data[
|
||||
: 1 + value_offset + value_size
|
||||
] # Keep a copy so we can re-serialize to an exact replica
|
||||
return result
|
||||
|
||||
def to_bytes(self):
|
||||
@@ -311,7 +345,8 @@ class DataElement:
|
||||
elif self.type == DataElement.UNSIGNED_INTEGER:
|
||||
if self.value < 0:
|
||||
raise ValueError('UNSIGNED_INTEGER cannot be negative')
|
||||
elif self.value_size == 1:
|
||||
|
||||
if self.value_size == 1:
|
||||
data = struct.pack('B', self.value)
|
||||
elif self.value_size == 2:
|
||||
data = struct.pack('>H', self.value)
|
||||
@@ -334,11 +369,11 @@ class DataElement:
|
||||
raise ValueError('invalid value_size')
|
||||
elif self.type == DataElement.UUID:
|
||||
data = bytes(reversed(bytes(self.value)))
|
||||
elif self.type == DataElement.TEXT_STRING or self.type == DataElement.URL:
|
||||
elif self.type in (DataElement.TEXT_STRING, DataElement.URL):
|
||||
data = self.value.encode('utf8')
|
||||
elif self.type == DataElement.BOOLEAN:
|
||||
data = bytes([1 if self.value else 0])
|
||||
elif self.type == DataElement.SEQUENCE or self.type == DataElement.ALTERNATIVE:
|
||||
elif self.type in (DataElement.SEQUENCE, DataElement.ALTERNATIVE):
|
||||
data = b''.join([bytes(element) for element in self.value])
|
||||
else:
|
||||
data = self.value
|
||||
@@ -349,9 +384,11 @@ class DataElement:
|
||||
if size != 0:
|
||||
raise ValueError('NIL must be empty')
|
||||
size_index = 0
|
||||
elif (self.type == DataElement.UNSIGNED_INTEGER or
|
||||
self.type == DataElement.SIGNED_INTEGER or
|
||||
self.type == DataElement.UUID):
|
||||
elif self.type in (
|
||||
DataElement.UNSIGNED_INTEGER,
|
||||
DataElement.SIGNED_INTEGER,
|
||||
DataElement.UUID,
|
||||
):
|
||||
if size <= 1:
|
||||
size_index = 0
|
||||
elif size == 2:
|
||||
@@ -364,10 +401,12 @@ class DataElement:
|
||||
size_index = 4
|
||||
else:
|
||||
raise ValueError('invalid data size')
|
||||
elif (self.type == DataElement.TEXT_STRING or
|
||||
self.type == DataElement.SEQUENCE or
|
||||
self.type == DataElement.ALTERNATIVE or
|
||||
self.type == DataElement.URL):
|
||||
elif self.type in (
|
||||
DataElement.TEXT_STRING,
|
||||
DataElement.SEQUENCE,
|
||||
DataElement.ALTERNATIVE,
|
||||
DataElement.URL,
|
||||
):
|
||||
if size <= 0xFF:
|
||||
size_index = 5
|
||||
size_bytes = bytes([size])
|
||||
@@ -392,11 +431,19 @@ class DataElement:
|
||||
type_name = name_or_number(self.TYPE_NAMES, self.type)
|
||||
if self.type == DataElement.NIL:
|
||||
value_string = ''
|
||||
elif self.type == DataElement.SEQUENCE or self.type == DataElement.ALTERNATIVE:
|
||||
elif self.type in (DataElement.SEQUENCE, DataElement.ALTERNATIVE):
|
||||
container_separator = '\n' if pretty else ''
|
||||
element_separator = '\n' if pretty else ','
|
||||
value_string = f'[{container_separator}{element_separator.join([element.to_string(pretty, indentation + 1 if pretty else 0) for element in self.value])}{container_separator}{prefix}]'
|
||||
elif self.type == DataElement.UNSIGNED_INTEGER or self.type == DataElement.SIGNED_INTEGER:
|
||||
elements = [
|
||||
element.to_string(pretty, indentation + 1 if pretty else 0)
|
||||
for element in self.value
|
||||
]
|
||||
value_string = (
|
||||
f'[{container_separator}'
|
||||
f'{element_separator.join(elements)}'
|
||||
f'{container_separator}{prefix}]'
|
||||
)
|
||||
elif self.type in (DataElement.UNSIGNED_INTEGER, DataElement.SIGNED_INTEGER):
|
||||
value_string = f'{self.value}#{self.value_size}'
|
||||
elif isinstance(self.value, DataElement):
|
||||
value_string = self.value.to_string(pretty, indentation)
|
||||
@@ -410,17 +457,17 @@ class DataElement:
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class ServiceAttribute:
|
||||
def __init__(self, id, value):
|
||||
self.id = id
|
||||
def __init__(self, attribute_id: int, value: DataElement) -> None:
|
||||
self.id = attribute_id
|
||||
self.value = value
|
||||
|
||||
@staticmethod
|
||||
def list_from_data_elements(elements):
|
||||
attribute_list = []
|
||||
for i in range(0, len(elements) // 2):
|
||||
attribute_id, attribute_value = elements[2 * i:2 * (i + 1)]
|
||||
attribute_id, attribute_value = elements[2 * i : 2 * (i + 1)]
|
||||
if attribute_id.type != DataElement.UNSIGNED_INTEGER:
|
||||
logger.warn('attribute ID element is not an integer')
|
||||
logger.warning('attribute ID element is not an integer')
|
||||
continue
|
||||
attribute_list.append(ServiceAttribute(attribute_id.value, attribute_value))
|
||||
|
||||
@@ -428,30 +475,41 @@ class ServiceAttribute:
|
||||
|
||||
@staticmethod
|
||||
def find_attribute_in_list(attribute_list, attribute_id):
|
||||
return next((attribute.value for attribute in attribute_list if attribute.id == attribute_id), None)
|
||||
return next(
|
||||
(
|
||||
attribute.value
|
||||
for attribute in attribute_list
|
||||
if attribute.id == attribute_id
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def id_name(id):
|
||||
return name_or_number(SDP_ATTRIBUTE_ID_NAMES, id)
|
||||
def id_name(id_code):
|
||||
return name_or_number(SDP_ATTRIBUTE_ID_NAMES, id_code)
|
||||
|
||||
@staticmethod
|
||||
def is_uuid_in_value(uuid, value):
|
||||
# Find if a uuid matches a value, either directly or recursing into sequences
|
||||
if value.type == DataElement.UUID:
|
||||
return value.value == uuid
|
||||
elif value.type == DataElement.SEQUENCE:
|
||||
|
||||
if value.type == DataElement.SEQUENCE:
|
||||
for element in value.value:
|
||||
if ServiceAttribute.is_uuid_in_value(uuid, element):
|
||||
return True
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
def to_string(self, color=False):
|
||||
if color:
|
||||
return f'Attribute(id={colors.color(self.id_name(self.id),"magenta")},value={self.value})'
|
||||
else:
|
||||
return f'Attribute(id={self.id_name(self.id)},value={self.value})'
|
||||
return False
|
||||
|
||||
def to_string(self, with_colors=False):
|
||||
if with_colors:
|
||||
return (
|
||||
f'Attribute(id={color(self.id_name(self.id),"magenta")},'
|
||||
f'value={self.value})'
|
||||
)
|
||||
|
||||
return f'Attribute(id={self.id_name(self.id)},value={self.value})'
|
||||
|
||||
def __str__(self):
|
||||
return self.to_string()
|
||||
@@ -462,11 +520,14 @@ class SDP_PDU:
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part B - 4.2 PROTOCOL DATA UNIT FORMAT
|
||||
'''
|
||||
sdp_pdu_classes = {}
|
||||
|
||||
sdp_pdu_classes: Dict[int, Type[SDP_PDU]] = {}
|
||||
name = None
|
||||
pdu_id = 0
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(pdu):
|
||||
pdu_id, transaction_id, parameters_length = struct.unpack_from('>BHH', pdu, 0)
|
||||
pdu_id, transaction_id, _parameters_length = struct.unpack_from('>BHH', pdu, 0)
|
||||
|
||||
cls = SDP_PDU.sdp_pdu_classes.get(pdu_id)
|
||||
if cls is None:
|
||||
@@ -484,13 +545,15 @@ class SDP_PDU:
|
||||
@staticmethod
|
||||
def parse_service_record_handle_list_preceded_by_count(data, offset):
|
||||
count = struct.unpack_from('>H', data, offset - 2)[0]
|
||||
handle_list = [struct.unpack_from('>I', data, offset + x * 4)[0] for x in range(count)]
|
||||
handle_list = [
|
||||
struct.unpack_from('>I', data, offset + x * 4)[0] for x in range(count)
|
||||
]
|
||||
return offset + count * 4, handle_list
|
||||
|
||||
@staticmethod
|
||||
def parse_bytes_preceded_by_length(data, offset):
|
||||
length = struct.unpack_from('>H', data, offset - 2)[0]
|
||||
return offset + length, data[offset:offset + length]
|
||||
return offset + length, data[offset : offset + length]
|
||||
|
||||
@staticmethod
|
||||
def error_name(error_code):
|
||||
@@ -532,7 +595,10 @@ class SDP_PDU:
|
||||
HCI_Object.init_from_fields(self, self.fields, kwargs)
|
||||
if pdu is None:
|
||||
parameters = HCI_Object.dict_to_bytes(kwargs, self.fields)
|
||||
pdu = struct.pack('>BHH', self.pdu_id, transaction_id, len(parameters)) + parameters
|
||||
pdu = (
|
||||
struct.pack('>BHH', self.pdu_id, transaction_id, len(parameters))
|
||||
+ parameters
|
||||
)
|
||||
self.pdu = pdu
|
||||
self.transaction_id = transaction_id
|
||||
|
||||
@@ -555,9 +621,7 @@ class SDP_PDU:
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@SDP_PDU.subclass([
|
||||
('error_code', {'size': 2, 'mapper': SDP_PDU.error_name})
|
||||
])
|
||||
@SDP_PDU.subclass([('error_code', {'size': 2, 'mapper': SDP_PDU.error_name})])
|
||||
class SDP_ErrorResponse(SDP_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part B - 4.4.1 SDP_ErrorResponse PDU
|
||||
@@ -565,11 +629,13 @@ class SDP_ErrorResponse(SDP_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@SDP_PDU.subclass([
|
||||
('service_search_pattern', DataElement.parse_from_bytes),
|
||||
('maximum_service_record_count', '>2'),
|
||||
('continuation_state', '*')
|
||||
])
|
||||
@SDP_PDU.subclass(
|
||||
[
|
||||
('service_search_pattern', DataElement.parse_from_bytes),
|
||||
('maximum_service_record_count', '>2'),
|
||||
('continuation_state', '*'),
|
||||
]
|
||||
)
|
||||
class SDP_ServiceSearchRequest(SDP_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part B - 4.5.1 SDP_ServiceSearchRequest PDU
|
||||
@@ -577,12 +643,17 @@ class SDP_ServiceSearchRequest(SDP_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@SDP_PDU.subclass([
|
||||
('total_service_record_count', '>2'),
|
||||
('current_service_record_count', '>2'),
|
||||
('service_record_handle_list', SDP_PDU.parse_service_record_handle_list_preceded_by_count),
|
||||
('continuation_state', '*')
|
||||
])
|
||||
@SDP_PDU.subclass(
|
||||
[
|
||||
('total_service_record_count', '>2'),
|
||||
('current_service_record_count', '>2'),
|
||||
(
|
||||
'service_record_handle_list',
|
||||
SDP_PDU.parse_service_record_handle_list_preceded_by_count,
|
||||
),
|
||||
('continuation_state', '*'),
|
||||
]
|
||||
)
|
||||
class SDP_ServiceSearchResponse(SDP_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part B - 4.5.2 SDP_ServiceSearchResponse PDU
|
||||
@@ -590,12 +661,14 @@ class SDP_ServiceSearchResponse(SDP_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@SDP_PDU.subclass([
|
||||
('service_record_handle', '>4'),
|
||||
('maximum_attribute_byte_count', '>2'),
|
||||
('attribute_id_list', DataElement.parse_from_bytes),
|
||||
('continuation_state', '*')
|
||||
])
|
||||
@SDP_PDU.subclass(
|
||||
[
|
||||
('service_record_handle', '>4'),
|
||||
('maximum_attribute_byte_count', '>2'),
|
||||
('attribute_id_list', DataElement.parse_from_bytes),
|
||||
('continuation_state', '*'),
|
||||
]
|
||||
)
|
||||
class SDP_ServiceAttributeRequest(SDP_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part B - 4.6.1 SDP_ServiceAttributeRequest PDU
|
||||
@@ -603,11 +676,13 @@ class SDP_ServiceAttributeRequest(SDP_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@SDP_PDU.subclass([
|
||||
('attribute_list_byte_count', '>2'),
|
||||
('attribute_list', SDP_PDU.parse_bytes_preceded_by_length),
|
||||
('continuation_state', '*')
|
||||
])
|
||||
@SDP_PDU.subclass(
|
||||
[
|
||||
('attribute_list_byte_count', '>2'),
|
||||
('attribute_list', SDP_PDU.parse_bytes_preceded_by_length),
|
||||
('continuation_state', '*'),
|
||||
]
|
||||
)
|
||||
class SDP_ServiceAttributeResponse(SDP_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part B - 4.6.2 SDP_ServiceAttributeResponse PDU
|
||||
@@ -615,12 +690,14 @@ class SDP_ServiceAttributeResponse(SDP_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@SDP_PDU.subclass([
|
||||
('service_search_pattern', DataElement.parse_from_bytes),
|
||||
('maximum_attribute_byte_count', '>2'),
|
||||
('attribute_id_list', DataElement.parse_from_bytes),
|
||||
('continuation_state', '*')
|
||||
])
|
||||
@SDP_PDU.subclass(
|
||||
[
|
||||
('service_search_pattern', DataElement.parse_from_bytes),
|
||||
('maximum_attribute_byte_count', '>2'),
|
||||
('attribute_id_list', DataElement.parse_from_bytes),
|
||||
('continuation_state', '*'),
|
||||
]
|
||||
)
|
||||
class SDP_ServiceSearchAttributeRequest(SDP_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part B - 4.7.1 SDP_ServiceSearchAttributeRequest PDU
|
||||
@@ -628,11 +705,13 @@ class SDP_ServiceSearchAttributeRequest(SDP_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@SDP_PDU.subclass([
|
||||
('attribute_lists_byte_count', '>2'),
|
||||
('attribute_lists', SDP_PDU.parse_bytes_preceded_by_length),
|
||||
('continuation_state', '*')
|
||||
])
|
||||
@SDP_PDU.subclass(
|
||||
[
|
||||
('attribute_lists_byte_count', '>2'),
|
||||
('attribute_lists', SDP_PDU.parse_bytes_preceded_by_length),
|
||||
('continuation_state', '*'),
|
||||
]
|
||||
)
|
||||
class SDP_ServiceSearchAttributeResponse(SDP_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part B - 4.7.2 SDP_ServiceSearchAttributeResponse PDU
|
||||
@@ -642,9 +721,9 @@ class SDP_ServiceSearchAttributeResponse(SDP_PDU):
|
||||
# -----------------------------------------------------------------------------
|
||||
class Client:
|
||||
def __init__(self, device):
|
||||
self.device = device
|
||||
self.device = device
|
||||
self.pending_request = None
|
||||
self.channel = None
|
||||
self.channel = None
|
||||
|
||||
async def connect(self, connection):
|
||||
result = await self.device.l2cap_channel_manager.connect(connection, SDP_PSM)
|
||||
@@ -659,7 +738,9 @@ class Client:
|
||||
if self.pending_request is not None:
|
||||
raise InvalidStateError('request already pending')
|
||||
|
||||
service_search_pattern = DataElement.sequence([DataElement.uuid(uuid) for uuid in uuids])
|
||||
service_search_pattern = DataElement.sequence(
|
||||
[DataElement.uuid(uuid) for uuid in uuids]
|
||||
)
|
||||
|
||||
# Request and accumulate until there's no more continuation
|
||||
service_record_handle_list = []
|
||||
@@ -668,10 +749,10 @@ class Client:
|
||||
while watchdog > 0:
|
||||
response_pdu = await self.channel.send_request(
|
||||
SDP_ServiceSearchRequest(
|
||||
transaction_id = 0, # Transaction ID TODO: pick a real value
|
||||
service_search_pattern = service_search_pattern,
|
||||
maximum_service_record_count = 0xFFFF,
|
||||
continuation_state = continuation_state
|
||||
transaction_id=0, # Transaction ID TODO: pick a real value
|
||||
service_search_pattern=service_search_pattern,
|
||||
maximum_service_record_count=0xFFFF,
|
||||
continuation_state=continuation_state,
|
||||
)
|
||||
)
|
||||
response = SDP_PDU.from_bytes(response_pdu)
|
||||
@@ -689,11 +770,15 @@ class Client:
|
||||
if self.pending_request is not None:
|
||||
raise InvalidStateError('request already pending')
|
||||
|
||||
service_search_pattern = DataElement.sequence([DataElement.uuid(uuid) for uuid in uuids])
|
||||
service_search_pattern = DataElement.sequence(
|
||||
[DataElement.uuid(uuid) for uuid in uuids]
|
||||
)
|
||||
attribute_id_list = DataElement.sequence(
|
||||
[
|
||||
DataElement.unsigned_integer(attribute_id[0], value_size=attribute_id[1])
|
||||
if type(attribute_id) is tuple
|
||||
DataElement.unsigned_integer(
|
||||
attribute_id[0], value_size=attribute_id[1]
|
||||
)
|
||||
if isinstance(attribute_id, tuple)
|
||||
else DataElement.unsigned_integer_16(attribute_id)
|
||||
for attribute_id in attribute_ids
|
||||
]
|
||||
@@ -706,11 +791,11 @@ class Client:
|
||||
while watchdog > 0:
|
||||
response_pdu = await self.channel.send_request(
|
||||
SDP_ServiceSearchAttributeRequest(
|
||||
transaction_id = 0, # Transaction ID TODO: pick a real value
|
||||
service_search_pattern = service_search_pattern,
|
||||
maximum_attribute_byte_count = 0xFFFF,
|
||||
attribute_id_list = attribute_id_list,
|
||||
continuation_state = continuation_state
|
||||
transaction_id=0, # Transaction ID TODO: pick a real value
|
||||
service_search_pattern=service_search_pattern,
|
||||
maximum_attribute_byte_count=0xFFFF,
|
||||
attribute_id_list=attribute_id_list,
|
||||
continuation_state=continuation_state,
|
||||
)
|
||||
)
|
||||
response = SDP_PDU.from_bytes(response_pdu)
|
||||
@@ -725,7 +810,7 @@ class Client:
|
||||
# Parse the result into attribute lists
|
||||
attribute_lists_sequences = DataElement.from_bytes(accumulator)
|
||||
if attribute_lists_sequences.type != DataElement.SEQUENCE:
|
||||
logger.warn('unexpected data type')
|
||||
logger.warning('unexpected data type')
|
||||
return []
|
||||
|
||||
return [
|
||||
@@ -740,8 +825,10 @@ class Client:
|
||||
|
||||
attribute_id_list = DataElement.sequence(
|
||||
[
|
||||
DataElement.unsigned_integer(attribute_id[0], value_size=attribute_id[1])
|
||||
if type(attribute_id) is tuple
|
||||
DataElement.unsigned_integer(
|
||||
attribute_id[0], value_size=attribute_id[1]
|
||||
)
|
||||
if isinstance(attribute_id, tuple)
|
||||
else DataElement.unsigned_integer_16(attribute_id)
|
||||
for attribute_id in attribute_ids
|
||||
]
|
||||
@@ -754,11 +841,11 @@ class Client:
|
||||
while watchdog > 0:
|
||||
response_pdu = await self.channel.send_request(
|
||||
SDP_ServiceAttributeRequest(
|
||||
transaction_id = 0, # Transaction ID TODO: pick a real value
|
||||
service_record_handle = service_record_handle,
|
||||
maximum_attribute_byte_count = 0xFFFF,
|
||||
attribute_id_list = attribute_id_list,
|
||||
continuation_state = continuation_state
|
||||
transaction_id=0, # Transaction ID TODO: pick a real value
|
||||
service_record_handle=service_record_handle,
|
||||
maximum_attribute_byte_count=0xFFFF,
|
||||
attribute_id_list=attribute_id_list,
|
||||
continuation_state=continuation_state,
|
||||
)
|
||||
)
|
||||
response = SDP_PDU.from_bytes(response_pdu)
|
||||
@@ -773,7 +860,7 @@ class Client:
|
||||
# Parse the result into a list of attributes
|
||||
attribute_list_sequence = DataElement.from_bytes(accumulator)
|
||||
if attribute_list_sequence.type != DataElement.SEQUENCE:
|
||||
logger.warn('unexpected data type')
|
||||
logger.warning('unexpected data type')
|
||||
return []
|
||||
|
||||
return ServiceAttribute.list_from_data_elements(attribute_list_sequence.value)
|
||||
@@ -784,8 +871,9 @@ class Server:
|
||||
CONTINUATION_STATE = bytes([0x01, 0x43])
|
||||
|
||||
def __init__(self, device):
|
||||
self.device = device
|
||||
self.service_records = {} # Service records maps, by record handle
|
||||
self.device = device
|
||||
self.service_records = {} # Service records maps, by record handle
|
||||
self.channel = None
|
||||
self.current_response = None
|
||||
|
||||
def register(self, l2cap_channel_manager):
|
||||
@@ -820,11 +908,10 @@ class Server:
|
||||
try:
|
||||
sdp_pdu = SDP_PDU.from_bytes(pdu)
|
||||
except Exception as error:
|
||||
logger.warn(color(f'failed to parse SDP Request PDU: {error}', 'red'))
|
||||
logger.warning(color(f'failed to parse SDP Request PDU: {error}', 'red'))
|
||||
self.send_response(
|
||||
SDP_ErrorResponse(
|
||||
transaction_id = 0,
|
||||
error_code = SDP_INVALID_REQUEST_SYNTAX_ERROR
|
||||
transaction_id=0, error_code=SDP_INVALID_REQUEST_SYNTAX_ERROR
|
||||
)
|
||||
)
|
||||
|
||||
@@ -840,16 +927,16 @@ class Server:
|
||||
logger.warning(f'{color("!!! Exception in handler:", "red")} {error}')
|
||||
self.send_response(
|
||||
SDP_ErrorResponse(
|
||||
transaction_id = sdp_pdu.transaction_id,
|
||||
error_code = SDP_INSUFFICIENT_RESOURCES_TO_SATISFY_REQUEST_ERROR
|
||||
transaction_id=sdp_pdu.transaction_id,
|
||||
error_code=SDP_INSUFFICIENT_RESOURCES_TO_SATISFY_REQUEST_ERROR,
|
||||
)
|
||||
)
|
||||
else:
|
||||
logger.error(color('SDP Request not handled???', 'red'))
|
||||
self.send_response(
|
||||
SDP_ErrorResponse(
|
||||
transaction_id = sdp_pdu.transaction_id,
|
||||
error_code = SDP_INVALID_REQUEST_SYNTAX_ERROR
|
||||
transaction_id=sdp_pdu.transaction_id,
|
||||
error_code=SDP_INVALID_REQUEST_SYNTAX_ERROR,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -872,17 +959,18 @@ class Server:
|
||||
if attribute_id.value_size == 4:
|
||||
# Attribute ID range
|
||||
id_range_start = attribute_id.value >> 16
|
||||
id_range_end = attribute_id.value & 0xFFFF
|
||||
id_range_end = attribute_id.value & 0xFFFF
|
||||
else:
|
||||
id_range_start = attribute_id.value
|
||||
id_range_end = attribute_id.value
|
||||
id_range_end = attribute_id.value
|
||||
attributes += [
|
||||
attribute for attribute in service
|
||||
attribute
|
||||
for attribute in service
|
||||
if attribute.id >= id_range_start and attribute.id <= id_range_end
|
||||
]
|
||||
|
||||
# Return the maching attributes, sorted by attribute id
|
||||
attributes.sort(key = lambda x: x.id)
|
||||
# Return the matching attributes, sorted by attribute id
|
||||
attributes.sort(key=lambda x: x.id)
|
||||
attribute_list = DataElement.sequence([])
|
||||
for attribute in attributes:
|
||||
attribute_list.value.append(DataElement.unsigned_integer_16(attribute.id))
|
||||
@@ -896,8 +984,8 @@ class Server:
|
||||
if not self.current_response:
|
||||
self.send_response(
|
||||
SDP_ErrorResponse(
|
||||
transaction_id = request.transaction_id,
|
||||
error_code = SDP_INVALID_CONTINUATION_STATE_ERROR
|
||||
transaction_id=request.transaction_id,
|
||||
error_code=SDP_INVALID_CONTINUATION_STATE_ERROR,
|
||||
)
|
||||
)
|
||||
return
|
||||
@@ -910,30 +998,38 @@ class Server:
|
||||
service_record_handles = list(matching_services.keys())
|
||||
|
||||
# Only return up to the maximum requested
|
||||
service_record_handles_subset = service_record_handles[:request.maximum_service_record_count]
|
||||
service_record_handles_subset = service_record_handles[
|
||||
: request.maximum_service_record_count
|
||||
]
|
||||
|
||||
# Serialize to a byte array, and remember the total count
|
||||
logger.debug(f'Service Record Handles: {service_record_handles}')
|
||||
self.current_response = (
|
||||
len(service_record_handles),
|
||||
service_record_handles_subset
|
||||
service_record_handles_subset,
|
||||
)
|
||||
|
||||
# Respond, keeping any unsent handles for later
|
||||
service_record_handles = self.current_response[1][:request.maximum_service_record_count]
|
||||
service_record_handles = self.current_response[1][
|
||||
: request.maximum_service_record_count
|
||||
]
|
||||
self.current_response = (
|
||||
self.current_response[0],
|
||||
self.current_response[1][request.maximum_service_record_count:]
|
||||
self.current_response[1][request.maximum_service_record_count :],
|
||||
)
|
||||
continuation_state = (
|
||||
Server.CONTINUATION_STATE if self.current_response[1] else bytes([0])
|
||||
)
|
||||
service_record_handle_list = b''.join(
|
||||
[struct.pack('>I', handle) for handle in service_record_handles]
|
||||
)
|
||||
continuation_state = Server.CONTINUATION_STATE if self.current_response[1] else bytes([0])
|
||||
service_record_handle_list = b''.join([struct.pack('>I', handle) for handle in service_record_handles])
|
||||
self.send_response(
|
||||
SDP_ServiceSearchResponse(
|
||||
transaction_id = request.transaction_id,
|
||||
total_service_record_count = self.current_response[0],
|
||||
current_service_record_count = len(service_record_handles),
|
||||
service_record_handle_list = service_record_handle_list,
|
||||
continuation_state = continuation_state
|
||||
transaction_id=request.transaction_id,
|
||||
total_service_record_count=self.current_response[0],
|
||||
current_service_record_count=len(service_record_handles),
|
||||
service_record_handle_list=service_record_handle_list,
|
||||
continuation_state=continuation_state,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -943,8 +1039,8 @@ class Server:
|
||||
if not self.current_response:
|
||||
self.send_response(
|
||||
SDP_ErrorResponse(
|
||||
transaction_id = request.transaction_id,
|
||||
error_code = SDP_INVALID_CONTINUATION_STATE_ERROR
|
||||
transaction_id=request.transaction_id,
|
||||
error_code=SDP_INVALID_CONTINUATION_STATE_ERROR,
|
||||
)
|
||||
)
|
||||
return
|
||||
@@ -957,27 +1053,31 @@ class Server:
|
||||
if service is None:
|
||||
self.send_response(
|
||||
SDP_ErrorResponse(
|
||||
transaction_id = request.transaction_id,
|
||||
error_code = SDP_INVALID_SERVICE_RECORD_HANDLE_ERROR
|
||||
transaction_id=request.transaction_id,
|
||||
error_code=SDP_INVALID_SERVICE_RECORD_HANDLE_ERROR,
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
# Get the attributes for the service
|
||||
attribute_list = Server.get_service_attributes(service, request.attribute_id_list.value)
|
||||
attribute_list = Server.get_service_attributes(
|
||||
service, request.attribute_id_list.value
|
||||
)
|
||||
|
||||
# Serialize to a byte array
|
||||
logger.debug(f'Attributes: {attribute_list}')
|
||||
self.current_response = bytes(attribute_list)
|
||||
|
||||
# Respond, keeping any pending chunks for later
|
||||
attribute_list, continuation_state = self.get_next_response_payload(request.maximum_attribute_byte_count)
|
||||
attribute_list, continuation_state = self.get_next_response_payload(
|
||||
request.maximum_attribute_byte_count
|
||||
)
|
||||
self.send_response(
|
||||
SDP_ServiceAttributeResponse(
|
||||
transaction_id = request.transaction_id,
|
||||
attribute_list_byte_count = len(attribute_list),
|
||||
attribute_list = attribute_list,
|
||||
continuation_state = continuation_state
|
||||
transaction_id=request.transaction_id,
|
||||
attribute_list_byte_count=len(attribute_list),
|
||||
attribute_list=attribute_list,
|
||||
continuation_state=continuation_state,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -987,8 +1087,8 @@ class Server:
|
||||
if not self.current_response:
|
||||
self.send_response(
|
||||
SDP_ErrorResponse(
|
||||
transaction_id = request.transaction_id,
|
||||
error_code = SDP_INVALID_CONTINUATION_STATE_ERROR
|
||||
transaction_id=request.transaction_id,
|
||||
error_code=SDP_INVALID_CONTINUATION_STATE_ERROR,
|
||||
)
|
||||
)
|
||||
else:
|
||||
@@ -996,12 +1096,16 @@ class Server:
|
||||
self.current_response = None
|
||||
|
||||
# Find the matching services
|
||||
matching_services = self.match_services(request.service_search_pattern).values()
|
||||
matching_services = self.match_services(
|
||||
request.service_search_pattern
|
||||
).values()
|
||||
|
||||
# Filter the required attributes
|
||||
attribute_lists = DataElement.sequence([])
|
||||
for service in matching_services:
|
||||
attribute_list = Server.get_service_attributes(service, request.attribute_id_list.value)
|
||||
attribute_list = Server.get_service_attributes(
|
||||
service, request.attribute_id_list.value
|
||||
)
|
||||
if attribute_list.value:
|
||||
attribute_lists.value.append(attribute_list)
|
||||
|
||||
@@ -1010,12 +1114,14 @@ class Server:
|
||||
self.current_response = bytes(attribute_lists)
|
||||
|
||||
# Respond, keeping any pending chunks for later
|
||||
attribute_lists, continuation_state = self.get_next_response_payload(request.maximum_attribute_byte_count)
|
||||
attribute_lists, continuation_state = self.get_next_response_payload(
|
||||
request.maximum_attribute_byte_count
|
||||
)
|
||||
self.send_response(
|
||||
SDP_ServiceSearchAttributeResponse(
|
||||
transaction_id = request.transaction_id,
|
||||
attribute_lists_byte_count = len(attribute_lists),
|
||||
attribute_lists = attribute_lists,
|
||||
continuation_state = continuation_state
|
||||
transaction_id=request.transaction_id,
|
||||
attribute_lists_byte_count=len(attribute_lists),
|
||||
attribute_lists=attribute_lists,
|
||||
continuation_state=continuation_state,
|
||||
)
|
||||
)
|
||||
|
||||
962
bumble/smp.py
962
bumble/smp.py
File diff suppressed because it is too large
Load Diff
170
bumble/snoop.py
Normal file
170
bumble/snoop.py
Normal file
@@ -0,0 +1,170 @@
|
||||
# Copyright 2021-2023 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from contextlib import contextmanager
|
||||
from enum import IntEnum
|
||||
import logging
|
||||
import struct
|
||||
import datetime
|
||||
from typing import BinaryIO, Generator
|
||||
import os
|
||||
|
||||
from bumble.hci import HCI_COMMAND_PACKET, HCI_EVENT_PACKET
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Classes
|
||||
# -----------------------------------------------------------------------------
|
||||
class Snooper:
|
||||
"""
|
||||
Base class for snooper implementations.
|
||||
|
||||
A snooper is an object that will be provided with HCI packets as they are
|
||||
exchanged between a host and a controller.
|
||||
"""
|
||||
|
||||
class Direction(IntEnum):
|
||||
HOST_TO_CONTROLLER = 0
|
||||
CONTROLLER_TO_HOST = 1
|
||||
|
||||
class DataLinkType(IntEnum):
|
||||
H1 = 1001
|
||||
H4 = 1002
|
||||
HCI_BSCP = 1003
|
||||
H5 = 1004
|
||||
|
||||
def snoop(self, hci_packet: bytes, direction: Direction) -> None:
|
||||
"""Snoop on an HCI packet."""
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class BtSnooper(Snooper):
|
||||
"""
|
||||
Snooper that saves HCI packets using the BTSnoop format, based on RFC 1761.
|
||||
"""
|
||||
|
||||
IDENTIFICATION_PATTERN = b'btsnoop\0'
|
||||
TIMESTAMP_ANCHOR = datetime.datetime(2000, 1, 1)
|
||||
TIMESTAMP_DELTA = 0x00E03AB44A676000
|
||||
ONE_MS = datetime.timedelta(microseconds=1)
|
||||
|
||||
def __init__(self, output: BinaryIO):
|
||||
self.output = output
|
||||
|
||||
# Write the header
|
||||
self.output.write(
|
||||
self.IDENTIFICATION_PATTERN + struct.pack('>LL', 1, self.DataLinkType.H4)
|
||||
)
|
||||
|
||||
def snoop(self, hci_packet: bytes, direction: Snooper.Direction) -> None:
|
||||
flags = int(direction)
|
||||
packet_type = hci_packet[0]
|
||||
if packet_type in (HCI_EVENT_PACKET, HCI_COMMAND_PACKET):
|
||||
flags |= 0x10
|
||||
|
||||
# Compute the current timestamp
|
||||
timestamp = (
|
||||
int((datetime.datetime.utcnow() - self.TIMESTAMP_ANCHOR) / self.ONE_MS)
|
||||
+ self.TIMESTAMP_DELTA
|
||||
)
|
||||
|
||||
# Emit the record
|
||||
self.output.write(
|
||||
struct.pack(
|
||||
'>IIIIQ',
|
||||
len(hci_packet), # Original Length
|
||||
len(hci_packet), # Included Length
|
||||
flags, # Packet Flags
|
||||
0, # Cumulative Drops
|
||||
timestamp, # Timestamp
|
||||
)
|
||||
+ hci_packet
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
_SNOOPER_INSTANCE_COUNT = 0
|
||||
|
||||
|
||||
@contextmanager
|
||||
def create_snooper(spec: str) -> Generator[Snooper, None, None]:
|
||||
"""
|
||||
Create a snooper given a specification string.
|
||||
|
||||
The general syntax for the specification string is:
|
||||
<snooper-type>:<type-specific-arguments>
|
||||
|
||||
Supported snooper types are:
|
||||
|
||||
btsnoop
|
||||
The syntax for the type-specific arguments for this type is:
|
||||
<io-type>:<io-type-specific-arguments>
|
||||
|
||||
Supported I/O types are:
|
||||
|
||||
file
|
||||
The type-specific arguments for this I/O type is a string that is converted
|
||||
to a file path using the python `str.format()` string formatting. The log
|
||||
records will be written to that file if it can be opened/created.
|
||||
The keyword args that may be referenced by the string pattern are:
|
||||
now: the value of `datetime.now()`
|
||||
utcnow: the value of `datetime.utcnow()`
|
||||
pid: the current process ID.
|
||||
instance: the instance ID in the current process.
|
||||
|
||||
Examples:
|
||||
btsnoop:file:my_btsnoop.log
|
||||
btsnoop:file:/tmp/bumble_{now:%Y-%m-%d-%H:%M:%S}_{pid}.log
|
||||
|
||||
"""
|
||||
if ':' not in spec:
|
||||
raise ValueError('snooper type prefix missing')
|
||||
|
||||
snooper_type, snooper_args = spec.split(':', maxsplit=1)
|
||||
|
||||
if snooper_type == 'btsnoop':
|
||||
if ':' not in snooper_args:
|
||||
raise ValueError('I/O type for btsnoop snooper type missing')
|
||||
|
||||
io_type, io_name = snooper_args.split(':', maxsplit=1)
|
||||
if io_type == 'file':
|
||||
# Process the file name string pattern.
|
||||
global _SNOOPER_INSTANCE_COUNT
|
||||
file_path = io_name.format(
|
||||
now=datetime.datetime.now(),
|
||||
utcnow=datetime.datetime.utcnow(),
|
||||
pid=os.getpid(),
|
||||
instance=_SNOOPER_INSTANCE_COUNT,
|
||||
)
|
||||
|
||||
# Open the file
|
||||
logger.debug(f'Snoop file: {file_path}')
|
||||
with open(file_path, 'wb') as snoop_file:
|
||||
_SNOOPER_INSTANCE_COUNT += 1
|
||||
yield BtSnooper(snoop_file)
|
||||
_SNOOPER_INSTANCE_COUNT -= 1
|
||||
return
|
||||
|
||||
raise ValueError(f'I/O type {io_type} not supported')
|
||||
|
||||
raise ValueError(f'snooper type {snooper_type} not found')
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
# Copyright 2021-2023 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -15,11 +15,13 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from contextlib import asynccontextmanager
|
||||
import logging
|
||||
import os
|
||||
|
||||
from .common import Transport, AsyncPipeSink
|
||||
from ..link import RemoteLink
|
||||
from .common import Transport, AsyncPipeSink, SnoopingTransport
|
||||
from ..controller import Controller
|
||||
from ..snoop import create_snooper
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -28,68 +30,153 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_transport(name):
|
||||
'''
|
||||
Open a transport by name.
|
||||
The name must be <type>:<parameters>
|
||||
Where <parameters> depend on the type (and may be empty for some types).
|
||||
The supported types are: serial,udp,tcp,pty,usb
|
||||
'''
|
||||
scheme, *spec = name.split(':', 1)
|
||||
if scheme == 'serial' and spec:
|
||||
from .serial import open_serial_transport
|
||||
return await open_serial_transport(spec[0])
|
||||
elif scheme == 'udp' and spec:
|
||||
from .udp import open_udp_transport
|
||||
return await open_udp_transport(spec[0])
|
||||
elif scheme == 'tcp-client' and spec:
|
||||
from .tcp_client import open_tcp_client_transport
|
||||
return await open_tcp_client_transport(spec[0])
|
||||
elif scheme == 'tcp-server' and spec:
|
||||
from .tcp_server import open_tcp_server_transport
|
||||
return await open_tcp_server_transport(spec[0])
|
||||
elif scheme == 'ws-client' and spec:
|
||||
from .ws_client import open_ws_client_transport
|
||||
return await open_ws_client_transport(spec[0])
|
||||
elif scheme == 'ws-server' and spec:
|
||||
from .ws_server import open_ws_server_transport
|
||||
return await open_ws_server_transport(spec[0])
|
||||
elif scheme == 'pty':
|
||||
from .pty import open_pty_transport
|
||||
return await open_pty_transport(spec[0] if spec else None)
|
||||
elif scheme == 'file':
|
||||
from .file import open_file_transport
|
||||
return await open_file_transport(spec[0] if spec else None)
|
||||
elif scheme == 'vhci':
|
||||
from .vhci import open_vhci_transport
|
||||
return await open_vhci_transport(spec[0] if spec else None)
|
||||
elif scheme == 'hci-socket':
|
||||
from .hci_socket import open_hci_socket_transport
|
||||
return await open_hci_socket_transport(spec[0] if spec else None)
|
||||
elif scheme == 'usb':
|
||||
from .usb import open_usb_transport
|
||||
return await open_usb_transport(spec[0] if spec else None)
|
||||
elif scheme == 'pyusb':
|
||||
from .pyusb import open_pyusb_transport
|
||||
return await open_pyusb_transport(spec[0] if spec else None)
|
||||
elif scheme == 'android-emulator':
|
||||
from .android_emulator import open_android_emulator_transport
|
||||
return await open_android_emulator_transport(spec[0] if spec else None)
|
||||
else:
|
||||
raise ValueError('unknown transport scheme')
|
||||
def _wrap_transport(transport: Transport) -> Transport:
|
||||
"""
|
||||
Automatically wrap a Transport instance when a wrapping class can be inferred
|
||||
from the environment.
|
||||
If no wrapping class is applicable, the transport argument is returned as-is.
|
||||
"""
|
||||
|
||||
# If BUMBLE_SNOOPER is set, try to automatically create a snooper.
|
||||
if snooper_spec := os.getenv('BUMBLE_SNOOPER'):
|
||||
try:
|
||||
return SnoopingTransport.create_with(
|
||||
transport, create_snooper(snooper_spec)
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(f'Exception while creating snooper: {exc}')
|
||||
|
||||
return transport
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_transport_or_link(name):
|
||||
async def open_transport(name: str) -> Transport:
|
||||
"""
|
||||
Open a transport by name.
|
||||
The name must be <type>:<parameters>
|
||||
Where <parameters> depend on the type (and may be empty for some types).
|
||||
The supported types are:
|
||||
* serial
|
||||
* udp
|
||||
* tcp-client
|
||||
* tcp-server
|
||||
* ws-client
|
||||
* ws-server
|
||||
* pty
|
||||
* file
|
||||
* vhci
|
||||
* hci-socket
|
||||
* usb
|
||||
* pyusb
|
||||
* android-emulator
|
||||
"""
|
||||
|
||||
return _wrap_transport(await _open_transport(name))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def _open_transport(name: str) -> Transport:
|
||||
# pylint: disable=import-outside-toplevel
|
||||
# pylint: disable=too-many-return-statements
|
||||
|
||||
scheme, *spec = name.split(':', 1)
|
||||
if scheme == 'serial' and spec:
|
||||
from .serial import open_serial_transport
|
||||
|
||||
return await open_serial_transport(spec[0])
|
||||
|
||||
if scheme == 'udp' and spec:
|
||||
from .udp import open_udp_transport
|
||||
|
||||
return await open_udp_transport(spec[0])
|
||||
|
||||
if scheme == 'tcp-client' and spec:
|
||||
from .tcp_client import open_tcp_client_transport
|
||||
|
||||
return await open_tcp_client_transport(spec[0])
|
||||
|
||||
if scheme == 'tcp-server' and spec:
|
||||
from .tcp_server import open_tcp_server_transport
|
||||
|
||||
return await open_tcp_server_transport(spec[0])
|
||||
|
||||
if scheme == 'ws-client' and spec:
|
||||
from .ws_client import open_ws_client_transport
|
||||
|
||||
return await open_ws_client_transport(spec[0])
|
||||
|
||||
if scheme == 'ws-server' and spec:
|
||||
from .ws_server import open_ws_server_transport
|
||||
|
||||
return await open_ws_server_transport(spec[0])
|
||||
|
||||
if scheme == 'pty':
|
||||
from .pty import open_pty_transport
|
||||
|
||||
return await open_pty_transport(spec[0] if spec else None)
|
||||
|
||||
if scheme == 'file':
|
||||
from .file import open_file_transport
|
||||
|
||||
return await open_file_transport(spec[0] if spec else None)
|
||||
|
||||
if scheme == 'vhci':
|
||||
from .vhci import open_vhci_transport
|
||||
|
||||
return await open_vhci_transport(spec[0] if spec else None)
|
||||
|
||||
if scheme == 'hci-socket':
|
||||
from .hci_socket import open_hci_socket_transport
|
||||
|
||||
return await open_hci_socket_transport(spec[0] if spec else None)
|
||||
|
||||
if scheme == 'usb':
|
||||
from .usb import open_usb_transport
|
||||
|
||||
return await open_usb_transport(spec[0] if spec else None)
|
||||
|
||||
if scheme == 'pyusb':
|
||||
from .pyusb import open_pyusb_transport
|
||||
|
||||
return await open_pyusb_transport(spec[0] if spec else None)
|
||||
|
||||
if scheme == 'android-emulator':
|
||||
from .android_emulator import open_android_emulator_transport
|
||||
|
||||
return await open_android_emulator_transport(spec[0] if spec else None)
|
||||
|
||||
if scheme == 'android-netsim':
|
||||
from .android_netsim import open_android_netsim_transport
|
||||
|
||||
return await open_android_netsim_transport(spec[0] if spec else None)
|
||||
|
||||
raise ValueError('unknown transport scheme')
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_transport_or_link(name: str) -> Transport:
|
||||
"""
|
||||
Open a transport or a link relay.
|
||||
|
||||
Args:
|
||||
name:
|
||||
Name of the transport or link relay to open.
|
||||
When the name starts with "link-relay:", open a link relay (see RemoteLink
|
||||
for details on what the arguments are).
|
||||
For other namespaces, see `open_transport`.
|
||||
|
||||
"""
|
||||
if name.startswith('link-relay:'):
|
||||
from ..link import RemoteLink # lazy import
|
||||
|
||||
link = RemoteLink(name[11:])
|
||||
await link.wait_until_connected()
|
||||
controller = Controller('remote', link = link)
|
||||
controller = Controller('remote', link=link)
|
||||
|
||||
class LinkTransport(Transport):
|
||||
async def close(self):
|
||||
link.close()
|
||||
|
||||
return LinkTransport(controller, AsyncPipeSink(controller))
|
||||
else:
|
||||
return await open_transport(name)
|
||||
return _wrap_transport(LinkTransport(controller, AsyncPipeSink(controller)))
|
||||
|
||||
return await open_transport(name)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
# Copyright 2021-2023 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -16,12 +16,14 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import logging
|
||||
import grpc
|
||||
import grpc.aio
|
||||
|
||||
from .common import PumpedTransport, PumpedPacketSource, PumpedPacketSink
|
||||
from .emulated_bluetooth_pb2_grpc import EmulatedBluetoothServiceStub
|
||||
from .emulated_bluetooth_packets_pb2 import HCIPacket
|
||||
from .emulated_bluetooth_vhci_pb2_grpc import VhciForwardingServiceStub
|
||||
|
||||
# pylint: disable=no-name-in-module
|
||||
from .grpc_protobuf.emulated_bluetooth_pb2_grpc import EmulatedBluetoothServiceStub
|
||||
from .grpc_protobuf.emulated_bluetooth_packets_pb2 import HCIPacket
|
||||
from .grpc_protobuf.emulated_bluetooth_vhci_pb2_grpc import VhciForwardingServiceStub
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -59,15 +61,10 @@ async def open_android_emulator_transport(spec):
|
||||
return bytes([packet.type]) + packet.packet
|
||||
|
||||
async def write(self, packet):
|
||||
await self.hci_device.write(
|
||||
HCIPacket(
|
||||
type = packet[0],
|
||||
packet = packet[1:]
|
||||
)
|
||||
)
|
||||
await self.hci_device.write(HCIPacket(type=packet[0], packet=packet[1:]))
|
||||
|
||||
# Parse the parameters
|
||||
mode = 'host'
|
||||
mode = 'host'
|
||||
server_host = 'localhost'
|
||||
server_port = 8554
|
||||
if spec is not None:
|
||||
@@ -100,7 +97,7 @@ async def open_android_emulator_transport(spec):
|
||||
transport = PumpedTransport(
|
||||
PumpedPacketSource(hci_device.read),
|
||||
PumpedPacketSink(hci_device.write),
|
||||
channel.close
|
||||
channel.close,
|
||||
)
|
||||
transport.start()
|
||||
|
||||
|
||||
410
bumble/transport/android_netsim.py
Normal file
410
bumble/transport/android_netsim.py
Normal file
@@ -0,0 +1,410 @@
|
||||
# Copyright 2021-2023 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import atexit
|
||||
import logging
|
||||
import grpc.aio
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
from .common import (
|
||||
ParserSource,
|
||||
PumpedTransport,
|
||||
PumpedPacketSource,
|
||||
PumpedPacketSink,
|
||||
Transport,
|
||||
)
|
||||
|
||||
# pylint: disable=no-name-in-module
|
||||
from .grpc_protobuf.packet_streamer_pb2_grpc import PacketStreamerStub
|
||||
from .grpc_protobuf.packet_streamer_pb2_grpc import (
|
||||
PacketStreamerServicer,
|
||||
add_PacketStreamerServicer_to_server,
|
||||
)
|
||||
from .grpc_protobuf.packet_streamer_pb2 import PacketRequest, PacketResponse
|
||||
from .grpc_protobuf.hci_packet_pb2 import HCIPacket
|
||||
from .grpc_protobuf.startup_pb2 import Chip, ChipInfo
|
||||
from .grpc_protobuf.common_pb2 import ChipKind
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
DEFAULT_NAME = 'bumble0'
|
||||
DEFAULT_MANUFACTURER = 'Bumble'
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def get_ini_dir() -> Optional[pathlib.Path]:
|
||||
if sys.platform == 'darwin':
|
||||
if tmpdir := os.getenv('TMPDIR', None):
|
||||
return pathlib.Path(tmpdir)
|
||||
if home := os.getenv('HOME', None):
|
||||
return pathlib.Path(home) / 'Library/Caches/TemporaryItems'
|
||||
elif sys.platform == 'linux':
|
||||
if xdg_runtime_dir := os.environ.get('XDG_RUNTIME_DIR', None):
|
||||
return pathlib.Path(xdg_runtime_dir)
|
||||
elif sys.platform == 'win32':
|
||||
if local_app_data_dir := os.environ.get('LOCALAPPDATA', None):
|
||||
return pathlib.Path(local_app_data_dir) / 'Temp'
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def find_grpc_port() -> int:
|
||||
if not (ini_dir := get_ini_dir()):
|
||||
logger.debug('no known directory for .ini file')
|
||||
return 0
|
||||
|
||||
ini_file = ini_dir / 'netsim.ini'
|
||||
if ini_file.is_file():
|
||||
logger.debug(f'Found .ini file at {ini_file}')
|
||||
with open(ini_file, 'r') as ini_file_data:
|
||||
for line in ini_file_data.readlines():
|
||||
if '=' in line:
|
||||
key, value = line.split('=')
|
||||
if key == 'grpc.port':
|
||||
logger.debug(f'gRPC port = {value}')
|
||||
return int(value)
|
||||
|
||||
# Not found
|
||||
return 0
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def publish_grpc_port(grpc_port) -> bool:
|
||||
if not (ini_dir := get_ini_dir()):
|
||||
logger.debug('no known directory for .ini file')
|
||||
return False
|
||||
|
||||
if not ini_dir.is_dir():
|
||||
logger.debug('ini directory does not exist')
|
||||
return False
|
||||
|
||||
ini_file = ini_dir / 'netsim.ini'
|
||||
try:
|
||||
ini_file.write_text(f'grpc.port={grpc_port}\n')
|
||||
logger.debug(f"published gRPC port at {ini_file}")
|
||||
|
||||
def cleanup():
|
||||
logger.debug("removing .ini file")
|
||||
ini_file.unlink()
|
||||
|
||||
atexit.register(cleanup)
|
||||
return True
|
||||
except OSError:
|
||||
logger.debug('failed to write to .ini file')
|
||||
return False
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_android_netsim_controller_transport(server_host, server_port):
|
||||
if not server_port:
|
||||
raise ValueError('invalid port')
|
||||
if server_host == '_' or not server_host:
|
||||
server_host = 'localhost'
|
||||
|
||||
if not publish_grpc_port(server_port):
|
||||
logger.warning("unable to publish gRPC port")
|
||||
|
||||
class HciDevice:
|
||||
def __init__(self, context, on_data_received):
|
||||
self.context = context
|
||||
self.on_data_received = on_data_received
|
||||
self.name = None
|
||||
self.loop = asyncio.get_running_loop()
|
||||
self.done = self.loop.create_future()
|
||||
self.task = self.loop.create_task(self.pump())
|
||||
|
||||
async def pump(self):
|
||||
try:
|
||||
await self.pump_loop()
|
||||
except asyncio.CancelledError:
|
||||
logger.debug('Pump task canceled')
|
||||
self.done.set_result(None)
|
||||
|
||||
async def pump_loop(self):
|
||||
while True:
|
||||
request = await self.context.read()
|
||||
if request == grpc.aio.EOF:
|
||||
logger.debug('End of request stream')
|
||||
self.done.set_result(None)
|
||||
return
|
||||
|
||||
# If we're not initialized yet, wait for a init packet.
|
||||
if self.name is None:
|
||||
if request.WhichOneof('request_type') == 'initial_info':
|
||||
logger.debug(f'Received initial info: {request}')
|
||||
|
||||
# We only accept BLUETOOTH
|
||||
if request.initial_info.chip.kind != ChipKind.BLUETOOTH:
|
||||
logger.warning('Unsupported chip type')
|
||||
error = PacketResponse(error='Unsupported chip type')
|
||||
await self.context.write(error)
|
||||
return
|
||||
|
||||
self.name = request.initial_info.name
|
||||
continue
|
||||
|
||||
# Expect a data packet
|
||||
request_type = request.WhichOneof('request_type')
|
||||
if request_type != 'hci_packet':
|
||||
logger.warning(f'Unexpected request type: {request_type}')
|
||||
error = PacketResponse(error='Unexpected request type')
|
||||
await self.context.write(error)
|
||||
continue
|
||||
|
||||
# Process the packet
|
||||
data = (
|
||||
bytes([request.hci_packet.packet_type]) + request.hci_packet.packet
|
||||
)
|
||||
logger.debug(f'<<< PACKET: {data.hex()}')
|
||||
self.on_data_received(data)
|
||||
|
||||
def send_packet(self, data):
|
||||
async def send():
|
||||
await self.context.write(
|
||||
PacketResponse(
|
||||
hci_packet=HCIPacket(packet_type=data[0], packet=data[1:])
|
||||
)
|
||||
)
|
||||
|
||||
self.loop.create_task(send())
|
||||
|
||||
def terminate(self):
|
||||
self.task.cancel()
|
||||
|
||||
async def wait_for_termination(self):
|
||||
await self.done
|
||||
|
||||
class Server(PacketStreamerServicer, ParserSource):
|
||||
def __init__(self):
|
||||
PacketStreamerServicer.__init__(self)
|
||||
ParserSource.__init__(self)
|
||||
self.device = None
|
||||
|
||||
# Create a gRPC server with `so_reuseport=0` so that if there's already
|
||||
# a server listening on that port, we get an exception.
|
||||
self.grpc_server = grpc.aio.server(options=(('grpc.so_reuseport', 0),))
|
||||
add_PacketStreamerServicer_to_server(self, self.grpc_server)
|
||||
self.grpc_server.add_insecure_port(f'{server_host}:{server_port}')
|
||||
logger.debug(f'gRPC server listening on {server_host}:{server_port}')
|
||||
|
||||
async def start(self):
|
||||
logger.debug('Starting gRPC server')
|
||||
await self.grpc_server.start()
|
||||
|
||||
async def serve(self):
|
||||
# Keep serving until terminated.
|
||||
try:
|
||||
await self.grpc_server.wait_for_termination()
|
||||
logger.debug('gRPC server terminated')
|
||||
except asyncio.CancelledError:
|
||||
logger.debug('gRPC server cancelled')
|
||||
await self.grpc_server.stop(None)
|
||||
|
||||
def on_packet(self, packet):
|
||||
if not self.device:
|
||||
logger.debug('no device, dropping packet')
|
||||
return
|
||||
|
||||
self.device.send_packet(packet)
|
||||
|
||||
async def StreamPackets(self, _request_iterator, context):
|
||||
logger.debug('StreamPackets request')
|
||||
|
||||
# Check that we won't already have a device
|
||||
if self.device:
|
||||
logger.debug('busy, already serving a device')
|
||||
return PacketResponse(error='Busy')
|
||||
|
||||
# Instantiate a new device
|
||||
self.device = HciDevice(context, self.parser.feed_data)
|
||||
|
||||
# Wait for the device to terminate
|
||||
logger.debug('Waiting for device to terminate')
|
||||
try:
|
||||
await self.device.wait_for_termination()
|
||||
except asyncio.CancelledError:
|
||||
logger.debug('Request canceled')
|
||||
self.device.terminate()
|
||||
|
||||
logger.debug('Device terminated')
|
||||
self.device = None
|
||||
|
||||
server = Server()
|
||||
await server.start()
|
||||
asyncio.get_running_loop().create_task(server.serve())
|
||||
|
||||
class GrpcServerTransport(Transport):
|
||||
async def close(self):
|
||||
await super().close()
|
||||
|
||||
return GrpcServerTransport(server, server)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_android_netsim_host_transport(server_host, server_port, options):
|
||||
# Wrapper for I/O operations
|
||||
class HciDevice:
|
||||
def __init__(self, name, manufacturer, hci_device):
|
||||
self.name = name
|
||||
self.manufacturer = manufacturer
|
||||
self.hci_device = hci_device
|
||||
|
||||
async def start(self): # Send the startup info
|
||||
chip_info = ChipInfo(
|
||||
name=self.name,
|
||||
chip=Chip(kind=ChipKind.BLUETOOTH, manufacturer=self.manufacturer),
|
||||
)
|
||||
logger.debug(f'Sending chip info to netsim: {chip_info}')
|
||||
await self.hci_device.write(PacketRequest(initial_info=chip_info))
|
||||
|
||||
async def read(self):
|
||||
response = await self.hci_device.read()
|
||||
response_type = response.WhichOneof('response_type')
|
||||
if response_type == 'error':
|
||||
logger.warning(f'received error: {response.error}')
|
||||
raise RuntimeError(response.error)
|
||||
elif response_type == 'hci_packet':
|
||||
return (
|
||||
bytes([response.hci_packet.packet_type])
|
||||
+ response.hci_packet.packet
|
||||
)
|
||||
|
||||
raise ValueError('unsupported response type')
|
||||
|
||||
async def write(self, packet):
|
||||
await self.hci_device.write(
|
||||
PacketRequest(
|
||||
hci_packet=HCIPacket(packet_type=packet[0], packet=packet[1:])
|
||||
)
|
||||
)
|
||||
|
||||
name = options.get('name', DEFAULT_NAME)
|
||||
manufacturer = DEFAULT_MANUFACTURER
|
||||
|
||||
if server_host == '_' or not server_host:
|
||||
server_host = 'localhost'
|
||||
|
||||
if not server_port:
|
||||
# Look for the gRPC config in a .ini file
|
||||
server_host = 'localhost'
|
||||
server_port = find_grpc_port()
|
||||
if not server_port:
|
||||
raise RuntimeError('gRPC server port not found')
|
||||
|
||||
# Connect to the gRPC server
|
||||
server_address = f'{server_host}:{server_port}'
|
||||
logger.debug(f'Connecting to gRPC server at {server_address}')
|
||||
channel = grpc.aio.insecure_channel(server_address)
|
||||
|
||||
# Connect as a host
|
||||
service = PacketStreamerStub(channel)
|
||||
hci_device = HciDevice(
|
||||
name=name,
|
||||
manufacturer=manufacturer,
|
||||
hci_device=service.StreamPackets(),
|
||||
)
|
||||
await hci_device.start()
|
||||
|
||||
# Create the transport object
|
||||
transport = PumpedTransport(
|
||||
PumpedPacketSource(hci_device.read),
|
||||
PumpedPacketSink(hci_device.write),
|
||||
channel.close,
|
||||
)
|
||||
transport.start()
|
||||
|
||||
return transport
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_android_netsim_transport(spec):
|
||||
'''
|
||||
Open a transport connection as a client or server, implementing Android's `netsim`
|
||||
simulator protocol over gRPC.
|
||||
The parameter string has this syntax:
|
||||
[<host>:<port>][<options>]
|
||||
Where <options> is a ','-separated list of <name>=<value> pairs.
|
||||
|
||||
General options:
|
||||
mode=host|controller (default: host)
|
||||
Specifies whether the transport is used
|
||||
to connect *to* a netsim server (netsim is the controller), or accept
|
||||
connections *as* a netsim-compatible server.
|
||||
|
||||
In `host` mode:
|
||||
The <host>:<port> part is optional. When not specified, the transport
|
||||
looks for a netsim .ini file, from which it will read the `grpc.backend.port`
|
||||
property.
|
||||
Options for this mode are:
|
||||
name=<name>
|
||||
The "chip" name, used to identify the "chip" instance. This
|
||||
may be useful when several clients are connected, since each needs to use a
|
||||
different name.
|
||||
|
||||
In `controller` mode:
|
||||
The <host>:<port> part is required. <host> may be the address of a local network
|
||||
interface, or '_' to accept connections on all local network interfaces.
|
||||
|
||||
Examples:
|
||||
(empty string) --> connect to netsim on the port specified in the .ini file
|
||||
localhost:8555 --> connect to netsim on localhost:8555
|
||||
name=bumble1 --> connect to netsim, using `bumble1` as the "chip" name.
|
||||
localhost:8555,name=bumble1 --> connect to netsim on localhost:8555, using
|
||||
`bumble1` as the "chip" name.
|
||||
_:8877,mode=controller --> accept connections as a controller on any interface
|
||||
on port 8877.
|
||||
'''
|
||||
|
||||
# Parse the parameters
|
||||
params = spec.split(',') if spec else []
|
||||
if params and ':' in params[0]:
|
||||
# Explicit <host>:<port>
|
||||
host, port = params[0].split(':')
|
||||
params_offset = 1
|
||||
else:
|
||||
host = None
|
||||
port = 0
|
||||
params_offset = 0
|
||||
|
||||
options = {}
|
||||
for param in params[params_offset:]:
|
||||
if '=' not in param:
|
||||
raise ValueError('invalid parameter, expected <name>=<value>')
|
||||
option_name, option_value = param.split('=')
|
||||
options[option_name] = option_value
|
||||
|
||||
mode = options.get('mode', 'host')
|
||||
if mode == 'host':
|
||||
return await open_android_netsim_host_transport(host, port, options)
|
||||
if mode == 'controller':
|
||||
if host is None:
|
||||
raise ValueError('<host>:<port> missing')
|
||||
return await open_android_netsim_controller_transport(host, port)
|
||||
|
||||
raise ValueError('invalid mode option')
|
||||
@@ -15,12 +15,16 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
import contextlib
|
||||
import struct
|
||||
import asyncio
|
||||
import logging
|
||||
from colors import color
|
||||
from typing import ContextManager
|
||||
|
||||
from .. import hci
|
||||
from ..colors import color
|
||||
from ..snoop import Snooper
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -33,10 +37,10 @@ logger = logging.getLogger(__name__)
|
||||
# For each packet type, the info represents:
|
||||
# (length-size, length-offset, unpack-type)
|
||||
HCI_PACKET_INFO = {
|
||||
hci.HCI_COMMAND_PACKET: (1, 2, 'B'),
|
||||
hci.HCI_ACL_DATA_PACKET: (2, 2, 'H'),
|
||||
hci.HCI_COMMAND_PACKET: (1, 2, 'B'),
|
||||
hci.HCI_ACL_DATA_PACKET: (2, 2, 'H'),
|
||||
hci.HCI_SYNCHRONOUS_DATA_PACKET: (1, 2, 'B'),
|
||||
hci.HCI_EVENT_PACKET: (1, 1, 'B')
|
||||
hci.HCI_EVENT_PACKET: (1, 1, 'B'),
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +52,7 @@ class PacketPump:
|
||||
|
||||
def __init__(self, reader, sink):
|
||||
self.reader = reader
|
||||
self.sink = sink
|
||||
self.sink = sink
|
||||
|
||||
async def run(self):
|
||||
while True:
|
||||
@@ -65,43 +69,51 @@ class PacketPump:
|
||||
# -----------------------------------------------------------------------------
|
||||
class PacketParser:
|
||||
'''
|
||||
In-line parser that accepts data and emits 'on_packet' when a full packet has been parsed
|
||||
In-line parser that accepts data and emits 'on_packet' when a full packet has been
|
||||
parsed
|
||||
'''
|
||||
NEED_TYPE = 0
|
||||
NEED_LENGTH = 1
|
||||
NEED_BODY = 2
|
||||
|
||||
def __init__(self, sink = None):
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
|
||||
NEED_TYPE = 0
|
||||
NEED_LENGTH = 1
|
||||
NEED_BODY = 2
|
||||
|
||||
def __init__(self, sink=None):
|
||||
self.sink = sink
|
||||
self.extended_packet_info = {}
|
||||
self.reset()
|
||||
|
||||
def reset(self):
|
||||
self.state = PacketParser.NEED_TYPE
|
||||
self.state = PacketParser.NEED_TYPE
|
||||
self.bytes_needed = 1
|
||||
self.packet = bytearray()
|
||||
self.packet_info = None
|
||||
self.packet = bytearray()
|
||||
self.packet_info = None
|
||||
|
||||
def feed_data(self, data):
|
||||
data_offset = 0
|
||||
data_left = len(data)
|
||||
while data_left and self.bytes_needed:
|
||||
consumed = min(self.bytes_needed, data_left)
|
||||
self.packet.extend(data[data_offset:data_offset + consumed])
|
||||
data_offset += consumed
|
||||
data_left -= consumed
|
||||
self.packet.extend(data[data_offset : data_offset + consumed])
|
||||
data_offset += consumed
|
||||
data_left -= consumed
|
||||
self.bytes_needed -= consumed
|
||||
|
||||
if self.bytes_needed == 0:
|
||||
if self.state == PacketParser.NEED_TYPE:
|
||||
packet_type = self.packet[0]
|
||||
self.packet_info = HCI_PACKET_INFO.get(packet_type) or self.extended_packet_info.get(packet_type)
|
||||
self.packet_info = HCI_PACKET_INFO.get(
|
||||
packet_type
|
||||
) or self.extended_packet_info.get(packet_type)
|
||||
if self.packet_info is None:
|
||||
raise ValueError(f'invalid packet type {packet_type}')
|
||||
self.state = PacketParser.NEED_LENGTH
|
||||
self.bytes_needed = self.packet_info[0] + self.packet_info[1]
|
||||
elif self.state == PacketParser.NEED_LENGTH:
|
||||
body_length = struct.unpack_from(self.packet_info[2], self.packet, 1 + self.packet_info[1])[0]
|
||||
body_length = struct.unpack_from(
|
||||
self.packet_info[2], self.packet, 1 + self.packet_info[1]
|
||||
)[0]
|
||||
self.bytes_needed = body_length
|
||||
self.state = PacketParser.NEED_BODY
|
||||
|
||||
@@ -111,7 +123,9 @@ class PacketParser:
|
||||
try:
|
||||
self.sink.on_packet(bytes(self.packet))
|
||||
except Exception as error:
|
||||
logger.warning(color(f'!!! Exception in on_packet: {error}', 'red'))
|
||||
logger.warning(
|
||||
color(f'!!! Exception in on_packet: {error}', 'red')
|
||||
)
|
||||
self.reset()
|
||||
|
||||
def set_packet_sink(self, sink):
|
||||
@@ -187,6 +201,7 @@ class AsyncPipeSink:
|
||||
'''
|
||||
Sink that forwards packets asynchronously to another sink
|
||||
'''
|
||||
|
||||
def __init__(self, sink):
|
||||
self.sink = sink
|
||||
self.loop = asyncio.get_running_loop()
|
||||
@@ -202,7 +217,7 @@ class ParserSource:
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.parser = PacketParser()
|
||||
self.parser = PacketParser()
|
||||
self.terminated = asyncio.get_running_loop().create_future()
|
||||
|
||||
def set_packet_sink(self, sink):
|
||||
@@ -235,9 +250,23 @@ class StreamPacketSink:
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Transport:
|
||||
"""
|
||||
Base class for all transports.
|
||||
|
||||
A Transport represents a source and a sink together.
|
||||
An instance must be closed by calling close() when no longer used. Instances
|
||||
implement the ContextManager protocol so that they may be used in a `async with`
|
||||
statement.
|
||||
An instance is iterable. The iterator yields, in order, its source and sink, so
|
||||
that it may be used with a convenient call syntax like:
|
||||
|
||||
async with create_transport() as (source, sink):
|
||||
...
|
||||
"""
|
||||
|
||||
def __init__(self, source, sink):
|
||||
self.source = source
|
||||
self.sink = sink
|
||||
self.sink = sink
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
@@ -248,7 +277,7 @@ class Transport:
|
||||
def __iter__(self):
|
||||
return iter((self.source, self.sink))
|
||||
|
||||
async def close(self):
|
||||
async def close(self) -> None:
|
||||
self.source.close()
|
||||
self.sink.close()
|
||||
|
||||
@@ -258,7 +287,7 @@ class PumpedPacketSource(ParserSource):
|
||||
def __init__(self, receive):
|
||||
super().__init__()
|
||||
self.receive_function = receive
|
||||
self.pump_task = None
|
||||
self.pump_task = None
|
||||
|
||||
def start(self):
|
||||
async def pump_packets():
|
||||
@@ -270,11 +299,11 @@ class PumpedPacketSource(ParserSource):
|
||||
logger.debug('source pump task done')
|
||||
break
|
||||
except Exception as error:
|
||||
logger.warn(f'exception while waiting for packet: {error}')
|
||||
logger.warning(f'exception while waiting for packet: {error}')
|
||||
self.terminated.set_result(error)
|
||||
break
|
||||
|
||||
self.pump_task = asyncio.get_running_loop().create_task(pump_packets())
|
||||
self.pump_task = asyncio.create_task(pump_packets())
|
||||
|
||||
def close(self):
|
||||
if self.pump_task:
|
||||
@@ -285,8 +314,8 @@ class PumpedPacketSource(ParserSource):
|
||||
class PumpedPacketSink:
|
||||
def __init__(self, send):
|
||||
self.send_function = send
|
||||
self.packet_queue = asyncio.Queue()
|
||||
self.pump_task = None
|
||||
self.packet_queue = asyncio.Queue()
|
||||
self.pump_task = None
|
||||
|
||||
def on_packet(self, packet):
|
||||
self.packet_queue.put_nowait(packet)
|
||||
@@ -301,10 +330,10 @@ class PumpedPacketSink:
|
||||
logger.debug('sink pump task done')
|
||||
break
|
||||
except Exception as error:
|
||||
logger.warn(f'exception while sending packet: {error}')
|
||||
logger.warning(f'exception while sending packet: {error}')
|
||||
break
|
||||
|
||||
self.pump_task = asyncio.get_running_loop().create_task(pump_packets())
|
||||
self.pump_task = asyncio.create_task(pump_packets())
|
||||
|
||||
def close(self):
|
||||
if self.pump_task:
|
||||
@@ -324,3 +353,60 @@ class PumpedTransport(Transport):
|
||||
async def close(self):
|
||||
await super().close()
|
||||
await self.close_function()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class SnoopingTransport(Transport):
|
||||
"""Transport wrapper that snoops on packets to/from a wrapped transport."""
|
||||
|
||||
@staticmethod
|
||||
def create_with(
|
||||
transport: Transport, snooper: ContextManager[Snooper]
|
||||
) -> SnoopingTransport:
|
||||
"""
|
||||
Create an instance given a snooper that works as as context manager.
|
||||
|
||||
The returned instance will exit the snooper context when it is closed.
|
||||
"""
|
||||
with contextlib.ExitStack() as exit_stack:
|
||||
return SnoopingTransport(
|
||||
transport, exit_stack.enter_context(snooper), exit_stack.pop_all().close
|
||||
)
|
||||
raise RuntimeError('unexpected code path') # Satisfy the type checker
|
||||
|
||||
class Source:
|
||||
def __init__(self, source, snooper):
|
||||
self.source = source
|
||||
self.snooper = snooper
|
||||
self.sink = None
|
||||
|
||||
def set_packet_sink(self, sink):
|
||||
self.sink = sink
|
||||
self.source.set_packet_sink(self)
|
||||
|
||||
def on_packet(self, packet):
|
||||
self.snooper.snoop(packet, Snooper.Direction.CONTROLLER_TO_HOST)
|
||||
if self.sink:
|
||||
self.sink.on_packet(packet)
|
||||
|
||||
class Sink:
|
||||
def __init__(self, sink, snooper):
|
||||
self.sink = sink
|
||||
self.snooper = snooper
|
||||
|
||||
def on_packet(self, packet):
|
||||
self.snooper.snoop(packet, Snooper.Direction.HOST_TO_CONTROLLER)
|
||||
if self.sink:
|
||||
self.sink.on_packet(packet)
|
||||
|
||||
def __init__(self, transport, snooper, close_snooper=None):
|
||||
super().__init__(
|
||||
self.Source(transport.source, snooper), self.Sink(transport.sink, snooper)
|
||||
)
|
||||
self.transport = transport
|
||||
self.close_snooper = close_snooper
|
||||
|
||||
async def close(self):
|
||||
await self.transport.close()
|
||||
if self.close_snooper:
|
||||
self.close_snooper()
|
||||
|
||||
@@ -1,53 +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.
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: emulated_bluetooth.proto
|
||||
"""Generated protocol buffer code."""
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||
from google.protobuf import message as _message
|
||||
from google.protobuf import reflection as _reflection
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
from . import emulated_bluetooth_packets_pb2 as emulated__bluetooth__packets__pb2
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18\x65mulated_bluetooth.proto\x12\x1b\x61ndroid.emulation.bluetooth\x1a emulated_bluetooth_packets.proto\"\x19\n\x07RawData\x12\x0e\n\x06packet\x18\x01 \x01(\x0c\x32\xcb\x02\n\x18\x45mulatedBluetoothService\x12\x64\n\x12registerClassicPhy\x12$.android.emulation.bluetooth.RawData\x1a$.android.emulation.bluetooth.RawData(\x01\x30\x01\x12`\n\x0eregisterBlePhy\x12$.android.emulation.bluetooth.RawData\x1a$.android.emulation.bluetooth.RawData(\x01\x30\x01\x12g\n\x11registerHCIDevice\x12&.android.emulation.bluetooth.HCIPacket\x1a&.android.emulation.bluetooth.HCIPacket(\x01\x30\x01\x42\"\n\x1e\x63om.android.emulator.bluetoothP\x01\x62\x06proto3')
|
||||
|
||||
|
||||
|
||||
_RAWDATA = DESCRIPTOR.message_types_by_name['RawData']
|
||||
RawData = _reflection.GeneratedProtocolMessageType('RawData', (_message.Message,), {
|
||||
'DESCRIPTOR' : _RAWDATA,
|
||||
'__module__' : 'emulated_bluetooth_pb2'
|
||||
# @@protoc_insertion_point(class_scope:android.emulation.bluetooth.RawData)
|
||||
})
|
||||
_sym_db.RegisterMessage(RawData)
|
||||
|
||||
_EMULATEDBLUETOOTHSERVICE = DESCRIPTOR.services_by_name['EmulatedBluetoothService']
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
|
||||
DESCRIPTOR._options = None
|
||||
DESCRIPTOR._serialized_options = b'\n\036com.android.emulator.bluetoothP\001'
|
||||
_RAWDATA._serialized_start=91
|
||||
_RAWDATA._serialized_end=116
|
||||
_EMULATEDBLUETOOTHSERVICE._serialized_start=119
|
||||
_EMULATEDBLUETOOTHSERVICE._serialized_end=450
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
@@ -30,8 +30,9 @@ logger = logging.getLogger(__name__)
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_file_transport(spec):
|
||||
'''
|
||||
Open a File transport (typically not for a real file, but for a PTY or other unix virtual files).
|
||||
The parameter string is the path of the file to open
|
||||
Open a File transport (typically not for a real file, but for a PTY or other unix
|
||||
virtual files).
|
||||
The parameter string is the path of the file to open.
|
||||
'''
|
||||
|
||||
# Open the file
|
||||
@@ -39,14 +40,12 @@ async def open_file_transport(spec):
|
||||
|
||||
# Setup reading
|
||||
read_transport, packet_source = await asyncio.get_running_loop().connect_read_pipe(
|
||||
lambda: StreamPacketSource(),
|
||||
file
|
||||
StreamPacketSource, file
|
||||
)
|
||||
|
||||
# Setup writing
|
||||
write_transport, _ = await asyncio.get_running_loop().connect_write_pipe(
|
||||
lambda: asyncio.BaseProtocol(),
|
||||
file
|
||||
asyncio.BaseProtocol, file
|
||||
)
|
||||
packet_sink = StreamPacketSink(write_transport)
|
||||
|
||||
@@ -57,4 +56,3 @@ async def open_file_transport(spec):
|
||||
file.close()
|
||||
|
||||
return FileTransport(packet_source, packet_sink)
|
||||
|
||||
|
||||
0
bumble/transport/grpc_protobuf/__init__.py
Normal file
0
bumble/transport/grpc_protobuf/__init__.py
Normal file
25
bumble/transport/grpc_protobuf/common_pb2.py
Normal file
25
bumble/transport/grpc_protobuf/common_pb2.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: common.proto
|
||||
"""Generated protocol buffer code."""
|
||||
from google.protobuf.internal import builder as _builder
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0c\x63ommon.proto\x12\rnetsim.common*=\n\x08\x43hipKind\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\r\n\tBLUETOOTH\x10\x01\x12\x08\n\x04WIFI\x10\x02\x12\x07\n\x03UWB\x10\x03\x62\x06proto3')
|
||||
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'common_pb2', globals())
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
|
||||
DESCRIPTOR._options = None
|
||||
_CHIPKIND._serialized_start=31
|
||||
_CHIPKIND._serialized_end=92
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
12
bumble/transport/grpc_protobuf/common_pb2.pyi
Normal file
12
bumble/transport/grpc_protobuf/common_pb2.pyi
Normal file
@@ -0,0 +1,12 @@
|
||||
from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from typing import ClassVar as _ClassVar
|
||||
|
||||
BLUETOOTH: ChipKind
|
||||
DESCRIPTOR: _descriptor.FileDescriptor
|
||||
UNSPECIFIED: ChipKind
|
||||
UWB: ChipKind
|
||||
WIFI: ChipKind
|
||||
|
||||
class ChipKind(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||
__slots__ = []
|
||||
4
bumble/transport/grpc_protobuf/common_pb2_grpc.py
Normal file
4
bumble/transport/grpc_protobuf/common_pb2_grpc.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
||||
"""Client and server classes corresponding to protobuf-defined services."""
|
||||
import grpc
|
||||
|
||||
File diff suppressed because one or more lines are too long
158
bumble/transport/grpc_protobuf/emulated_bluetooth_device_pb2.pyi
Normal file
158
bumble/transport/grpc_protobuf/emulated_bluetooth_device_pb2.pyi
Normal file
@@ -0,0 +1,158 @@
|
||||
from . import grpc_endpoint_description_pb2 as _grpc_endpoint_description_pb2
|
||||
from google.protobuf import empty_pb2 as _empty_pb2
|
||||
from google.protobuf.internal import containers as _containers
|
||||
from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import message as _message
|
||||
from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union
|
||||
|
||||
DESCRIPTOR: _descriptor.FileDescriptor
|
||||
|
||||
class Advertisement(_message.Message):
|
||||
__slots__ = ["connection_mode", "device_name", "discovery_mode"]
|
||||
class ConnectionMode(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||
__slots__ = []
|
||||
class DiscoveryMode(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||
__slots__ = []
|
||||
CONNECTION_MODE_DIRECTED: Advertisement.ConnectionMode
|
||||
CONNECTION_MODE_FIELD_NUMBER: _ClassVar[int]
|
||||
CONNECTION_MODE_NON_CONNECTABLE: Advertisement.ConnectionMode
|
||||
CONNECTION_MODE_UNDIRECTED: Advertisement.ConnectionMode
|
||||
CONNECTION_MODE_UNSPECIFIED: Advertisement.ConnectionMode
|
||||
DEVICE_NAME_FIELD_NUMBER: _ClassVar[int]
|
||||
DISCOVERY_MODE_FIELD_NUMBER: _ClassVar[int]
|
||||
DISCOVERY_MODE_GENERAL: Advertisement.DiscoveryMode
|
||||
DISCOVERY_MODE_LIMITED: Advertisement.DiscoveryMode
|
||||
DISCOVERY_MODE_NON_DISCOVERABLE: Advertisement.DiscoveryMode
|
||||
DISCOVERY_MODE_UNSPECIFIED: Advertisement.DiscoveryMode
|
||||
connection_mode: Advertisement.ConnectionMode
|
||||
device_name: str
|
||||
discovery_mode: Advertisement.DiscoveryMode
|
||||
def __init__(self, device_name: _Optional[str] = ..., connection_mode: _Optional[_Union[Advertisement.ConnectionMode, str]] = ..., discovery_mode: _Optional[_Union[Advertisement.DiscoveryMode, str]] = ...) -> None: ...
|
||||
|
||||
class CallbackIdentifier(_message.Message):
|
||||
__slots__ = ["identity"]
|
||||
IDENTITY_FIELD_NUMBER: _ClassVar[int]
|
||||
identity: str
|
||||
def __init__(self, identity: _Optional[str] = ...) -> None: ...
|
||||
|
||||
class CharacteristicValueRequest(_message.Message):
|
||||
__slots__ = ["callback_device_id", "callback_id", "data", "from_device"]
|
||||
CALLBACK_DEVICE_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
CALLBACK_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
DATA_FIELD_NUMBER: _ClassVar[int]
|
||||
FROM_DEVICE_FIELD_NUMBER: _ClassVar[int]
|
||||
callback_device_id: CallbackIdentifier
|
||||
callback_id: Uuid
|
||||
data: bytes
|
||||
from_device: DeviceIdentifier
|
||||
def __init__(self, callback_device_id: _Optional[_Union[CallbackIdentifier, _Mapping]] = ..., from_device: _Optional[_Union[DeviceIdentifier, _Mapping]] = ..., callback_id: _Optional[_Union[Uuid, _Mapping]] = ..., data: _Optional[bytes] = ...) -> None: ...
|
||||
|
||||
class CharacteristicValueResponse(_message.Message):
|
||||
__slots__ = ["data", "status"]
|
||||
class GattStatus(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||
__slots__ = []
|
||||
DATA_FIELD_NUMBER: _ClassVar[int]
|
||||
GATT_STATUS_FAILURE: CharacteristicValueResponse.GattStatus
|
||||
GATT_STATUS_SUCCESS: CharacteristicValueResponse.GattStatus
|
||||
GATT_STATUS_UNSPECIFIED: CharacteristicValueResponse.GattStatus
|
||||
STATUS_FIELD_NUMBER: _ClassVar[int]
|
||||
data: bytes
|
||||
status: CharacteristicValueResponse.GattStatus
|
||||
def __init__(self, status: _Optional[_Union[CharacteristicValueResponse.GattStatus, str]] = ..., data: _Optional[bytes] = ...) -> None: ...
|
||||
|
||||
class ConnectionStateChange(_message.Message):
|
||||
__slots__ = ["callback_device_id", "from_device", "new_state"]
|
||||
class ConnectionState(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||
__slots__ = []
|
||||
CALLBACK_DEVICE_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
CONNECTION_STATE_CONNECTED: ConnectionStateChange.ConnectionState
|
||||
CONNECTION_STATE_DISCONNECTED: ConnectionStateChange.ConnectionState
|
||||
CONNECTION_STATE_UNDEFINED: ConnectionStateChange.ConnectionState
|
||||
FROM_DEVICE_FIELD_NUMBER: _ClassVar[int]
|
||||
NEW_STATE_FIELD_NUMBER: _ClassVar[int]
|
||||
callback_device_id: CallbackIdentifier
|
||||
from_device: DeviceIdentifier
|
||||
new_state: ConnectionStateChange.ConnectionState
|
||||
def __init__(self, callback_device_id: _Optional[_Union[CallbackIdentifier, _Mapping]] = ..., from_device: _Optional[_Union[DeviceIdentifier, _Mapping]] = ..., new_state: _Optional[_Union[ConnectionStateChange.ConnectionState, str]] = ...) -> None: ...
|
||||
|
||||
class DeviceIdentifier(_message.Message):
|
||||
__slots__ = ["address"]
|
||||
ADDRESS_FIELD_NUMBER: _ClassVar[int]
|
||||
address: str
|
||||
def __init__(self, address: _Optional[str] = ...) -> None: ...
|
||||
|
||||
class GattCharacteristic(_message.Message):
|
||||
__slots__ = ["callback_id", "permissions", "properties", "uuid"]
|
||||
class Permissions(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||
__slots__ = []
|
||||
class Properties(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||
__slots__ = []
|
||||
CALLBACK_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
PERMISSIONS_FIELD_NUMBER: _ClassVar[int]
|
||||
PERMISSION_READ: GattCharacteristic.Permissions
|
||||
PERMISSION_READ_ENCRYPTED: GattCharacteristic.Permissions
|
||||
PERMISSION_READ_ENCRYPTED_MITM: GattCharacteristic.Permissions
|
||||
PERMISSION_UNSPECIFIED: GattCharacteristic.Permissions
|
||||
PERMISSION_WRITE: GattCharacteristic.Permissions
|
||||
PERMISSION_WRITE_ENCRYPTED: GattCharacteristic.Permissions
|
||||
PERMISSION_WRITE_ENCRYPTED_MITM: GattCharacteristic.Permissions
|
||||
PERMISSION_WRITE_SIGNED: GattCharacteristic.Permissions
|
||||
PERMISSION_WRITE_SIGNED_MITM: GattCharacteristic.Permissions
|
||||
PROPERTIES_FIELD_NUMBER: _ClassVar[int]
|
||||
PROPERTY_BROADCAST: GattCharacteristic.Properties
|
||||
PROPERTY_EXTENDED_PROPS: GattCharacteristic.Properties
|
||||
PROPERTY_INDICATE: GattCharacteristic.Properties
|
||||
PROPERTY_NOTIFY: GattCharacteristic.Properties
|
||||
PROPERTY_READ: GattCharacteristic.Properties
|
||||
PROPERTY_SIGNED_WRITE: GattCharacteristic.Properties
|
||||
PROPERTY_UNSPECIFIED: GattCharacteristic.Properties
|
||||
PROPERTY_WRITE: GattCharacteristic.Properties
|
||||
PROPERTY_WRITE_NO_RESPONSE: GattCharacteristic.Properties
|
||||
UUID_FIELD_NUMBER: _ClassVar[int]
|
||||
callback_id: Uuid
|
||||
permissions: int
|
||||
properties: int
|
||||
uuid: Uuid
|
||||
def __init__(self, uuid: _Optional[_Union[Uuid, _Mapping]] = ..., properties: _Optional[int] = ..., permissions: _Optional[int] = ..., callback_id: _Optional[_Union[Uuid, _Mapping]] = ...) -> None: ...
|
||||
|
||||
class GattDevice(_message.Message):
|
||||
__slots__ = ["advertisement", "endpoint", "profile"]
|
||||
ADVERTISEMENT_FIELD_NUMBER: _ClassVar[int]
|
||||
ENDPOINT_FIELD_NUMBER: _ClassVar[int]
|
||||
PROFILE_FIELD_NUMBER: _ClassVar[int]
|
||||
advertisement: Advertisement
|
||||
endpoint: _grpc_endpoint_description_pb2.Endpoint
|
||||
profile: GattProfile
|
||||
def __init__(self, endpoint: _Optional[_Union[_grpc_endpoint_description_pb2.Endpoint, _Mapping]] = ..., advertisement: _Optional[_Union[Advertisement, _Mapping]] = ..., profile: _Optional[_Union[GattProfile, _Mapping]] = ...) -> None: ...
|
||||
|
||||
class GattProfile(_message.Message):
|
||||
__slots__ = ["services"]
|
||||
SERVICES_FIELD_NUMBER: _ClassVar[int]
|
||||
services: _containers.RepeatedCompositeFieldContainer[GattService]
|
||||
def __init__(self, services: _Optional[_Iterable[_Union[GattService, _Mapping]]] = ...) -> None: ...
|
||||
|
||||
class GattService(_message.Message):
|
||||
__slots__ = ["characteristics", "service_type", "uuid"]
|
||||
class ServiceType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||
__slots__ = []
|
||||
CHARACTERISTICS_FIELD_NUMBER: _ClassVar[int]
|
||||
SERVICE_TYPE_FIELD_NUMBER: _ClassVar[int]
|
||||
SERVICE_TYPE_PRIMARY: GattService.ServiceType
|
||||
SERVICE_TYPE_SECONDARY: GattService.ServiceType
|
||||
SERVICE_TYPE_UNSPECIFIED: GattService.ServiceType
|
||||
UUID_FIELD_NUMBER: _ClassVar[int]
|
||||
characteristics: _containers.RepeatedCompositeFieldContainer[GattCharacteristic]
|
||||
service_type: GattService.ServiceType
|
||||
uuid: Uuid
|
||||
def __init__(self, uuid: _Optional[_Union[Uuid, _Mapping]] = ..., service_type: _Optional[_Union[GattService.ServiceType, str]] = ..., characteristics: _Optional[_Iterable[_Union[GattCharacteristic, _Mapping]]] = ...) -> None: ...
|
||||
|
||||
class Uuid(_message.Message):
|
||||
__slots__ = ["id", "lsb", "msb"]
|
||||
ID_FIELD_NUMBER: _ClassVar[int]
|
||||
LSB_FIELD_NUMBER: _ClassVar[int]
|
||||
MSB_FIELD_NUMBER: _ClassVar[int]
|
||||
id: int
|
||||
lsb: int
|
||||
msb: int
|
||||
def __init__(self, id: _Optional[int] = ..., lsb: _Optional[int] = ..., msb: _Optional[int] = ...) -> None: ...
|
||||
@@ -0,0 +1,193 @@
|
||||
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
||||
"""Client and server classes corresponding to protobuf-defined services."""
|
||||
import grpc
|
||||
|
||||
from . import emulated_bluetooth_device_pb2 as emulated__bluetooth__device__pb2
|
||||
from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2
|
||||
|
||||
|
||||
class GattDeviceServiceStub(object):
|
||||
"""You can provide your own GattDevice by implementing this service
|
||||
and registering it with the android emulator.
|
||||
|
||||
The device will appear as a real bluetooth device, and you will
|
||||
receive callbacks when the bluetooth system wants to
|
||||
read, write or observe a characteristic.
|
||||
"""
|
||||
|
||||
def __init__(self, channel):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
channel: A grpc.Channel.
|
||||
"""
|
||||
self.OnCharacteristicReadRequest = channel.unary_unary(
|
||||
'/android.emulation.bluetooth.GattDeviceService/OnCharacteristicReadRequest',
|
||||
request_serializer=emulated__bluetooth__device__pb2.CharacteristicValueRequest.SerializeToString,
|
||||
response_deserializer=emulated__bluetooth__device__pb2.CharacteristicValueResponse.FromString,
|
||||
)
|
||||
self.OnCharacteristicWriteRequest = channel.unary_unary(
|
||||
'/android.emulation.bluetooth.GattDeviceService/OnCharacteristicWriteRequest',
|
||||
request_serializer=emulated__bluetooth__device__pb2.CharacteristicValueRequest.SerializeToString,
|
||||
response_deserializer=emulated__bluetooth__device__pb2.CharacteristicValueResponse.FromString,
|
||||
)
|
||||
self.OnCharacteristicObserveRequest = channel.unary_stream(
|
||||
'/android.emulation.bluetooth.GattDeviceService/OnCharacteristicObserveRequest',
|
||||
request_serializer=emulated__bluetooth__device__pb2.CharacteristicValueRequest.SerializeToString,
|
||||
response_deserializer=emulated__bluetooth__device__pb2.CharacteristicValueResponse.FromString,
|
||||
)
|
||||
self.OnConnectionStateChange = channel.unary_unary(
|
||||
'/android.emulation.bluetooth.GattDeviceService/OnConnectionStateChange',
|
||||
request_serializer=emulated__bluetooth__device__pb2.ConnectionStateChange.SerializeToString,
|
||||
response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString,
|
||||
)
|
||||
|
||||
|
||||
class GattDeviceServiceServicer(object):
|
||||
"""You can provide your own GattDevice by implementing this service
|
||||
and registering it with the android emulator.
|
||||
|
||||
The device will appear as a real bluetooth device, and you will
|
||||
receive callbacks when the bluetooth system wants to
|
||||
read, write or observe a characteristic.
|
||||
"""
|
||||
|
||||
def OnCharacteristicReadRequest(self, request, context):
|
||||
"""A remote client has requested to read a local characteristic.
|
||||
|
||||
Return the current observed value.
|
||||
"""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def OnCharacteristicWriteRequest(self, request, context):
|
||||
"""A remote client has requested to write to a local characteristic.
|
||||
|
||||
Return the current observed value.
|
||||
"""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def OnCharacteristicObserveRequest(self, request, context):
|
||||
"""Listens for notifications from the emulated device, the device should
|
||||
write to the stream with a response when a change has occurred.
|
||||
"""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def OnConnectionStateChange(self, request, context):
|
||||
"""A remote device has been connected or disconnected.
|
||||
"""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
|
||||
def add_GattDeviceServiceServicer_to_server(servicer, server):
|
||||
rpc_method_handlers = {
|
||||
'OnCharacteristicReadRequest': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.OnCharacteristicReadRequest,
|
||||
request_deserializer=emulated__bluetooth__device__pb2.CharacteristicValueRequest.FromString,
|
||||
response_serializer=emulated__bluetooth__device__pb2.CharacteristicValueResponse.SerializeToString,
|
||||
),
|
||||
'OnCharacteristicWriteRequest': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.OnCharacteristicWriteRequest,
|
||||
request_deserializer=emulated__bluetooth__device__pb2.CharacteristicValueRequest.FromString,
|
||||
response_serializer=emulated__bluetooth__device__pb2.CharacteristicValueResponse.SerializeToString,
|
||||
),
|
||||
'OnCharacteristicObserveRequest': grpc.unary_stream_rpc_method_handler(
|
||||
servicer.OnCharacteristicObserveRequest,
|
||||
request_deserializer=emulated__bluetooth__device__pb2.CharacteristicValueRequest.FromString,
|
||||
response_serializer=emulated__bluetooth__device__pb2.CharacteristicValueResponse.SerializeToString,
|
||||
),
|
||||
'OnConnectionStateChange': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.OnConnectionStateChange,
|
||||
request_deserializer=emulated__bluetooth__device__pb2.ConnectionStateChange.FromString,
|
||||
response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString,
|
||||
),
|
||||
}
|
||||
generic_handler = grpc.method_handlers_generic_handler(
|
||||
'android.emulation.bluetooth.GattDeviceService', rpc_method_handlers)
|
||||
server.add_generic_rpc_handlers((generic_handler,))
|
||||
|
||||
|
||||
# This class is part of an EXPERIMENTAL API.
|
||||
class GattDeviceService(object):
|
||||
"""You can provide your own GattDevice by implementing this service
|
||||
and registering it with the android emulator.
|
||||
|
||||
The device will appear as a real bluetooth device, and you will
|
||||
receive callbacks when the bluetooth system wants to
|
||||
read, write or observe a characteristic.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def OnCharacteristicReadRequest(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_unary(request, target, '/android.emulation.bluetooth.GattDeviceService/OnCharacteristicReadRequest',
|
||||
emulated__bluetooth__device__pb2.CharacteristicValueRequest.SerializeToString,
|
||||
emulated__bluetooth__device__pb2.CharacteristicValueResponse.FromString,
|
||||
options, channel_credentials,
|
||||
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
|
||||
|
||||
@staticmethod
|
||||
def OnCharacteristicWriteRequest(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_unary(request, target, '/android.emulation.bluetooth.GattDeviceService/OnCharacteristicWriteRequest',
|
||||
emulated__bluetooth__device__pb2.CharacteristicValueRequest.SerializeToString,
|
||||
emulated__bluetooth__device__pb2.CharacteristicValueResponse.FromString,
|
||||
options, channel_credentials,
|
||||
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
|
||||
|
||||
@staticmethod
|
||||
def OnCharacteristicObserveRequest(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_stream(request, target, '/android.emulation.bluetooth.GattDeviceService/OnCharacteristicObserveRequest',
|
||||
emulated__bluetooth__device__pb2.CharacteristicValueRequest.SerializeToString,
|
||||
emulated__bluetooth__device__pb2.CharacteristicValueResponse.FromString,
|
||||
options, channel_credentials,
|
||||
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
|
||||
|
||||
@staticmethod
|
||||
def OnConnectionStateChange(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_unary(request, target, '/android.emulation.bluetooth.GattDeviceService/OnConnectionStateChange',
|
||||
emulated__bluetooth__device__pb2.ConnectionStateChange.SerializeToString,
|
||||
google_dot_protobuf_dot_empty__pb2.Empty.FromString,
|
||||
options, channel_credentials,
|
||||
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
|
||||
@@ -1,25 +1,10 @@
|
||||
# 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.
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: emulated_bluetooth_packets.proto
|
||||
"""Generated protocol buffer code."""
|
||||
from google.protobuf.internal import builder as _builder
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||
from google.protobuf import message as _message
|
||||
from google.protobuf import reflection as _reflection
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
@@ -30,17 +15,8 @@ _sym_db = _symbol_database.Default()
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n emulated_bluetooth_packets.proto\x12\x1b\x61ndroid.emulation.bluetooth\"\xfb\x01\n\tHCIPacket\x12?\n\x04type\x18\x01 \x01(\x0e\x32\x31.android.emulation.bluetooth.HCIPacket.PacketType\x12\x0e\n\x06packet\x18\x02 \x01(\x0c\"\x9c\x01\n\nPacketType\x12\x1b\n\x17PACKET_TYPE_UNSPECIFIED\x10\x00\x12\x1b\n\x17PACKET_TYPE_HCI_COMMAND\x10\x01\x12\x13\n\x0fPACKET_TYPE_ACL\x10\x02\x12\x13\n\x0fPACKET_TYPE_SCO\x10\x03\x12\x15\n\x11PACKET_TYPE_EVENT\x10\x04\x12\x13\n\x0fPACKET_TYPE_ISO\x10\x05\x42J\n\x1f\x63om.android.emulation.bluetoothP\x01\xf8\x01\x01\xa2\x02\x03\x41\x45\x42\xaa\x02\x1b\x41ndroid.Emulation.Bluetoothb\x06proto3')
|
||||
|
||||
|
||||
|
||||
_HCIPACKET = DESCRIPTOR.message_types_by_name['HCIPacket']
|
||||
_HCIPACKET_PACKETTYPE = _HCIPACKET.enum_types_by_name['PacketType']
|
||||
HCIPacket = _reflection.GeneratedProtocolMessageType('HCIPacket', (_message.Message,), {
|
||||
'DESCRIPTOR' : _HCIPACKET,
|
||||
'__module__' : 'emulated_bluetooth_packets_pb2'
|
||||
# @@protoc_insertion_point(class_scope:android.emulation.bluetooth.HCIPacket)
|
||||
})
|
||||
_sym_db.RegisterMessage(HCIPacket)
|
||||
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'emulated_bluetooth_packets_pb2', globals())
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
|
||||
DESCRIPTOR._options = None
|
||||
@@ -0,0 +1,22 @@
|
||||
from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import message as _message
|
||||
from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union
|
||||
|
||||
DESCRIPTOR: _descriptor.FileDescriptor
|
||||
|
||||
class HCIPacket(_message.Message):
|
||||
__slots__ = ["packet", "type"]
|
||||
class PacketType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||
__slots__ = []
|
||||
PACKET_FIELD_NUMBER: _ClassVar[int]
|
||||
PACKET_TYPE_ACL: HCIPacket.PacketType
|
||||
PACKET_TYPE_EVENT: HCIPacket.PacketType
|
||||
PACKET_TYPE_HCI_COMMAND: HCIPacket.PacketType
|
||||
PACKET_TYPE_ISO: HCIPacket.PacketType
|
||||
PACKET_TYPE_SCO: HCIPacket.PacketType
|
||||
PACKET_TYPE_UNSPECIFIED: HCIPacket.PacketType
|
||||
TYPE_FIELD_NUMBER: _ClassVar[int]
|
||||
packet: bytes
|
||||
type: HCIPacket.PacketType
|
||||
def __init__(self, type: _Optional[_Union[HCIPacket.PacketType, str]] = ..., packet: _Optional[bytes] = ...) -> None: ...
|
||||
@@ -0,0 +1,4 @@
|
||||
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
||||
"""Client and server classes corresponding to protobuf-defined services."""
|
||||
import grpc
|
||||
|
||||
32
bumble/transport/grpc_protobuf/emulated_bluetooth_pb2.py
Normal file
32
bumble/transport/grpc_protobuf/emulated_bluetooth_pb2.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: emulated_bluetooth.proto
|
||||
"""Generated protocol buffer code."""
|
||||
from google.protobuf.internal import builder as _builder
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
from . import emulated_bluetooth_packets_pb2 as emulated__bluetooth__packets__pb2
|
||||
from . import emulated_bluetooth_device_pb2 as emulated__bluetooth__device__pb2
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18\x65mulated_bluetooth.proto\x12\x1b\x61ndroid.emulation.bluetooth\x1a emulated_bluetooth_packets.proto\x1a\x1f\x65mulated_bluetooth_device.proto\"\x19\n\x07RawData\x12\x0e\n\x06packet\x18\x01 \x01(\x0c\"a\n\x12RegistrationStatus\x12K\n\x12\x63\x61llback_device_id\x18\x01 \x01(\x0b\x32/.android.emulation.bluetooth.CallbackIdentifier2\xbb\x03\n\x18\x45mulatedBluetoothService\x12\x64\n\x12registerClassicPhy\x12$.android.emulation.bluetooth.RawData\x1a$.android.emulation.bluetooth.RawData(\x01\x30\x01\x12`\n\x0eregisterBlePhy\x12$.android.emulation.bluetooth.RawData\x1a$.android.emulation.bluetooth.RawData(\x01\x30\x01\x12g\n\x11registerHCIDevice\x12&.android.emulation.bluetooth.HCIPacket\x1a&.android.emulation.bluetooth.HCIPacket(\x01\x30\x01\x12n\n\x12registerGattDevice\x12\'.android.emulation.bluetooth.GattDevice\x1a/.android.emulation.bluetooth.RegistrationStatusB\"\n\x1e\x63om.android.emulator.bluetoothP\x01\x62\x06proto3')
|
||||
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'emulated_bluetooth_pb2', globals())
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
|
||||
DESCRIPTOR._options = None
|
||||
DESCRIPTOR._serialized_options = b'\n\036com.android.emulator.bluetoothP\001'
|
||||
_RAWDATA._serialized_start=124
|
||||
_RAWDATA._serialized_end=149
|
||||
_REGISTRATIONSTATUS._serialized_start=151
|
||||
_REGISTRATIONSTATUS._serialized_end=248
|
||||
_EMULATEDBLUETOOTHSERVICE._serialized_start=251
|
||||
_EMULATEDBLUETOOTHSERVICE._serialized_end=694
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
19
bumble/transport/grpc_protobuf/emulated_bluetooth_pb2.pyi
Normal file
19
bumble/transport/grpc_protobuf/emulated_bluetooth_pb2.pyi
Normal file
@@ -0,0 +1,19 @@
|
||||
from . import emulated_bluetooth_packets_pb2 as _emulated_bluetooth_packets_pb2
|
||||
from . import emulated_bluetooth_device_pb2 as _emulated_bluetooth_device_pb2
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import message as _message
|
||||
from typing import ClassVar as _ClassVar, Mapping as _Mapping, Optional as _Optional, Union as _Union
|
||||
|
||||
DESCRIPTOR: _descriptor.FileDescriptor
|
||||
|
||||
class RawData(_message.Message):
|
||||
__slots__ = ["packet"]
|
||||
PACKET_FIELD_NUMBER: _ClassVar[int]
|
||||
packet: bytes
|
||||
def __init__(self, packet: _Optional[bytes] = ...) -> None: ...
|
||||
|
||||
class RegistrationStatus(_message.Message):
|
||||
__slots__ = ["callback_device_id"]
|
||||
CALLBACK_DEVICE_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
callback_device_id: _emulated_bluetooth_device_pb2.CallbackIdentifier
|
||||
def __init__(self, callback_device_id: _Optional[_Union[_emulated_bluetooth_device_pb2.CallbackIdentifier, _Mapping]] = ...) -> None: ...
|
||||
@@ -1,21 +1,8 @@
|
||||
# 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.
|
||||
|
||||
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
||||
"""Client and server classes corresponding to protobuf-defined services."""
|
||||
import grpc
|
||||
|
||||
from . import emulated_bluetooth_device_pb2 as emulated__bluetooth__device__pb2
|
||||
from . import emulated_bluetooth_packets_pb2 as emulated__bluetooth__packets__pb2
|
||||
from . import emulated_bluetooth_pb2 as emulated__bluetooth__pb2
|
||||
|
||||
@@ -53,6 +40,11 @@ class EmulatedBluetoothServiceStub(object):
|
||||
request_serializer=emulated__bluetooth__packets__pb2.HCIPacket.SerializeToString,
|
||||
response_deserializer=emulated__bluetooth__packets__pb2.HCIPacket.FromString,
|
||||
)
|
||||
self.registerGattDevice = channel.unary_unary(
|
||||
'/android.emulation.bluetooth.EmulatedBluetoothService/registerGattDevice',
|
||||
request_serializer=emulated__bluetooth__device__pb2.GattDevice.SerializeToString,
|
||||
response_deserializer=emulated__bluetooth__pb2.RegistrationStatus.FromString,
|
||||
)
|
||||
|
||||
|
||||
class EmulatedBluetoothServiceServicer(object):
|
||||
@@ -118,6 +110,22 @@ class EmulatedBluetoothServiceServicer(object):
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def registerGattDevice(self, request, context):
|
||||
"""Registers an emulated bluetooth device. The emulator will reach out to
|
||||
the emulated device to read/write and subscribe to properties.
|
||||
|
||||
The following gRPC error codes can be returned:
|
||||
- FAILED_PRECONDITION (code 9):
|
||||
- root canal is not available on this device
|
||||
- unable to reach the endpoint for the GattDevice
|
||||
- INTERNAL (code 13) if there was an internal emulator failure.
|
||||
|
||||
The device will not be discoverable in case of an error.
|
||||
"""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
|
||||
def add_EmulatedBluetoothServiceServicer_to_server(servicer, server):
|
||||
rpc_method_handlers = {
|
||||
@@ -136,6 +144,11 @@ def add_EmulatedBluetoothServiceServicer_to_server(servicer, server):
|
||||
request_deserializer=emulated__bluetooth__packets__pb2.HCIPacket.FromString,
|
||||
response_serializer=emulated__bluetooth__packets__pb2.HCIPacket.SerializeToString,
|
||||
),
|
||||
'registerGattDevice': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.registerGattDevice,
|
||||
request_deserializer=emulated__bluetooth__device__pb2.GattDevice.FromString,
|
||||
response_serializer=emulated__bluetooth__pb2.RegistrationStatus.SerializeToString,
|
||||
),
|
||||
}
|
||||
generic_handler = grpc.method_handlers_generic_handler(
|
||||
'android.emulation.bluetooth.EmulatedBluetoothService', rpc_method_handlers)
|
||||
@@ -205,3 +218,20 @@ class EmulatedBluetoothService(object):
|
||||
emulated__bluetooth__packets__pb2.HCIPacket.FromString,
|
||||
options, channel_credentials,
|
||||
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
|
||||
|
||||
@staticmethod
|
||||
def registerGattDevice(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_unary(request, target, '/android.emulation.bluetooth.EmulatedBluetoothService/registerGattDevice',
|
||||
emulated__bluetooth__device__pb2.GattDevice.SerializeToString,
|
||||
emulated__bluetooth__pb2.RegistrationStatus.FromString,
|
||||
options, channel_credentials,
|
||||
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
|
||||
@@ -1,39 +1,23 @@
|
||||
# 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.
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: emulated_bluetooth_vhci.proto
|
||||
"""Generated protocol buffer code."""
|
||||
from google.protobuf.internal import builder as _builder
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||
from google.protobuf import message as _message
|
||||
from google.protobuf import reflection as _reflection
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
import emulated_bluetooth_packets_pb2 as emulated__bluetooth__packets__pb2
|
||||
from . import emulated_bluetooth_packets_pb2 as emulated__bluetooth__packets__pb2
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1d\x65mulated_bluetooth_vhci.proto\x12\x1b\x61ndroid.emulation.bluetooth\x1a emulated_bluetooth_packets.proto2y\n\x15VhciForwardingService\x12`\n\nattachVhci\x12&.android.emulation.bluetooth.HCIPacket\x1a&.android.emulation.bluetooth.HCIPacket(\x01\x30\x01\x42J\n\x1f\x63om.android.emulation.bluetoothP\x01\xf8\x01\x01\xa2\x02\x03\x41\x45\x42\xaa\x02\x1b\x41ndroid.Emulation.Bluetoothb\x06proto3')
|
||||
|
||||
|
||||
|
||||
_VHCIFORWARDINGSERVICE = DESCRIPTOR.services_by_name['VhciForwardingService']
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'emulated_bluetooth_vhci_pb2', globals())
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
|
||||
DESCRIPTOR._options = None
|
||||
@@ -0,0 +1,5 @@
|
||||
import emulated_bluetooth_packets_pb2 as _emulated_bluetooth_packets_pb2
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from typing import ClassVar as _ClassVar
|
||||
|
||||
DESCRIPTOR: _descriptor.FileDescriptor
|
||||
@@ -1,17 +1,3 @@
|
||||
# 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.
|
||||
|
||||
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
||||
"""Client and server classes corresponding to protobuf-defined services."""
|
||||
import grpc
|
||||
@@ -0,0 +1,30 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: grpc_endpoint_description.proto
|
||||
"""Generated protocol buffer code."""
|
||||
from google.protobuf.internal import builder as _builder
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1fgrpc_endpoint_description.proto\x12\x18\x61ndroid.emulation.remote\"V\n\x0b\x43redentials\x12\x16\n\x0epem_root_certs\x18\x01 \x01(\t\x12\x17\n\x0fpem_private_key\x18\x02 \x01(\t\x12\x16\n\x0epem_cert_chain\x18\x03 \x01(\t\"$\n\x06Header\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"\x96\x01\n\x08\x45ndpoint\x12\x0e\n\x06target\x18\x01 \x01(\t\x12>\n\x0ftls_credentials\x18\x02 \x01(\x0b\x32%.android.emulation.remote.Credentials\x12:\n\x10required_headers\x18\x03 \x03(\x0b\x32 .android.emulation.remote.HeaderB \n\x1c\x63om.android.emulation.remoteP\x01\x62\x06proto3')
|
||||
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'grpc_endpoint_description_pb2', globals())
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
|
||||
DESCRIPTOR._options = None
|
||||
DESCRIPTOR._serialized_options = b'\n\034com.android.emulation.remoteP\001'
|
||||
_CREDENTIALS._serialized_start=61
|
||||
_CREDENTIALS._serialized_end=147
|
||||
_HEADER._serialized_start=149
|
||||
_HEADER._serialized_end=185
|
||||
_ENDPOINT._serialized_start=188
|
||||
_ENDPOINT._serialized_end=338
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
@@ -0,0 +1,34 @@
|
||||
from google.protobuf.internal import containers as _containers
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import message as _message
|
||||
from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union
|
||||
|
||||
DESCRIPTOR: _descriptor.FileDescriptor
|
||||
|
||||
class Credentials(_message.Message):
|
||||
__slots__ = ["pem_cert_chain", "pem_private_key", "pem_root_certs"]
|
||||
PEM_CERT_CHAIN_FIELD_NUMBER: _ClassVar[int]
|
||||
PEM_PRIVATE_KEY_FIELD_NUMBER: _ClassVar[int]
|
||||
PEM_ROOT_CERTS_FIELD_NUMBER: _ClassVar[int]
|
||||
pem_cert_chain: str
|
||||
pem_private_key: str
|
||||
pem_root_certs: str
|
||||
def __init__(self, pem_root_certs: _Optional[str] = ..., pem_private_key: _Optional[str] = ..., pem_cert_chain: _Optional[str] = ...) -> None: ...
|
||||
|
||||
class Endpoint(_message.Message):
|
||||
__slots__ = ["required_headers", "target", "tls_credentials"]
|
||||
REQUIRED_HEADERS_FIELD_NUMBER: _ClassVar[int]
|
||||
TARGET_FIELD_NUMBER: _ClassVar[int]
|
||||
TLS_CREDENTIALS_FIELD_NUMBER: _ClassVar[int]
|
||||
required_headers: _containers.RepeatedCompositeFieldContainer[Header]
|
||||
target: str
|
||||
tls_credentials: Credentials
|
||||
def __init__(self, target: _Optional[str] = ..., tls_credentials: _Optional[_Union[Credentials, _Mapping]] = ..., required_headers: _Optional[_Iterable[_Union[Header, _Mapping]]] = ...) -> None: ...
|
||||
|
||||
class Header(_message.Message):
|
||||
__slots__ = ["key", "value"]
|
||||
KEY_FIELD_NUMBER: _ClassVar[int]
|
||||
VALUE_FIELD_NUMBER: _ClassVar[int]
|
||||
key: str
|
||||
value: str
|
||||
def __init__(self, key: _Optional[str] = ..., value: _Optional[str] = ...) -> None: ...
|
||||
@@ -0,0 +1,4 @@
|
||||
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
||||
"""Client and server classes corresponding to protobuf-defined services."""
|
||||
import grpc
|
||||
|
||||
28
bumble/transport/grpc_protobuf/hci_packet_pb2.py
Normal file
28
bumble/transport/grpc_protobuf/hci_packet_pb2.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: hci_packet.proto
|
||||
"""Generated protocol buffer code."""
|
||||
from google.protobuf.internal import builder as _builder
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x10hci_packet.proto\x12\rnetsim.packet\"\xb2\x01\n\tHCIPacket\x12\x38\n\x0bpacket_type\x18\x01 \x01(\x0e\x32#.netsim.packet.HCIPacket.PacketType\x12\x0e\n\x06packet\x18\x02 \x01(\x0c\"[\n\nPacketType\x12\x1a\n\x16HCI_PACKET_UNSPECIFIED\x10\x00\x12\x0b\n\x07\x43OMMAND\x10\x01\x12\x07\n\x03\x41\x43L\x10\x02\x12\x07\n\x03SCO\x10\x03\x12\t\n\x05\x45VENT\x10\x04\x12\x07\n\x03ISO\x10\x05\x42J\n\x1f\x63om.android.emulation.bluetoothP\x01\xf8\x01\x01\xa2\x02\x03\x41\x45\x42\xaa\x02\x1b\x41ndroid.Emulation.Bluetoothb\x06proto3')
|
||||
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'hci_packet_pb2', globals())
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
|
||||
DESCRIPTOR._options = None
|
||||
DESCRIPTOR._serialized_options = b'\n\037com.android.emulation.bluetoothP\001\370\001\001\242\002\003AEB\252\002\033Android.Emulation.Bluetooth'
|
||||
_HCIPACKET._serialized_start=36
|
||||
_HCIPACKET._serialized_end=214
|
||||
_HCIPACKET_PACKETTYPE._serialized_start=123
|
||||
_HCIPACKET_PACKETTYPE._serialized_end=214
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
22
bumble/transport/grpc_protobuf/hci_packet_pb2.pyi
Normal file
22
bumble/transport/grpc_protobuf/hci_packet_pb2.pyi
Normal file
@@ -0,0 +1,22 @@
|
||||
from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import message as _message
|
||||
from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union
|
||||
|
||||
DESCRIPTOR: _descriptor.FileDescriptor
|
||||
|
||||
class HCIPacket(_message.Message):
|
||||
__slots__ = ["packet", "packet_type"]
|
||||
class PacketType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||
__slots__ = []
|
||||
ACL: HCIPacket.PacketType
|
||||
COMMAND: HCIPacket.PacketType
|
||||
EVENT: HCIPacket.PacketType
|
||||
HCI_PACKET_UNSPECIFIED: HCIPacket.PacketType
|
||||
ISO: HCIPacket.PacketType
|
||||
PACKET_FIELD_NUMBER: _ClassVar[int]
|
||||
PACKET_TYPE_FIELD_NUMBER: _ClassVar[int]
|
||||
SCO: HCIPacket.PacketType
|
||||
packet: bytes
|
||||
packet_type: HCIPacket.PacketType
|
||||
def __init__(self, packet_type: _Optional[_Union[HCIPacket.PacketType, str]] = ..., packet: _Optional[bytes] = ...) -> None: ...
|
||||
4
bumble/transport/grpc_protobuf/hci_packet_pb2_grpc.py
Normal file
4
bumble/transport/grpc_protobuf/hci_packet_pb2_grpc.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
||||
"""Client and server classes corresponding to protobuf-defined services."""
|
||||
import grpc
|
||||
|
||||
31
bumble/transport/grpc_protobuf/packet_streamer_pb2.py
Normal file
31
bumble/transport/grpc_protobuf/packet_streamer_pb2.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: packet_streamer.proto
|
||||
"""Generated protocol buffer code."""
|
||||
from google.protobuf.internal import builder as _builder
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
from . import hci_packet_pb2 as hci__packet__pb2
|
||||
from . import startup_pb2 as startup__pb2
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15packet_streamer.proto\x12\rnetsim.packet\x1a\x10hci_packet.proto\x1a\rstartup.proto\"\x93\x01\n\rPacketRequest\x12\x30\n\x0cinitial_info\x18\x01 \x01(\x0b\x32\x18.netsim.startup.ChipInfoH\x00\x12.\n\nhci_packet\x18\x02 \x01(\x0b\x32\x18.netsim.packet.HCIPacketH\x00\x12\x10\n\x06packet\x18\x03 \x01(\x0cH\x00\x42\x0e\n\x0crequest_type\"t\n\x0ePacketResponse\x12\x0f\n\x05\x65rror\x18\x01 \x01(\tH\x00\x12.\n\nhci_packet\x18\x02 \x01(\x0b\x32\x18.netsim.packet.HCIPacketH\x00\x12\x10\n\x06packet\x18\x03 \x01(\x0cH\x00\x42\x0f\n\rresponse_type2b\n\x0ePacketStreamer\x12P\n\rStreamPackets\x12\x1c.netsim.packet.PacketRequest\x1a\x1d.netsim.packet.PacketResponse(\x01\x30\x01\x62\x06proto3')
|
||||
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'packet_streamer_pb2', globals())
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
|
||||
DESCRIPTOR._options = None
|
||||
_PACKETREQUEST._serialized_start=74
|
||||
_PACKETREQUEST._serialized_end=221
|
||||
_PACKETRESPONSE._serialized_start=223
|
||||
_PACKETRESPONSE._serialized_end=339
|
||||
_PACKETSTREAMER._serialized_start=341
|
||||
_PACKETSTREAMER._serialized_end=439
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
27
bumble/transport/grpc_protobuf/packet_streamer_pb2.pyi
Normal file
27
bumble/transport/grpc_protobuf/packet_streamer_pb2.pyi
Normal file
@@ -0,0 +1,27 @@
|
||||
from . import hci_packet_pb2 as _hci_packet_pb2
|
||||
from . import startup_pb2 as _startup_pb2
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import message as _message
|
||||
from typing import ClassVar as _ClassVar, Mapping as _Mapping, Optional as _Optional, Union as _Union
|
||||
|
||||
DESCRIPTOR: _descriptor.FileDescriptor
|
||||
|
||||
class PacketRequest(_message.Message):
|
||||
__slots__ = ["hci_packet", "initial_info", "packet"]
|
||||
HCI_PACKET_FIELD_NUMBER: _ClassVar[int]
|
||||
INITIAL_INFO_FIELD_NUMBER: _ClassVar[int]
|
||||
PACKET_FIELD_NUMBER: _ClassVar[int]
|
||||
hci_packet: _hci_packet_pb2.HCIPacket
|
||||
initial_info: _startup_pb2.ChipInfo
|
||||
packet: bytes
|
||||
def __init__(self, initial_info: _Optional[_Union[_startup_pb2.ChipInfo, _Mapping]] = ..., hci_packet: _Optional[_Union[_hci_packet_pb2.HCIPacket, _Mapping]] = ..., packet: _Optional[bytes] = ...) -> None: ...
|
||||
|
||||
class PacketResponse(_message.Message):
|
||||
__slots__ = ["error", "hci_packet", "packet"]
|
||||
ERROR_FIELD_NUMBER: _ClassVar[int]
|
||||
HCI_PACKET_FIELD_NUMBER: _ClassVar[int]
|
||||
PACKET_FIELD_NUMBER: _ClassVar[int]
|
||||
error: str
|
||||
hci_packet: _hci_packet_pb2.HCIPacket
|
||||
packet: bytes
|
||||
def __init__(self, error: _Optional[str] = ..., hci_packet: _Optional[_Union[_hci_packet_pb2.HCIPacket, _Mapping]] = ..., packet: _Optional[bytes] = ...) -> None: ...
|
||||
109
bumble/transport/grpc_protobuf/packet_streamer_pb2_grpc.py
Normal file
109
bumble/transport/grpc_protobuf/packet_streamer_pb2_grpc.py
Normal file
@@ -0,0 +1,109 @@
|
||||
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
||||
"""Client and server classes corresponding to protobuf-defined services."""
|
||||
import grpc
|
||||
|
||||
from . import packet_streamer_pb2 as packet__streamer__pb2
|
||||
|
||||
|
||||
class PacketStreamerStub(object):
|
||||
"""*
|
||||
This is the packet service for the network simulator.
|
||||
|
||||
Android Virtual Devices (AVDs) and accessory devices use this service to
|
||||
connect to the network simulator and pass packets back and forth.
|
||||
|
||||
AVDs running in a guest VM are built with virtual controllers for each radio
|
||||
chip. These controllers route chip requests to host emulators (qemu and
|
||||
crosvm) using virtio and from there they are forwarded to this gRpc service.
|
||||
|
||||
This setup provides a transparent radio environment across AVDs and
|
||||
accessories because the network simulator contains libraries to emulate
|
||||
Bluetooth, 80211MAC, UWB, and Rtt chips.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, channel):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
channel: A grpc.Channel.
|
||||
"""
|
||||
self.StreamPackets = channel.stream_stream(
|
||||
'/netsim.packet.PacketStreamer/StreamPackets',
|
||||
request_serializer=packet__streamer__pb2.PacketRequest.SerializeToString,
|
||||
response_deserializer=packet__streamer__pb2.PacketResponse.FromString,
|
||||
)
|
||||
|
||||
|
||||
class PacketStreamerServicer(object):
|
||||
"""*
|
||||
This is the packet service for the network simulator.
|
||||
|
||||
Android Virtual Devices (AVDs) and accessory devices use this service to
|
||||
connect to the network simulator and pass packets back and forth.
|
||||
|
||||
AVDs running in a guest VM are built with virtual controllers for each radio
|
||||
chip. These controllers route chip requests to host emulators (qemu and
|
||||
crosvm) using virtio and from there they are forwarded to this gRpc service.
|
||||
|
||||
This setup provides a transparent radio environment across AVDs and
|
||||
accessories because the network simulator contains libraries to emulate
|
||||
Bluetooth, 80211MAC, UWB, and Rtt chips.
|
||||
|
||||
"""
|
||||
|
||||
def StreamPackets(self, request_iterator, context):
|
||||
"""Attach a virtual radio controller to the network simulation.
|
||||
"""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
|
||||
def add_PacketStreamerServicer_to_server(servicer, server):
|
||||
rpc_method_handlers = {
|
||||
'StreamPackets': grpc.stream_stream_rpc_method_handler(
|
||||
servicer.StreamPackets,
|
||||
request_deserializer=packet__streamer__pb2.PacketRequest.FromString,
|
||||
response_serializer=packet__streamer__pb2.PacketResponse.SerializeToString,
|
||||
),
|
||||
}
|
||||
generic_handler = grpc.method_handlers_generic_handler(
|
||||
'netsim.packet.PacketStreamer', rpc_method_handlers)
|
||||
server.add_generic_rpc_handlers((generic_handler,))
|
||||
|
||||
|
||||
# This class is part of an EXPERIMENTAL API.
|
||||
class PacketStreamer(object):
|
||||
"""*
|
||||
This is the packet service for the network simulator.
|
||||
|
||||
Android Virtual Devices (AVDs) and accessory devices use this service to
|
||||
connect to the network simulator and pass packets back and forth.
|
||||
|
||||
AVDs running in a guest VM are built with virtual controllers for each radio
|
||||
chip. These controllers route chip requests to host emulators (qemu and
|
||||
crosvm) using virtio and from there they are forwarded to this gRpc service.
|
||||
|
||||
This setup provides a transparent radio environment across AVDs and
|
||||
accessories because the network simulator contains libraries to emulate
|
||||
Bluetooth, 80211MAC, UWB, and Rtt chips.
|
||||
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def StreamPackets(request_iterator,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.stream_stream(request_iterator, target, '/netsim.packet.PacketStreamer/StreamPackets',
|
||||
packet__streamer__pb2.PacketRequest.SerializeToString,
|
||||
packet__streamer__pb2.PacketResponse.FromString,
|
||||
options, channel_credentials,
|
||||
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
|
||||
32
bumble/transport/grpc_protobuf/startup_pb2.py
Normal file
32
bumble/transport/grpc_protobuf/startup_pb2.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: startup.proto
|
||||
"""Generated protocol buffer code."""
|
||||
from google.protobuf.internal import builder as _builder
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
from . import common_pb2 as common__pb2
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\rstartup.proto\x12\x0enetsim.startup\x1a\x0c\x63ommon.proto\"\x7f\n\x0bStartupInfo\x12\x33\n\x07\x64\x65vices\x18\x01 \x03(\x0b\x32\".netsim.startup.StartupInfo.Device\x1a;\n\x06\x44\x65vice\x12\x0c\n\x04name\x18\x01 \x01(\t\x12#\n\x05\x63hips\x18\x02 \x03(\x0b\x32\x14.netsim.startup.Chip\"<\n\x08\x43hipInfo\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\"\n\x04\x63hip\x18\x02 \x01(\x0b\x32\x14.netsim.startup.Chip\"\x96\x01\n\x04\x43hip\x12%\n\x04kind\x18\x01 \x01(\x0e\x32\x17.netsim.common.ChipKind\x12\n\n\x02id\x18\x02 \x01(\t\x12\x14\n\x0cmanufacturer\x18\x03 \x01(\t\x12\x14\n\x0cproduct_name\x18\x04 \x01(\t\x12\r\n\x05\x66\x64_in\x18\x05 \x01(\x05\x12\x0e\n\x06\x66\x64_out\x18\x06 \x01(\x05\x12\x10\n\x08loopback\x18\x07 \x01(\x08\x62\x06proto3')
|
||||
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'startup_pb2', globals())
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
|
||||
DESCRIPTOR._options = None
|
||||
_STARTUPINFO._serialized_start=47
|
||||
_STARTUPINFO._serialized_end=174
|
||||
_STARTUPINFO_DEVICE._serialized_start=115
|
||||
_STARTUPINFO_DEVICE._serialized_end=174
|
||||
_CHIPINFO._serialized_start=176
|
||||
_CHIPINFO._serialized_end=236
|
||||
_CHIP._serialized_start=239
|
||||
_CHIP._serialized_end=389
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
46
bumble/transport/grpc_protobuf/startup_pb2.pyi
Normal file
46
bumble/transport/grpc_protobuf/startup_pb2.pyi
Normal file
@@ -0,0 +1,46 @@
|
||||
from . import common_pb2 as _common_pb2
|
||||
from google.protobuf.internal import containers as _containers
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import message as _message
|
||||
from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union
|
||||
|
||||
DESCRIPTOR: _descriptor.FileDescriptor
|
||||
|
||||
class Chip(_message.Message):
|
||||
__slots__ = ["fd_in", "fd_out", "id", "kind", "loopback", "manufacturer", "product_name"]
|
||||
FD_IN_FIELD_NUMBER: _ClassVar[int]
|
||||
FD_OUT_FIELD_NUMBER: _ClassVar[int]
|
||||
ID_FIELD_NUMBER: _ClassVar[int]
|
||||
KIND_FIELD_NUMBER: _ClassVar[int]
|
||||
LOOPBACK_FIELD_NUMBER: _ClassVar[int]
|
||||
MANUFACTURER_FIELD_NUMBER: _ClassVar[int]
|
||||
PRODUCT_NAME_FIELD_NUMBER: _ClassVar[int]
|
||||
fd_in: int
|
||||
fd_out: int
|
||||
id: str
|
||||
kind: _common_pb2.ChipKind
|
||||
loopback: bool
|
||||
manufacturer: str
|
||||
product_name: str
|
||||
def __init__(self, kind: _Optional[_Union[_common_pb2.ChipKind, str]] = ..., id: _Optional[str] = ..., manufacturer: _Optional[str] = ..., product_name: _Optional[str] = ..., fd_in: _Optional[int] = ..., fd_out: _Optional[int] = ..., loopback: bool = ...) -> None: ...
|
||||
|
||||
class ChipInfo(_message.Message):
|
||||
__slots__ = ["chip", "name"]
|
||||
CHIP_FIELD_NUMBER: _ClassVar[int]
|
||||
NAME_FIELD_NUMBER: _ClassVar[int]
|
||||
chip: Chip
|
||||
name: str
|
||||
def __init__(self, name: _Optional[str] = ..., chip: _Optional[_Union[Chip, _Mapping]] = ...) -> None: ...
|
||||
|
||||
class StartupInfo(_message.Message):
|
||||
__slots__ = ["devices"]
|
||||
class Device(_message.Message):
|
||||
__slots__ = ["chips", "name"]
|
||||
CHIPS_FIELD_NUMBER: _ClassVar[int]
|
||||
NAME_FIELD_NUMBER: _ClassVar[int]
|
||||
chips: _containers.RepeatedCompositeFieldContainer[Chip]
|
||||
name: str
|
||||
def __init__(self, name: _Optional[str] = ..., chips: _Optional[_Iterable[_Union[Chip, _Mapping]]] = ...) -> None: ...
|
||||
DEVICES_FIELD_NUMBER: _ClassVar[int]
|
||||
devices: _containers.RepeatedCompositeFieldContainer[StartupInfo.Device]
|
||||
def __init__(self, devices: _Optional[_Iterable[_Union[StartupInfo.Device, _Mapping]]] = ...) -> None: ...
|
||||
4
bumble/transport/grpc_protobuf/startup_pb2_grpc.py
Normal file
4
bumble/transport/grpc_protobuf/startup_pb2_grpc.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
||||
"""Client and server classes corresponding to protobuf-defined services."""
|
||||
import grpc
|
||||
|
||||
@@ -40,15 +40,21 @@ async def open_hci_socket_transport(spec):
|
||||
or a 0-based integer to indicate the adapter number.
|
||||
'''
|
||||
|
||||
HCI_CHANNEL_USER = 1
|
||||
HCI_CHANNEL_USER = 1 # pylint: disable=invalid-name
|
||||
|
||||
# Create a raw HCI socket
|
||||
try:
|
||||
hci_socket = socket.socket(socket.AF_BLUETOOTH, socket.SOCK_RAW | socket.SOCK_NONBLOCK, socket.BTPROTO_HCI)
|
||||
except AttributeError:
|
||||
hci_socket = socket.socket(
|
||||
socket.AF_BLUETOOTH,
|
||||
socket.SOCK_RAW | socket.SOCK_NONBLOCK,
|
||||
socket.BTPROTO_HCI,
|
||||
)
|
||||
except AttributeError as error:
|
||||
# Not supported on this platform
|
||||
logger.info("HCI sockets not supported on this platform")
|
||||
raise Exception('Bluetooth HCI sockets not supported on this platform')
|
||||
raise Exception(
|
||||
'Bluetooth HCI sockets not supported on this platform'
|
||||
) from error
|
||||
|
||||
# Compute the adapter index
|
||||
if spec is None:
|
||||
@@ -62,20 +68,37 @@ async def open_hci_socket_transport(spec):
|
||||
try:
|
||||
ctypes.cdll.LoadLibrary('libc.so.6')
|
||||
libc = ctypes.CDLL('libc.so.6', use_errno=True)
|
||||
except OSError:
|
||||
except OSError as error:
|
||||
logger.info("HCI sockets not supported on this platform")
|
||||
raise Exception('Bluetooth HCI sockets not supported on this platform')
|
||||
raise Exception(
|
||||
'Bluetooth HCI sockets not supported on this platform'
|
||||
) from error
|
||||
libc.bind.argtypes = (ctypes.c_int, ctypes.POINTER(ctypes.c_char), ctypes.c_int)
|
||||
libc.bind.restype = ctypes.c_int
|
||||
bind_address = struct.pack('<HHH', socket.AF_BLUETOOTH, adapter_index, HCI_CHANNEL_USER)
|
||||
if libc.bind(hci_socket.fileno(), ctypes.create_string_buffer(bind_address), len(bind_address)) != 0:
|
||||
bind_address = struct.pack(
|
||||
# pylint: disable=no-member
|
||||
'<HHH',
|
||||
socket.AF_BLUETOOTH,
|
||||
adapter_index,
|
||||
HCI_CHANNEL_USER,
|
||||
)
|
||||
if (
|
||||
libc.bind(
|
||||
hci_socket.fileno(),
|
||||
ctypes.create_string_buffer(bind_address),
|
||||
len(bind_address),
|
||||
)
|
||||
!= 0
|
||||
):
|
||||
raise IOError(ctypes.get_errno(), os.strerror(ctypes.get_errno()))
|
||||
|
||||
class HciSocketSource(ParserSource):
|
||||
def __init__(self, socket):
|
||||
def __init__(self, hci_socket):
|
||||
super().__init__()
|
||||
self.socket = socket
|
||||
asyncio.get_running_loop().add_reader(socket.fileno(), self.recv_until_would_block)
|
||||
self.socket = hci_socket
|
||||
asyncio.get_running_loop().add_reader(
|
||||
self.socket.fileno(), self.recv_until_would_block
|
||||
)
|
||||
|
||||
def recv_until_would_block(self):
|
||||
logger.debug('recv until would block +++')
|
||||
@@ -92,9 +115,9 @@ async def open_hci_socket_transport(spec):
|
||||
asyncio.get_running_loop().remove_reader(self.socket.fileno())
|
||||
|
||||
class HciSocketSink:
|
||||
def __init__(self, socket):
|
||||
self.socket = socket
|
||||
self.packets = collections.deque()
|
||||
def __init__(self, hci_socket):
|
||||
self.socket = hci_socket
|
||||
self.packets = collections.deque()
|
||||
self.writer_added = False
|
||||
|
||||
def send_until_would_block(self):
|
||||
@@ -112,9 +135,14 @@ async def open_hci_socket_transport(spec):
|
||||
break
|
||||
|
||||
if self.packets:
|
||||
# There's still something to send, ensure that we are monitoring the socket
|
||||
# There's still something to send, ensure that we are monitoring the
|
||||
# socket
|
||||
if not self.writer_added:
|
||||
asyncio.get_running_loop().add_writer(socket.fileno(), self.send_until_would_block)
|
||||
asyncio.get_running_loop().add_writer(
|
||||
# pylint: disable=no-member
|
||||
self.socket.fileno(),
|
||||
self.send_until_would_block,
|
||||
)
|
||||
self.writer_added = True
|
||||
else:
|
||||
# Nothing left to send, stop monitoring the socket
|
||||
@@ -131,9 +159,9 @@ async def open_hci_socket_transport(spec):
|
||||
asyncio.get_running_loop().remove_writer(self.socket.fileno())
|
||||
|
||||
class HciSocketTransport(Transport):
|
||||
def __init__(self, socket, source, sink):
|
||||
def __init__(self, hci_socket, source, sink):
|
||||
super().__init__(source, sink)
|
||||
self.socket = socket
|
||||
self.socket = hci_socket
|
||||
|
||||
async def close(self):
|
||||
logger.debug('closing HCI socket transport')
|
||||
|
||||
@@ -47,13 +47,11 @@ async def open_pty_transport(spec):
|
||||
tty.setraw(replica)
|
||||
|
||||
read_transport, packet_source = await asyncio.get_running_loop().connect_read_pipe(
|
||||
lambda: StreamPacketSource(),
|
||||
io.open(primary, 'rb', closefd=False)
|
||||
StreamPacketSource, io.open(primary, 'rb', closefd=False)
|
||||
)
|
||||
|
||||
write_transport, _ = await asyncio.get_running_loop().connect_write_pipe(
|
||||
lambda: asyncio.BaseProtocol(),
|
||||
io.open(primary, 'wb', closefd=False)
|
||||
asyncio.BaseProtocol, io.open(primary, 'wb', closefd=False)
|
||||
)
|
||||
packet_sink = StreamPacketSink(write_transport)
|
||||
|
||||
|
||||
0
bumble/transport/py.typed
Normal file
0
bumble/transport/py.typed
Normal file
@@ -17,14 +17,15 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import logging
|
||||
import usb.core
|
||||
import usb.util
|
||||
import threading
|
||||
import time
|
||||
from colors import color
|
||||
|
||||
import usb.core
|
||||
import usb.util
|
||||
|
||||
from .common import Transport, ParserSource
|
||||
from .. import hci
|
||||
from ..colors import color
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -48,25 +49,26 @@ async def open_pyusb_transport(spec):
|
||||
04b4:f901 --> the BT USB dongle with vendor=04b4 and product=f901
|
||||
'''
|
||||
|
||||
USB_RECIPIENT_DEVICE = 0x00
|
||||
USB_REQUEST_TYPE_CLASS = 0x01 << 5
|
||||
USB_ENDPOINT_EVENTS_IN = 0x81
|
||||
USB_ENDPOINT_ACL_IN = 0x82
|
||||
USB_ENDPOINT_SCO_IN = 0x83
|
||||
USB_ENDPOINT_ACL_OUT = 0x02
|
||||
# pylint: disable=invalid-name
|
||||
USB_RECIPIENT_DEVICE = 0x00
|
||||
USB_REQUEST_TYPE_CLASS = 0x01 << 5
|
||||
USB_ENDPOINT_EVENTS_IN = 0x81
|
||||
USB_ENDPOINT_ACL_IN = 0x82
|
||||
USB_ENDPOINT_SCO_IN = 0x83
|
||||
USB_ENDPOINT_ACL_OUT = 0x02
|
||||
# USB_ENDPOINT_SCO_OUT = 0x03
|
||||
USB_DEVICE_CLASS_WIRELESS_CONTROLLER = 0xE0
|
||||
USB_DEVICE_SUBCLASS_RF_CONTROLLER = 0x01
|
||||
USB_DEVICE_CLASS_WIRELESS_CONTROLLER = 0xE0
|
||||
USB_DEVICE_SUBCLASS_RF_CONTROLLER = 0x01
|
||||
USB_DEVICE_PROTOCOL_BLUETOOTH_PRIMARY_CONTROLLER = 0x01
|
||||
|
||||
READ_SIZE = 1024
|
||||
READ_SIZE = 1024
|
||||
READ_TIMEOUT = 1000
|
||||
|
||||
class UsbPacketSink:
|
||||
def __init__(self, device):
|
||||
self.device = device
|
||||
self.thread = threading.Thread(target=self.run)
|
||||
self.loop = asyncio.get_running_loop()
|
||||
self.device = device
|
||||
self.thread = threading.Thread(target=self.run)
|
||||
self.loop = asyncio.get_running_loop()
|
||||
self.stop_event = None
|
||||
|
||||
def on_packet(self, packet):
|
||||
@@ -80,9 +82,17 @@ async def open_pyusb_transport(spec):
|
||||
if packet_type == hci.HCI_ACL_DATA_PACKET:
|
||||
self.device.write(USB_ENDPOINT_ACL_OUT, packet[1:])
|
||||
elif packet_type == hci.HCI_COMMAND_PACKET:
|
||||
self.device.ctrl_transfer(USB_RECIPIENT_DEVICE | USB_REQUEST_TYPE_CLASS, 0, 0, 0, packet[1:])
|
||||
self.device.ctrl_transfer(
|
||||
USB_RECIPIENT_DEVICE | USB_REQUEST_TYPE_CLASS,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
packet[1:],
|
||||
)
|
||||
else:
|
||||
logger.warning(color(f'unsupported packet type {packet_type}', 'red'))
|
||||
logger.warning(
|
||||
color(f'unsupported packet type {packet_type}', 'red')
|
||||
)
|
||||
except usb.core.USBTimeoutError:
|
||||
logger.warning('USB Write Timeout')
|
||||
except usb.core.USBError as error:
|
||||
@@ -100,22 +110,21 @@ async def open_pyusb_transport(spec):
|
||||
def run(self):
|
||||
while self.stop_event is None:
|
||||
time.sleep(1)
|
||||
self.loop.call_soon_threadsafe(lambda: self.stop_event.set())
|
||||
self.loop.call_soon_threadsafe(self.stop_event.set)
|
||||
|
||||
class UsbPacketSource(asyncio.Protocol, ParserSource):
|
||||
def __init__(self, device, sco_enabled):
|
||||
super().__init__()
|
||||
self.device = device
|
||||
self.loop = asyncio.get_running_loop()
|
||||
self.queue = asyncio.Queue()
|
||||
self.device = device
|
||||
self.loop = asyncio.get_running_loop()
|
||||
self.queue = asyncio.Queue()
|
||||
self.dequeue_task = None
|
||||
self.event_thread = threading.Thread(
|
||||
target=self.run,
|
||||
args=(USB_ENDPOINT_EVENTS_IN, hci.HCI_EVENT_PACKET)
|
||||
target=self.run, args=(USB_ENDPOINT_EVENTS_IN, hci.HCI_EVENT_PACKET)
|
||||
)
|
||||
self.event_thread.stop_event = None
|
||||
self.acl_thread = threading.Thread(
|
||||
target=self.run,
|
||||
args=(USB_ENDPOINT_ACL_IN, hci.HCI_ACL_DATA_PACKET)
|
||||
target=self.run, args=(USB_ENDPOINT_ACL_IN, hci.HCI_ACL_DATA_PACKET)
|
||||
)
|
||||
self.acl_thread.stop_event = None
|
||||
|
||||
@@ -124,12 +133,12 @@ async def open_pyusb_transport(spec):
|
||||
if sco_enabled:
|
||||
self.sco_thread = threading.Thread(
|
||||
target=self.run,
|
||||
args=(USB_ENDPOINT_SCO_IN, hci.HCI_SYNCHRONOUS_DATA_PACKET)
|
||||
args=(USB_ENDPOINT_SCO_IN, hci.HCI_SYNCHRONOUS_DATA_PACKET),
|
||||
)
|
||||
self.sco_thread.stop_event = None
|
||||
|
||||
def data_received(self, packet):
|
||||
self.parser.feed_data(packet)
|
||||
def data_received(self, data):
|
||||
self.parser.feed_data(data)
|
||||
|
||||
def enqueue(self, packet):
|
||||
self.queue.put_nowait(packet)
|
||||
@@ -155,7 +164,7 @@ async def open_pyusb_transport(spec):
|
||||
|
||||
# Create stop events and wait for them to be signaled
|
||||
self.event_thread.stop_event = asyncio.Event()
|
||||
self.acl_thread.stop_event = asyncio.Event()
|
||||
self.acl_thread.stop_event = asyncio.Event()
|
||||
await self.event_thread.stop_event.wait()
|
||||
await self.acl_thread.stop_event.wait()
|
||||
if self.sco_enabled:
|
||||
@@ -173,16 +182,17 @@ async def open_pyusb_transport(spec):
|
||||
except usb.core.USBTimeoutError:
|
||||
continue
|
||||
except usb.core.USBError:
|
||||
# Don't log this: because pyusb doesn't really support multiple threads
|
||||
# reading at the same time, we can get occasional USBError(errno=5)
|
||||
# Input/Output errors reported, but they seem to be harmless.
|
||||
# Don't log this: because pyusb doesn't really support multiple
|
||||
# threads reading at the same time, we can get occasional
|
||||
# USBError(errno=5) Input/Output errors reported, but they seem to
|
||||
# be harmless.
|
||||
# Until support for async or multi-thread support is added to pyusb,
|
||||
# we'll just live with this as is...
|
||||
# logger.warning(f'USB read error: {error}')
|
||||
time.sleep(1) # Sleep one second to avoid busy looping
|
||||
|
||||
stop_event = current_thread.stop_event
|
||||
self.loop.call_soon_threadsafe(lambda: stop_event.set())
|
||||
self.loop.call_soon_threadsafe(stop_event.set)
|
||||
|
||||
class UsbTransport(Transport):
|
||||
def __init__(self, device, source, sink):
|
||||
@@ -194,18 +204,28 @@ async def open_pyusb_transport(spec):
|
||||
await self.sink.stop()
|
||||
usb.util.release_interface(self.device, 0)
|
||||
|
||||
usb_find = usb.core.find
|
||||
try:
|
||||
import libusb_package
|
||||
except ImportError:
|
||||
logger.debug('libusb_package is not available')
|
||||
else:
|
||||
usb_find = libusb_package.find
|
||||
|
||||
# Find the device according to the spec moniker
|
||||
if ':' in spec:
|
||||
vendor_id, product_id = spec.split(':')
|
||||
device = usb.core.find(idVendor=int(vendor_id, 16), idProduct=int(product_id, 16))
|
||||
device = usb_find(idVendor=int(vendor_id, 16), idProduct=int(product_id, 16))
|
||||
else:
|
||||
device_index = int(spec)
|
||||
devices = list(usb.core.find(
|
||||
find_all = 1,
|
||||
bDeviceClass = USB_DEVICE_CLASS_WIRELESS_CONTROLLER,
|
||||
bDeviceSubClass = USB_DEVICE_SUBCLASS_RF_CONTROLLER,
|
||||
bDeviceProtocol = USB_DEVICE_PROTOCOL_BLUETOOTH_PRIMARY_CONTROLLER
|
||||
))
|
||||
devices = list(
|
||||
usb_find(
|
||||
find_all=1,
|
||||
bDeviceClass=USB_DEVICE_CLASS_WIRELESS_CONTROLLER,
|
||||
bDeviceSubClass=USB_DEVICE_SUBCLASS_RF_CONTROLLER,
|
||||
bDeviceProtocol=USB_DEVICE_PROTOCOL_BLUETOOTH_PRIMARY_CONTROLLER,
|
||||
)
|
||||
)
|
||||
if len(devices) > device_index:
|
||||
device = devices[device_index]
|
||||
else:
|
||||
@@ -232,6 +252,7 @@ async def open_pyusb_transport(spec):
|
||||
|
||||
# Select an alternate setting for SCO, if available
|
||||
sco_enabled = False
|
||||
# pylint: disable=line-too-long
|
||||
# NOTE: this is disabled for now, because SCO with alternate settings is broken,
|
||||
# see: https://github.com/libusb/libusb/issues/36
|
||||
#
|
||||
@@ -273,4 +294,4 @@ async def open_pyusb_transport(spec):
|
||||
packet_source.start()
|
||||
packet_sink.start()
|
||||
|
||||
return UsbTransport(device, packet_source, packet_sink)
|
||||
return UsbTransport(device, packet_source, packet_sink)
|
||||
|
||||
@@ -60,13 +60,12 @@ async def open_serial_transport(spec):
|
||||
device = spec
|
||||
serial_transport, packet_source = await serial_asyncio.create_serial_connection(
|
||||
asyncio.get_running_loop(),
|
||||
lambda: StreamPacketSource(),
|
||||
StreamPacketSource,
|
||||
device,
|
||||
baudrate=speed,
|
||||
rtscts=rtscts,
|
||||
dsrdtr=dsrdtr
|
||||
dsrdtr=dsrdtr,
|
||||
)
|
||||
packet_sink = StreamPacketSink(serial_transport)
|
||||
|
||||
return Transport(packet_source, packet_sink)
|
||||
|
||||
|
||||
@@ -37,13 +37,13 @@ async def open_tcp_client_transport(spec):
|
||||
'''
|
||||
|
||||
class TcpPacketSource(StreamPacketSource):
|
||||
def connection_lost(self, error):
|
||||
logger.debug(f'connection lost: {error}')
|
||||
self.terminated.set_result(error)
|
||||
def connection_lost(self, exc):
|
||||
logger.debug(f'connection lost: {exc}')
|
||||
self.terminated.set_result(exc)
|
||||
|
||||
remote_host, remote_port = spec.split(':')
|
||||
tcp_transport, packet_source = await asyncio.get_running_loop().create_connection(
|
||||
lambda: TcpPacketSource(),
|
||||
TcpPacketSource,
|
||||
host=remote_host,
|
||||
port=int(remote_port),
|
||||
)
|
||||
|
||||
@@ -45,12 +45,12 @@ async def open_tcp_server_transport(spec):
|
||||
class TcpServerProtocol:
|
||||
def __init__(self, packet_source, packet_sink):
|
||||
self.packet_source = packet_source
|
||||
self.packet_sink = packet_sink
|
||||
self.packet_sink = packet_sink
|
||||
|
||||
# Called when a new connection is established
|
||||
def connection_made(self, transport):
|
||||
peername = transport.get_extra_info('peername')
|
||||
logger.debug('connection from {}'.format(peername))
|
||||
peer_name = transport.get_extra_info('peer_name')
|
||||
logger.debug(f'connection from {peer_name}')
|
||||
self.packet_sink.transport = transport
|
||||
|
||||
# Called when the client is disconnected
|
||||
@@ -78,7 +78,7 @@ async def open_tcp_server_transport(spec):
|
||||
|
||||
local_host, local_port = spec.split(':')
|
||||
packet_source = StreamPacketSource()
|
||||
packet_sink = TcpServerPacketSink()
|
||||
packet_sink = TcpServerPacketSink()
|
||||
await asyncio.get_running_loop().create_server(
|
||||
lambda: TcpServerProtocol(packet_source, packet_sink),
|
||||
host=local_host if local_host != '_' else None,
|
||||
|
||||
@@ -53,10 +53,13 @@ async def open_udp_transport(spec):
|
||||
local, remote = spec.split(',')
|
||||
local_host, local_port = local.split(':')
|
||||
remote_host, remote_port = remote.split(':')
|
||||
udp_transport, packet_source = await asyncio.get_running_loop().create_datagram_endpoint(
|
||||
lambda: UdpPacketSource(),
|
||||
(
|
||||
udp_transport,
|
||||
packet_source,
|
||||
) = await asyncio.get_running_loop().create_datagram_endpoint(
|
||||
UdpPacketSource,
|
||||
local_addr=(local_host, int(local_port)),
|
||||
remote_addr=(remote_host, int(remote_port))
|
||||
remote_addr=(remote_host, int(remote_port)),
|
||||
)
|
||||
packet_sink = UdpPacketSink(udp_transport)
|
||||
|
||||
|
||||
@@ -17,13 +17,16 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import logging
|
||||
import usb1
|
||||
import threading
|
||||
import collections
|
||||
from colors import color
|
||||
import ctypes
|
||||
import platform
|
||||
|
||||
import usb1
|
||||
|
||||
from .common import Transport, ParserSource
|
||||
from .. import hci
|
||||
from ..colors import color
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -33,6 +36,30 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def load_libusb():
|
||||
'''
|
||||
Attempt to load the libusb-1.0 C library from libusb_package in site-packages.
|
||||
If the library exists, we create a DLL object and initialize the usb1 backend.
|
||||
This only needs to be done once, but before a usb1.USBContext is created.
|
||||
If the library does not exists, do nothing and usb1 will search default system paths
|
||||
when usb1.USBContext is created.
|
||||
'''
|
||||
try:
|
||||
import libusb_package
|
||||
except ImportError:
|
||||
logger.debug('libusb_package is not available')
|
||||
else:
|
||||
if libusb_path := libusb_package.get_library_path():
|
||||
logger.debug(f'loading libusb library at {libusb_path}')
|
||||
dll_loader = (
|
||||
ctypes.WinDLL if platform.system() == 'Windows' else ctypes.CDLL
|
||||
)
|
||||
libusb_dll = dll_loader(
|
||||
str(libusb_path), use_errno=True, use_last_error=True
|
||||
)
|
||||
usb1.loadLibrary(libusb_dll)
|
||||
|
||||
|
||||
async def open_usb_transport(spec):
|
||||
'''
|
||||
Open a USB transport.
|
||||
@@ -44,48 +71,53 @@ async def open_usb_transport(spec):
|
||||
With <index> as the 0-based index to select amongst all the devices that appear
|
||||
to be supporting Bluetooth HCI (0 being the first one), or
|
||||
Where <vendor> and <product> are the vendor ID and product ID in hexadecimal. The
|
||||
/<serial-number> suffix or #<index> suffix max be specified when more than one device with
|
||||
the same vendor and product identifiers are present.
|
||||
/<serial-number> suffix or #<index> suffix max be specified when more than one
|
||||
device with the same vendor and product identifiers are present.
|
||||
|
||||
In addition, if the moniker ends with the symbol "!", the device will be used in "forced" mode:
|
||||
the first USB interface of the device will be used, regardless of the interface class/subclass.
|
||||
This may be useful for some devices that use a custom class/subclass but may nonetheless work as-is.
|
||||
In addition, if the moniker ends with the symbol "!", the device will be used in
|
||||
"forced" mode:
|
||||
the first USB interface of the device will be used, regardless of the interface
|
||||
class/subclass.
|
||||
This may be useful for some devices that use a custom class/subclass but may
|
||||
nonetheless work as-is.
|
||||
|
||||
Examples:
|
||||
0 --> the first BT USB dongle
|
||||
04b4:f901 --> the BT USB dongle with vendor=04b4 and product=f901
|
||||
04b4:f901#2 --> the third USB device with vendor=04b4 and product=f901
|
||||
04b4:f901/00E04C239987 --> the BT USB dongle with vendor=04b4 and product=f901 and serial number 00E04C239987
|
||||
04b4:f901/00E04C239987 --> the BT USB dongle with vendor=04b4 and product=f901 and
|
||||
serial number 00E04C239987
|
||||
usb:0B05:17CB! --> the BT USB dongle vendor=0B05 and product=17CB, in "forced" mode.
|
||||
'''
|
||||
|
||||
USB_RECIPIENT_DEVICE = 0x00
|
||||
USB_REQUEST_TYPE_CLASS = 0x01 << 5
|
||||
USB_DEVICE_CLASS_DEVICE = 0x00
|
||||
USB_DEVICE_CLASS_WIRELESS_CONTROLLER = 0xE0
|
||||
USB_DEVICE_SUBCLASS_RF_CONTROLLER = 0x01
|
||||
# pylint: disable=invalid-name
|
||||
USB_RECIPIENT_DEVICE = 0x00
|
||||
USB_REQUEST_TYPE_CLASS = 0x01 << 5
|
||||
USB_DEVICE_CLASS_DEVICE = 0x00
|
||||
USB_DEVICE_CLASS_WIRELESS_CONTROLLER = 0xE0
|
||||
USB_DEVICE_SUBCLASS_RF_CONTROLLER = 0x01
|
||||
USB_DEVICE_PROTOCOL_BLUETOOTH_PRIMARY_CONTROLLER = 0x01
|
||||
USB_ENDPOINT_TRANSFER_TYPE_BULK = 0x02
|
||||
USB_ENDPOINT_TRANSFER_TYPE_INTERRUPT = 0x03
|
||||
USB_ENDPOINT_IN = 0x80
|
||||
USB_ENDPOINT_TRANSFER_TYPE_BULK = 0x02
|
||||
USB_ENDPOINT_TRANSFER_TYPE_INTERRUPT = 0x03
|
||||
USB_ENDPOINT_IN = 0x80
|
||||
|
||||
USB_BT_HCI_CLASS_TUPLE = (
|
||||
USB_DEVICE_CLASS_WIRELESS_CONTROLLER,
|
||||
USB_DEVICE_SUBCLASS_RF_CONTROLLER,
|
||||
USB_DEVICE_PROTOCOL_BLUETOOTH_PRIMARY_CONTROLLER
|
||||
USB_DEVICE_PROTOCOL_BLUETOOTH_PRIMARY_CONTROLLER,
|
||||
)
|
||||
|
||||
READ_SIZE = 1024
|
||||
|
||||
class UsbPacketSink:
|
||||
def __init__(self, device, acl_out):
|
||||
self.device = device
|
||||
self.acl_out = acl_out
|
||||
self.transfer = device.getTransfer()
|
||||
self.packets = collections.deque() # Queue of packets waiting to be sent
|
||||
self.loop = asyncio.get_running_loop()
|
||||
self.device = device
|
||||
self.acl_out = acl_out
|
||||
self.transfer = device.getTransfer()
|
||||
self.packets = collections.deque() # Queue of packets waiting to be sent
|
||||
self.loop = asyncio.get_running_loop()
|
||||
self.cancel_done = self.loop.create_future()
|
||||
self.closed = False
|
||||
self.closed = False
|
||||
|
||||
def start(self):
|
||||
pass
|
||||
@@ -109,12 +141,15 @@ async def open_usb_transport(spec):
|
||||
status = transfer.getStatus()
|
||||
# logger.debug(f'<<< USB out transfer callback: status={status}')
|
||||
|
||||
# pylint: disable=no-member
|
||||
if status == usb1.TRANSFER_COMPLETED:
|
||||
self.loop.call_soon_threadsafe(self.on_packet_sent_)
|
||||
elif status == usb1.TRANSFER_CANCELLED:
|
||||
self.loop.call_soon_threadsafe(self.cancel_done.set_result, None)
|
||||
else:
|
||||
logger.warning(color(f'!!! out transfer not completed: status={status}', 'red'))
|
||||
logger.warning(
|
||||
color(f'!!! out transfer not completed: status={status}', 'red')
|
||||
)
|
||||
|
||||
def on_packet_sent_(self):
|
||||
if self.packets:
|
||||
@@ -129,32 +164,38 @@ async def open_usb_transport(spec):
|
||||
packet_type = packet[0]
|
||||
if packet_type == hci.HCI_ACL_DATA_PACKET:
|
||||
self.transfer.setBulk(
|
||||
self.acl_out,
|
||||
packet[1:],
|
||||
callback=self.on_packet_sent
|
||||
self.acl_out, packet[1:], callback=self.on_packet_sent
|
||||
)
|
||||
logger.debug('submit ACL')
|
||||
self.transfer.submit()
|
||||
elif packet_type == hci.HCI_COMMAND_PACKET:
|
||||
self.transfer.setControl(
|
||||
USB_RECIPIENT_DEVICE | USB_REQUEST_TYPE_CLASS, 0, 0, 0,
|
||||
USB_RECIPIENT_DEVICE | USB_REQUEST_TYPE_CLASS,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
packet[1:],
|
||||
callback=self.on_packet_sent
|
||||
callback=self.on_packet_sent,
|
||||
)
|
||||
logger.debug('submit COMMAND')
|
||||
self.transfer.submit()
|
||||
else:
|
||||
logger.warning(color(f'unsupported packet type {packet_type}', 'red'))
|
||||
|
||||
async def close(self):
|
||||
def close(self):
|
||||
self.closed = True
|
||||
|
||||
async def terminate(self):
|
||||
if not self.closed:
|
||||
self.close()
|
||||
|
||||
# Empty the packet queue so that we don't send any more data
|
||||
self.packets.clear()
|
||||
|
||||
# If we have a transfer in flight, cancel it
|
||||
if self.transfer.isSubmitted():
|
||||
# Try to cancel the transfer, but that may fail because it may have already completed
|
||||
# Try to cancel the transfer, but that may fail because it may have
|
||||
# already completed
|
||||
try:
|
||||
self.transfer.cancel()
|
||||
|
||||
@@ -167,18 +208,21 @@ async def open_usb_transport(spec):
|
||||
class UsbPacketSource(asyncio.Protocol, ParserSource):
|
||||
def __init__(self, context, device, acl_in, events_in):
|
||||
super().__init__()
|
||||
self.context = context
|
||||
self.device = device
|
||||
self.acl_in = acl_in
|
||||
self.events_in = events_in
|
||||
self.loop = asyncio.get_running_loop()
|
||||
self.queue = asyncio.Queue()
|
||||
self.closed = False
|
||||
self.context = context
|
||||
self.device = device
|
||||
self.acl_in = acl_in
|
||||
self.events_in = events_in
|
||||
self.loop = asyncio.get_running_loop()
|
||||
self.queue = asyncio.Queue()
|
||||
self.dequeue_task = None
|
||||
self.closed = False
|
||||
self.event_loop_done = self.loop.create_future()
|
||||
self.cancel_done = {
|
||||
hci.HCI_EVENT_PACKET: self.loop.create_future(),
|
||||
hci.HCI_ACL_DATA_PACKET: self.loop.create_future()
|
||||
hci.HCI_EVENT_PACKET: self.loop.create_future(),
|
||||
hci.HCI_ACL_DATA_PACKET: self.loop.create_future(),
|
||||
}
|
||||
self.events_in_transfer = None
|
||||
self.acl_in_transfer = None
|
||||
|
||||
# Create a thread to process events
|
||||
self.event_thread = threading.Thread(target=self.run)
|
||||
@@ -190,7 +234,7 @@ async def open_usb_transport(spec):
|
||||
self.events_in,
|
||||
READ_SIZE,
|
||||
callback=self.on_packet_received,
|
||||
user_data=hci.HCI_EVENT_PACKET
|
||||
user_data=hci.HCI_EVENT_PACKET,
|
||||
)
|
||||
self.events_in_transfer.submit()
|
||||
|
||||
@@ -199,7 +243,7 @@ async def open_usb_transport(spec):
|
||||
self.acl_in,
|
||||
READ_SIZE,
|
||||
callback=self.on_packet_received,
|
||||
user_data=hci.HCI_ACL_DATA_PACKET
|
||||
user_data=hci.HCI_ACL_DATA_PACKET,
|
||||
)
|
||||
self.acl_in_transfer.submit()
|
||||
|
||||
@@ -209,16 +253,28 @@ async def open_usb_transport(spec):
|
||||
def on_packet_received(self, transfer):
|
||||
packet_type = transfer.getUserData()
|
||||
status = transfer.getStatus()
|
||||
# logger.debug(f'<<< USB IN transfer callback: status={status} packet_type={packet_type} length={transfer.getActualLength()}')
|
||||
# logger.debug(
|
||||
# f'<<< USB IN transfer callback: status={status} '
|
||||
# f'packet_type={packet_type} '
|
||||
# f'length={transfer.getActualLength()}'
|
||||
# )
|
||||
|
||||
# pylint: disable=no-member
|
||||
if status == usb1.TRANSFER_COMPLETED:
|
||||
packet = bytes([packet_type]) + transfer.getBuffer()[:transfer.getActualLength()]
|
||||
packet = (
|
||||
bytes([packet_type])
|
||||
+ transfer.getBuffer()[: transfer.getActualLength()]
|
||||
)
|
||||
self.loop.call_soon_threadsafe(self.queue.put_nowait, packet)
|
||||
elif status == usb1.TRANSFER_CANCELLED:
|
||||
self.loop.call_soon_threadsafe(self.cancel_done[packet_type].set_result, None)
|
||||
self.loop.call_soon_threadsafe(
|
||||
self.cancel_done[packet_type].set_result, None
|
||||
)
|
||||
return
|
||||
else:
|
||||
logger.warning(color(f'!!! transfer not completed: status={status}', 'red'))
|
||||
logger.warning(
|
||||
color(f'!!! transfer not completed: status={status}', 'red')
|
||||
)
|
||||
|
||||
# Re-submit the transfer so we can receive more data
|
||||
transfer.submit()
|
||||
@@ -233,7 +289,11 @@ async def open_usb_transport(spec):
|
||||
|
||||
def run(self):
|
||||
logger.debug('starting USB event loop')
|
||||
while self.events_in_transfer.isSubmitted() or self.acl_in_transfer.isSubmitted():
|
||||
while (
|
||||
self.events_in_transfer.isSubmitted()
|
||||
or self.acl_in_transfer.isSubmitted()
|
||||
):
|
||||
# pylint: disable=no-member
|
||||
try:
|
||||
self.context.handleEvents()
|
||||
except usb1.USBErrorInterrupted:
|
||||
@@ -242,22 +302,33 @@ async def open_usb_transport(spec):
|
||||
logger.debug('USB event loop done')
|
||||
self.loop.call_soon_threadsafe(self.event_loop_done.set_result, None)
|
||||
|
||||
async def close(self):
|
||||
def close(self):
|
||||
self.closed = True
|
||||
|
||||
async def terminate(self):
|
||||
if not self.closed:
|
||||
self.close()
|
||||
|
||||
self.dequeue_task.cancel()
|
||||
|
||||
# Cancel the transfers
|
||||
for transfer in (self.events_in_transfer, self.acl_in_transfer):
|
||||
if transfer.isSubmitted():
|
||||
# Try to cancel the transfer, but that may fail because it may have already completed
|
||||
# Try to cancel the transfer, but that may fail because it may have
|
||||
# already completed
|
||||
packet_type = transfer.getUserData()
|
||||
try:
|
||||
transfer.cancel()
|
||||
logger.debug(f'waiting for IN[{packet_type}] transfer cancellation to be done...')
|
||||
logger.debug(
|
||||
f'waiting for IN[{packet_type}] transfer cancellation '
|
||||
'to be done...'
|
||||
)
|
||||
await self.cancel_done[packet_type]
|
||||
logger.debug(f'IN[{packet_type}] transfer cancellation done')
|
||||
except usb1.USBError:
|
||||
logger.debug(f'IN[{packet_type}] transfer likely already completed')
|
||||
logger.debug(
|
||||
f'IN[{packet_type}] transfer likely already completed'
|
||||
)
|
||||
|
||||
# Wait for the thread to terminate
|
||||
await self.event_loop_done
|
||||
@@ -265,8 +336,8 @@ async def open_usb_transport(spec):
|
||||
class UsbTransport(Transport):
|
||||
def __init__(self, context, device, interface, setting, source, sink):
|
||||
super().__init__(source, sink)
|
||||
self.context = context
|
||||
self.device = device
|
||||
self.context = context
|
||||
self.device = device
|
||||
self.interface = interface
|
||||
|
||||
# Get exclusive access
|
||||
@@ -281,13 +352,16 @@ async def open_usb_transport(spec):
|
||||
sink.start()
|
||||
|
||||
async def close(self):
|
||||
await self.source.close()
|
||||
await self.sink.close()
|
||||
self.source.close()
|
||||
self.sink.close()
|
||||
await self.source.terminate()
|
||||
await self.sink.terminate()
|
||||
self.device.releaseInterface(self.interface)
|
||||
self.device.close()
|
||||
self.context.close()
|
||||
|
||||
# Find the device according to the spec moniker
|
||||
load_libusb()
|
||||
context = usb1.USBContext()
|
||||
context.open()
|
||||
try:
|
||||
@@ -315,9 +389,9 @@ async def open_usb_transport(spec):
|
||||
except usb1.USBError:
|
||||
device_serial_number = None
|
||||
if (
|
||||
device.getVendorID() == int(vendor_id, 16) and
|
||||
device.getProductID() == int(product_id, 16) and
|
||||
(serial_number is None or serial_number == device_serial_number)
|
||||
device.getVendorID() == int(vendor_id, 16)
|
||||
and device.getProductID() == int(product_id, 16)
|
||||
and (serial_number is None or serial_number == device_serial_number)
|
||||
):
|
||||
if device_index == 0:
|
||||
found = device
|
||||
@@ -328,8 +402,11 @@ async def open_usb_transport(spec):
|
||||
# Look for a compatible device by index
|
||||
def device_is_bluetooth_hci(device):
|
||||
# Check if the device class indicates a match
|
||||
if (device.getDeviceClass(), device.getDeviceSubClass(), device.getDeviceProtocol()) == \
|
||||
USB_BT_HCI_CLASS_TUPLE:
|
||||
if (
|
||||
device.getDeviceClass(),
|
||||
device.getDeviceSubClass(),
|
||||
device.getDeviceProtocol(),
|
||||
) == USB_BT_HCI_CLASS_TUPLE:
|
||||
return True
|
||||
|
||||
# If the device class is 'Device', look for a matching interface
|
||||
@@ -337,8 +414,11 @@ async def open_usb_transport(spec):
|
||||
for configuration in device:
|
||||
for interface in configuration:
|
||||
for setting in interface:
|
||||
if (setting.getClass(), setting.getSubClass(), setting.getProtocol()) == \
|
||||
USB_BT_HCI_CLASS_TUPLE:
|
||||
if (
|
||||
setting.getClass(),
|
||||
setting.getSubClass(),
|
||||
setting.getProtocol(),
|
||||
) == USB_BT_HCI_CLASS_TUPLE:
|
||||
return True
|
||||
|
||||
return False
|
||||
@@ -360,44 +440,62 @@ async def open_usb_transport(spec):
|
||||
|
||||
# Look for the first interface with the right class and endpoints
|
||||
def find_endpoints(device):
|
||||
# pylint: disable-next=too-many-nested-blocks
|
||||
for (configuration_index, configuration) in enumerate(device):
|
||||
interface = None
|
||||
for interface in configuration:
|
||||
setting = None
|
||||
for setting in interface:
|
||||
if (
|
||||
not forced_mode and
|
||||
(setting.getClass(), setting.getSubClass(), setting.getProtocol()) != USB_BT_HCI_CLASS_TUPLE
|
||||
not forced_mode
|
||||
and (
|
||||
setting.getClass(),
|
||||
setting.getSubClass(),
|
||||
setting.getProtocol(),
|
||||
)
|
||||
!= USB_BT_HCI_CLASS_TUPLE
|
||||
):
|
||||
continue
|
||||
|
||||
events_in = None
|
||||
acl_in = None
|
||||
acl_out = None
|
||||
acl_in = None
|
||||
acl_out = None
|
||||
for endpoint in setting:
|
||||
attributes = endpoint.getAttributes()
|
||||
address = endpoint.getAddress()
|
||||
address = endpoint.getAddress()
|
||||
if attributes & 0x03 == USB_ENDPOINT_TRANSFER_TYPE_BULK:
|
||||
if address & USB_ENDPOINT_IN and acl_in is None:
|
||||
acl_in = address
|
||||
elif acl_out is None:
|
||||
acl_out = address
|
||||
elif attributes & 0x03 == USB_ENDPOINT_TRANSFER_TYPE_INTERRUPT:
|
||||
elif (
|
||||
attributes & 0x03
|
||||
== USB_ENDPOINT_TRANSFER_TYPE_INTERRUPT
|
||||
):
|
||||
if address & USB_ENDPOINT_IN and events_in is None:
|
||||
events_in = address
|
||||
|
||||
# Return if we found all 3 endpoints
|
||||
if acl_in is not None and acl_out is not None and events_in is not None:
|
||||
if (
|
||||
acl_in is not None
|
||||
and acl_out is not None
|
||||
and events_in is not None
|
||||
):
|
||||
return (
|
||||
configuration_index + 1,
|
||||
setting.getNumber(),
|
||||
setting.getAlternateSetting(),
|
||||
acl_in,
|
||||
acl_out,
|
||||
events_in
|
||||
events_in,
|
||||
)
|
||||
else:
|
||||
logger.debug(f'skipping configuration {configuration_index + 1} / interface {setting.getNumber()}')
|
||||
|
||||
logger.debug(
|
||||
f'skipping configuration {configuration_index + 1} / '
|
||||
f'interface {setting.getNumber()}'
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
endpoints = find_endpoints(found)
|
||||
if endpoints is None:
|
||||
@@ -414,14 +512,14 @@ async def open_usb_transport(spec):
|
||||
|
||||
device = found.open()
|
||||
|
||||
# Detach the kernel driver if supported and needed
|
||||
# Auto-detach the kernel driver if supported
|
||||
# pylint: disable=no-member
|
||||
if usb1.hasCapability(usb1.CAP_SUPPORTS_DETACH_KERNEL_DRIVER):
|
||||
try:
|
||||
if device.kernelDriverActive(interface):
|
||||
logger.debug("detaching kernel driver")
|
||||
device.detachKernelDriver(interface)
|
||||
except usb1.USBError:
|
||||
pass
|
||||
logger.debug('auto-detaching kernel driver')
|
||||
device.setAutoDetachKernelDriver(True)
|
||||
except usb1.USBError as error:
|
||||
logger.warning(f'unable to auto-detach kernel driver: {error}')
|
||||
|
||||
# Set the configuration if needed
|
||||
try:
|
||||
@@ -438,7 +536,7 @@ async def open_usb_transport(spec):
|
||||
logger.warning('failed to set configuration')
|
||||
|
||||
source = UsbPacketSource(context, device, acl_in, events_in)
|
||||
sink = UsbPacketSink(device, acl_out)
|
||||
sink = UsbPacketSink(device, acl_out)
|
||||
return UsbTransport(context, device, interface, setting, source, sink)
|
||||
except usb1.USBError as error:
|
||||
logger.warning(color(f'!!! failed to open USB device: {error}', 'red'))
|
||||
|
||||
@@ -33,7 +33,7 @@ async def open_vhci_transport(spec):
|
||||
path at /dev/vhci), or the path of a VHCI device
|
||||
'''
|
||||
|
||||
HCI_VENDOR_PKT = 0xff
|
||||
HCI_VENDOR_PKT = 0xFF
|
||||
HCI_BREDR = 0x00 # Controller type
|
||||
|
||||
# Open the VHCI device
|
||||
@@ -56,4 +56,3 @@ async def open_vhci_transport(spec):
|
||||
transport.sink.on_packet(bytes([HCI_VENDOR_PKT, HCI_BREDR]))
|
||||
|
||||
return transport
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user