mirror of
https://github.com/google/bumble.git
synced 2026-06-01 07:37:02 +00:00
formatting and linting automation
Squashed commits: [cd479ba] formatting and linting automation [7fbfabb] formatting and linting automation [c4f9505] fix after rebase [f506ad4] rename job [441d517] update doc (+7 squashed commits) [2e1b416] fix invoke and github action [6ae5bb4] doc for git blame [44b5461] add GitHub action [b07474f] add docs [4cd9a6f] more linter fixes [db71901] wip [540dc88] wip
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
# Migrate code style to Black
|
||||||
|
135df0dcc01ab765f432e19b1a5202d29bd55545
|
||||||
@@ -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
|
||||||
@@ -8,3 +8,4 @@ test-results.xml
|
|||||||
__pycache__
|
__pycache__
|
||||||
# generated by setuptools_scm
|
# generated by setuptools_scm
|
||||||
bumble/_version.py
|
bumble/_version.py
|
||||||
|
.vscode/launch.json
|
||||||
|
|||||||
Vendored
+75
@@ -0,0 +1,75 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
@@ -199,4 +199,4 @@
|
|||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ Bumble is a full-featured Bluetooth stack written entirely in Python. It support
|
|||||||
|
|
||||||
## Documentation
|
## 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:
|
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
|
## Usage
|
||||||
@@ -29,7 +29,7 @@ For a quick start to using Bumble, see the [Getting Started](docs/mkdocs/src/get
|
|||||||
|
|
||||||
### Dependencies
|
### 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
|
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.
|
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.
|
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
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -47,5 +47,3 @@ NOTE: this assumes you're running a Link Relay on port `10723`.
|
|||||||
|
|
||||||
## `console.py`
|
## `console.py`
|
||||||
A simple text-based-ui interactive Bluetooth device with GATT client capabilities.
|
A simple text-based-ui interactive Bluetooth device with GATT client capabilities.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+93
-61
@@ -29,19 +29,6 @@ from collections import OrderedDict
|
|||||||
import click
|
import click
|
||||||
import colors
|
import colors
|
||||||
|
|
||||||
from bumble import __version__
|
|
||||||
from bumble.core import UUID, AdvertisingData, TimeoutError, BT_LE_TRANSPORT
|
|
||||||
from bumble.device import ConnectionParametersPreferences, Device, Connection, Peer
|
|
||||||
from bumble.utils import AsyncRunner
|
|
||||||
from bumble.transport import open_transport_or_link
|
|
||||||
from bumble.gatt import Characteristic
|
|
||||||
from bumble.hci import (
|
|
||||||
HCI_Constant,
|
|
||||||
HCI_LE_1M_PHY,
|
|
||||||
HCI_LE_2M_PHY,
|
|
||||||
HCI_LE_CODED_PHY,
|
|
||||||
)
|
|
||||||
|
|
||||||
from prompt_toolkit import Application
|
from prompt_toolkit import Application
|
||||||
from prompt_toolkit.history import FileHistory
|
from prompt_toolkit.history import FileHistory
|
||||||
from prompt_toolkit.completion import Completer, Completion, NestedCompleter
|
from prompt_toolkit.completion import Completer, Completion, NestedCompleter
|
||||||
@@ -64,6 +51,21 @@ from prompt_toolkit.layout import (
|
|||||||
Dimension,
|
Dimension,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from bumble import __version__
|
||||||
|
import bumble.core
|
||||||
|
from bumble.core import UUID, AdvertisingData, BT_LE_TRANSPORT
|
||||||
|
from bumble.device import ConnectionParametersPreferences, Device, Connection, Peer
|
||||||
|
from bumble.utils import AsyncRunner
|
||||||
|
from bumble.transport import open_transport_or_link
|
||||||
|
from bumble.gatt import Characteristic
|
||||||
|
from bumble.hci import (
|
||||||
|
HCI_Constant,
|
||||||
|
HCI_LE_1M_PHY,
|
||||||
|
HCI_LE_2M_PHY,
|
||||||
|
HCI_LE_CODED_PHY,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Constants
|
# Constants
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -75,12 +77,6 @@ DISPLAY_MAX_RSSI = -30
|
|||||||
RSSI_MONITOR_INTERVAL = 5.0 # Seconds
|
RSSI_MONITOR_INTERVAL = 5.0 # Seconds
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Globals
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
App = None
|
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Utils
|
# Utils
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -104,19 +100,19 @@ def rssi_bar(rssi):
|
|||||||
def parse_phys(phys):
|
def parse_phys(phys):
|
||||||
if phys.lower() == '*':
|
if phys.lower() == '*':
|
||||||
return None
|
return None
|
||||||
else:
|
|
||||||
phy_list = []
|
phy_list = []
|
||||||
elements = phys.lower().split(',')
|
elements = phys.lower().split(',')
|
||||||
for element in elements:
|
for element in elements:
|
||||||
if element == '1m':
|
if element == '1m':
|
||||||
phy_list.append(HCI_LE_1M_PHY)
|
phy_list.append(HCI_LE_1M_PHY)
|
||||||
elif element == '2m':
|
elif element == '2m':
|
||||||
phy_list.append(HCI_LE_2M_PHY)
|
phy_list.append(HCI_LE_2M_PHY)
|
||||||
elif element == 'coded':
|
elif element == 'coded':
|
||||||
phy_list.append(HCI_LE_CODED_PHY)
|
phy_list.append(HCI_LE_CODED_PHY)
|
||||||
else:
|
else:
|
||||||
raise ValueError('invalid PHY name')
|
raise ValueError('invalid PHY name')
|
||||||
return phy_list
|
return phy_list
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -252,15 +248,16 @@ class ConsoleApp:
|
|||||||
|
|
||||||
layout = Layout(container, focused_element=self.input_field)
|
layout = Layout(container, focused_element=self.input_field)
|
||||||
|
|
||||||
kb = KeyBindings()
|
key_bindings = KeyBindings()
|
||||||
|
|
||||||
@kb.add("c-c")
|
@key_bindings.add("c-c")
|
||||||
@kb.add("c-q")
|
@key_bindings.add("c-q")
|
||||||
def _(event):
|
def _(event):
|
||||||
event.app.exit()
|
event.app.exit()
|
||||||
|
|
||||||
|
# pylint: disable=invalid-name
|
||||||
self.ui = Application(
|
self.ui = Application(
|
||||||
layout=layout, style=style, key_bindings=kb, full_screen=True
|
layout=layout, style=style, key_bindings=key_bindings, full_screen=True
|
||||||
)
|
)
|
||||||
|
|
||||||
async def run_async(self, device_config, transport):
|
async def run_async(self, device_config, transport):
|
||||||
@@ -275,8 +272,8 @@ class ConsoleApp:
|
|||||||
random_address = (
|
random_address = (
|
||||||
f"{random.randint(192,255):02X}" # address is static random
|
f"{random.randint(192,255):02X}" # address is static random
|
||||||
)
|
)
|
||||||
for c in random.sample(range(255), 5):
|
for random_byte in random.sample(range(255), 5):
|
||||||
random_address += f":{c:02X}"
|
random_address += f":{random_byte:02X}"
|
||||||
self.append_to_log(f"Setting random address: {random_address}")
|
self.append_to_log(f"Setting random address: {random_address}")
|
||||||
self.device = Device.with_hci(
|
self.device = Device.with_hci(
|
||||||
'Bumble', random_address, hci_source, hci_sink
|
'Bumble', random_address, hci_source, hci_sink
|
||||||
@@ -293,7 +290,7 @@ class ConsoleApp:
|
|||||||
def add_known_address(self, address):
|
def add_known_address(self, address):
|
||||||
self.known_addresses.add(address)
|
self.known_addresses.add(address)
|
||||||
|
|
||||||
def accept_input(self, buff):
|
def accept_input(self, _):
|
||||||
if len(self.input_field.text) == 0:
|
if len(self.input_field.text) == 0:
|
||||||
return
|
return
|
||||||
self.append_to_output([('', '* '), ('ansicyan', self.input_field.text)], False)
|
self.append_to_output([('', '* '), ('ansicyan', self.input_field.text)], False)
|
||||||
@@ -312,12 +309,24 @@ class ConsoleApp:
|
|||||||
connection_state = 'CONNECTING'
|
connection_state = 'CONNECTING'
|
||||||
elif self.connected_peer:
|
elif self.connected_peer:
|
||||||
connection = self.connected_peer.connection
|
connection = self.connected_peer.connection
|
||||||
connection_parameters = f'{connection.parameters.connection_interval}/{connection.parameters.peripheral_latency}/{connection.parameters.supervision_timeout}'
|
connection_parameters = (
|
||||||
|
f'{connection.parameters.connection_interval}/'
|
||||||
|
f'{connection.parameters.peripheral_latency}/'
|
||||||
|
f'{connection.parameters.supervision_timeout}'
|
||||||
|
)
|
||||||
if connection.transport == BT_LE_TRANSPORT:
|
if connection.transport == BT_LE_TRANSPORT:
|
||||||
phy_state = f' RX={le_phy_name(connection.phy.rx_phy)}/TX={le_phy_name(connection.phy.tx_phy)}'
|
phy_state = (
|
||||||
|
f' RX={le_phy_name(connection.phy.rx_phy)}/'
|
||||||
|
f'TX={le_phy_name(connection.phy.tx_phy)}'
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
phy_state = ''
|
phy_state = ''
|
||||||
connection_state = f'{connection.peer_address} {connection_parameters} {connection.data_length}{phy_state}'
|
connection_state = (
|
||||||
|
f'{connection.peer_address} '
|
||||||
|
f'{connection_parameters} '
|
||||||
|
f'{connection.data_length}'
|
||||||
|
f'{phy_state}'
|
||||||
|
)
|
||||||
encryption_state = (
|
encryption_state = (
|
||||||
'ENCRYPTED' if connection.is_encrypted else 'NOT ENCRYPTED'
|
'ENCRYPTED' if connection.is_encrypted else 'NOT ENCRYPTED'
|
||||||
)
|
)
|
||||||
@@ -410,7 +419,10 @@ class ConsoleApp:
|
|||||||
advertising_interval = (
|
advertising_interval = (
|
||||||
device.advertising_interval_min
|
device.advertising_interval_min
|
||||||
if device.advertising_interval_min == device.advertising_interval_max
|
if device.advertising_interval_min == device.advertising_interval_max
|
||||||
else f"{device.advertising_interval_min} to {device.advertising_interval_max}"
|
else (
|
||||||
|
f'{device.advertising_interval_min} to '
|
||||||
|
f'{device.advertising_interval_max}'
|
||||||
|
)
|
||||||
)
|
)
|
||||||
lines.append(('ansicyan', 'Advertising Interval: '))
|
lines.append(('ansicyan', 'Advertising Interval: '))
|
||||||
lines.append(('', f'{advertising_interval}\n'))
|
lines.append(('', f'{advertising_interval}\n'))
|
||||||
@@ -419,7 +431,7 @@ class ConsoleApp:
|
|||||||
self.ui.invalidate()
|
self.ui.invalidate()
|
||||||
|
|
||||||
def append_to_output(self, line, invalidate=True):
|
def append_to_output(self, line, invalidate=True):
|
||||||
if type(line) is str:
|
if isinstance(line, str):
|
||||||
line = [('', line)]
|
line = [('', line)]
|
||||||
self.output_lines = self.output_lines[-self.output_max_lines :]
|
self.output_lines = self.output_lines[-self.output_max_lines :]
|
||||||
self.output_lines.append(line)
|
self.output_lines.append(line)
|
||||||
@@ -489,6 +501,8 @@ class ConsoleApp:
|
|||||||
if characteristic.handle == attribute_handle:
|
if characteristic.handle == attribute_handle:
|
||||||
return characteristic
|
return characteristic
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
async def rssi_monitor_loop(self):
|
async def rssi_monitor_loop(self):
|
||||||
while True:
|
while True:
|
||||||
if self.monitor_rssi and self.connected_peer:
|
if self.monitor_rssi and self.connected_peer:
|
||||||
@@ -520,7 +534,8 @@ class ConsoleApp:
|
|||||||
if not params[1].startswith("filter="):
|
if not params[1].startswith("filter="):
|
||||||
self.show_error(
|
self.show_error(
|
||||||
'invalid syntax',
|
'invalid syntax',
|
||||||
'expected address filter=key1:value1,key2:value,... available filters: address',
|
'expected address filter=key1:value1,key2:value,... '
|
||||||
|
'available filters: address',
|
||||||
)
|
)
|
||||||
# regex: (word):(any char except ,)
|
# regex: (word):(any char except ,)
|
||||||
matches = re.findall(r"(\w+):([^,]+)", params[1])
|
matches = re.findall(r"(\w+):([^,]+)", params[1])
|
||||||
@@ -578,10 +593,10 @@ class ConsoleApp:
|
|||||||
timeout=DEFAULT_CONNECTION_TIMEOUT,
|
timeout=DEFAULT_CONNECTION_TIMEOUT,
|
||||||
)
|
)
|
||||||
self.top_tab = 'services'
|
self.top_tab = 'services'
|
||||||
except TimeoutError:
|
except bumble.core.TimeoutError:
|
||||||
self.show_error('connection timed out')
|
self.show_error('connection timed out')
|
||||||
|
|
||||||
async def do_disconnect(self, params):
|
async def do_disconnect(self, _):
|
||||||
if self.device.is_le_connecting:
|
if self.device.is_le_connecting:
|
||||||
await self.device.cancel_connection()
|
await self.device.cancel_connection()
|
||||||
else:
|
else:
|
||||||
@@ -595,7 +610,8 @@ class ConsoleApp:
|
|||||||
if len(params) != 1 or len(params[0].split('/')) != 3:
|
if len(params) != 1 or len(params[0].split('/')) != 3:
|
||||||
self.show_error(
|
self.show_error(
|
||||||
'invalid syntax',
|
'invalid syntax',
|
||||||
'expected update-parameters <interval-min>-<interval-max>/<max-latency>/<supervision>',
|
'expected update-parameters <interval-min>-<interval-max>'
|
||||||
|
'/<max-latency>/<supervision>',
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -616,7 +632,7 @@ class ConsoleApp:
|
|||||||
supervision_timeout,
|
supervision_timeout,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def do_encrypt(self, params):
|
async def do_encrypt(self, _):
|
||||||
if not self.connected_peer:
|
if not self.connected_peer:
|
||||||
self.show_error('not connected')
|
self.show_error('not connected')
|
||||||
return
|
return
|
||||||
@@ -643,14 +659,15 @@ class ConsoleApp:
|
|||||||
self.top_tab = params[0]
|
self.top_tab = params[0]
|
||||||
self.ui.invalidate()
|
self.ui.invalidate()
|
||||||
|
|
||||||
async def do_get_phy(self, params):
|
async def do_get_phy(self, _):
|
||||||
if not self.connected_peer:
|
if not self.connected_peer:
|
||||||
self.show_error('not connected')
|
self.show_error('not connected')
|
||||||
return
|
return
|
||||||
|
|
||||||
phy = await self.connected_peer.connection.get_phy()
|
phy = await self.connected_peer.connection.get_phy()
|
||||||
self.append_to_output(
|
self.append_to_output(
|
||||||
f'PHY: RX={HCI_Constant.le_phy_name(phy[0])}, TX={HCI_Constant.le_phy_name(phy[1])}'
|
f'PHY: RX={HCI_Constant.le_phy_name(phy[0])}, '
|
||||||
|
f'TX={HCI_Constant.le_phy_name(phy[1])}'
|
||||||
)
|
)
|
||||||
|
|
||||||
async def do_request_mtu(self, params):
|
async def do_request_mtu(self, params):
|
||||||
@@ -793,10 +810,10 @@ class ConsoleApp:
|
|||||||
tx_phys=parse_phys(tx_phys), rx_phys=parse_phys(rx_phys)
|
tx_phys=parse_phys(tx_phys), rx_phys=parse_phys(rx_phys)
|
||||||
)
|
)
|
||||||
|
|
||||||
async def do_exit(self, params):
|
async def do_exit(self, _):
|
||||||
self.ui.exit()
|
self.ui.exit()
|
||||||
|
|
||||||
async def do_quit(self, params):
|
async def do_quit(self, _):
|
||||||
self.ui.exit()
|
self.ui.exit()
|
||||||
|
|
||||||
async def do_filter(self, params):
|
async def do_filter(self, params):
|
||||||
@@ -827,7 +844,7 @@ class DeviceListener(Device.Listener, Connection.Listener):
|
|||||||
else:
|
else:
|
||||||
self._address_filter = re.compile(filter_addr)
|
self._address_filter = re.compile(filter_addr)
|
||||||
self.scan_results = OrderedDict(
|
self.scan_results = OrderedDict(
|
||||||
filter(lambda x: self.filter_address_match(x), self.scan_results)
|
filter(self.filter_address_match, self.scan_results)
|
||||||
)
|
)
|
||||||
self.app.show_scan_results(self.scan_results)
|
self.app.show_scan_results(self.scan_results)
|
||||||
|
|
||||||
@@ -838,6 +855,7 @@ class DeviceListener(Device.Listener, Connection.Listener):
|
|||||||
return bool(self.address_filter.match(address))
|
return bool(self.address_filter.match(address))
|
||||||
|
|
||||||
@AsyncRunner.run_in_task()
|
@AsyncRunner.run_in_task()
|
||||||
|
# pylint: disable=invalid-overridden-method
|
||||||
async def on_connection(self, connection):
|
async def on_connection(self, connection):
|
||||||
self.app.connected_peer = Peer(connection)
|
self.app.connected_peer = Peer(connection)
|
||||||
self.app.connection_rssi = None
|
self.app.connection_rssi = None
|
||||||
@@ -846,14 +864,16 @@ class DeviceListener(Device.Listener, Connection.Listener):
|
|||||||
|
|
||||||
def on_disconnection(self, reason):
|
def on_disconnection(self, reason):
|
||||||
self.app.append_to_output(
|
self.app.append_to_output(
|
||||||
f'disconnected from {self.app.connected_peer}, reason: {HCI_Constant.error_name(reason)}'
|
f'disconnected from {self.app.connected_peer}, '
|
||||||
|
f'reason: {HCI_Constant.error_name(reason)}'
|
||||||
)
|
)
|
||||||
self.app.connected_peer = None
|
self.app.connected_peer = None
|
||||||
self.app.connection_rssi = None
|
self.app.connection_rssi = None
|
||||||
|
|
||||||
def on_connection_parameters_update(self):
|
def on_connection_parameters_update(self):
|
||||||
self.app.append_to_output(
|
self.app.append_to_output(
|
||||||
f'connection parameters update: {self.app.connected_peer.connection.parameters}'
|
f'connection parameters update: '
|
||||||
|
f'{self.app.connected_peer.connection.parameters}'
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_connection_phy_update(self):
|
def on_connection_phy_update(self):
|
||||||
@@ -867,13 +887,19 @@ class DeviceListener(Device.Listener, Connection.Listener):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def on_connection_encryption_change(self):
|
def on_connection_encryption_change(self):
|
||||||
|
encryption_state = (
|
||||||
|
'encrypted'
|
||||||
|
if self.app.connected_peer.connection.is_encrypted
|
||||||
|
else 'not encrypted'
|
||||||
|
)
|
||||||
self.app.append_to_output(
|
self.app.append_to_output(
|
||||||
f'connection encryption change: {"encrypted" if self.app.connected_peer.connection.is_encrypted else "not encrypted"}'
|
'connection encryption change: ' f'{encryption_state}'
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_connection_data_length_change(self):
|
def on_connection_data_length_change(self):
|
||||||
self.app.append_to_output(
|
self.app.append_to_output(
|
||||||
f'connection data length change: {self.app.connected_peer.connection.data_length}'
|
'connection data length change: '
|
||||||
|
f'{self.app.connected_peer.connection.data_length}'
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_advertisement(self, advertisement):
|
def on_advertisement(self, advertisement):
|
||||||
@@ -930,10 +956,16 @@ class ScanResult:
|
|||||||
else:
|
else:
|
||||||
name = ''
|
name = ''
|
||||||
|
|
||||||
|
# Remove any '/P' qualifier suffix from the address string
|
||||||
|
address_str = str(self.address).replace('/P', '')
|
||||||
|
|
||||||
# RSSI bar
|
# RSSI bar
|
||||||
bar_string = rssi_bar(self.rssi)
|
bar_string = rssi_bar(self.rssi)
|
||||||
bar_padding = ' ' * (DEFAULT_RSSI_BAR_WIDTH + 5 - len(bar_string))
|
bar_padding = ' ' * (DEFAULT_RSSI_BAR_WIDTH + 5 - len(bar_string))
|
||||||
return f'{address_color(str(self.address))} [{type_color(address_type_string)}] {bar_string} {bar_padding} {name}'
|
return (
|
||||||
|
f'{address_color(address_str)} [{type_color(address_type_string)}] '
|
||||||
|
f'{bar_string} {bar_padding} {name}'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -961,7 +993,7 @@ def main(device_config, transport):
|
|||||||
if not os.path.isdir(BUMBLE_USER_DIR):
|
if not os.path.isdir(BUMBLE_USER_DIR):
|
||||||
os.mkdir(BUMBLE_USER_DIR)
|
os.mkdir(BUMBLE_USER_DIR)
|
||||||
|
|
||||||
# Create an instane of the app
|
# Create an instance of the app
|
||||||
app = ConsoleApp()
|
app = ConsoleApp()
|
||||||
|
|
||||||
# Setup logging
|
# Setup logging
|
||||||
@@ -978,4 +1010,4 @@ def main(device_config, transport):
|
|||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main() # pylint: disable=no-value-for-parameter
|
||||||
|
|||||||
+3
-2
@@ -29,12 +29,13 @@ from bumble.transport import open_transport_or_link
|
|||||||
async def async_main():
|
async def async_main():
|
||||||
if len(sys.argv) != 3:
|
if len(sys.argv) != 3:
|
||||||
print(
|
print(
|
||||||
'Usage: controllers.py <hci-transport-1> <hci-transport-2> [<hci-transport-3> ...]'
|
'Usage: controllers.py <hci-transport-1> <hci-transport-2> '
|
||||||
|
'[<hci-transport-3> ...]'
|
||||||
)
|
)
|
||||||
print('example: python controllers.py pty:ble1 pty:ble2')
|
print('example: python controllers.py pty:ble1 pty:ble2')
|
||||||
return
|
return
|
||||||
|
|
||||||
# Create a loccal link to attach the controllers to
|
# Create a local link to attach the controllers to
|
||||||
link = LocalLink()
|
link = LocalLink()
|
||||||
|
|
||||||
# Create a transport and controller for all requested names
|
# Create a transport and controller for all requested names
|
||||||
|
|||||||
+3
-3
@@ -21,7 +21,7 @@ import logging
|
|||||||
import click
|
import click
|
||||||
from colors import color
|
from colors import color
|
||||||
|
|
||||||
from bumble.core import ProtocolError, TimeoutError
|
import bumble.core
|
||||||
from bumble.device import Device, Peer
|
from bumble.device import Device, Peer
|
||||||
from bumble.gatt import show_services
|
from bumble.gatt import show_services
|
||||||
from bumble.transport import open_transport_or_link
|
from bumble.transport import open_transport_or_link
|
||||||
@@ -49,9 +49,9 @@ async def dump_gatt_db(peer, done):
|
|||||||
try:
|
try:
|
||||||
value = await attribute.read_value()
|
value = await attribute.read_value()
|
||||||
print(color(f'{value.hex()}', 'green'))
|
print(color(f'{value.hex()}', 'green'))
|
||||||
except ProtocolError as error:
|
except bumble.core.ProtocolError as error:
|
||||||
print(color(error, 'red'))
|
print(color(error, 'red'))
|
||||||
except TimeoutError:
|
except bumble.core.TimeoutError:
|
||||||
print(color('read timeout', 'red'))
|
print(color('read timeout', 'red'))
|
||||||
|
|
||||||
if done is not None:
|
if done is not None:
|
||||||
|
|||||||
+12
-6
@@ -99,6 +99,7 @@ class GattlinkHubBridge(GattlinkL2capEndpoint, Device.Listener):
|
|||||||
print(color(f'!!! Connection failed: {error}', 'red'))
|
print(color(f'!!! Connection failed: {error}', 'red'))
|
||||||
|
|
||||||
@AsyncRunner.run_in_task()
|
@AsyncRunner.run_in_task()
|
||||||
|
# pylint: disable=invalid-overridden-method
|
||||||
async def on_connection(self, connection):
|
async def on_connection(self, connection):
|
||||||
print(f'=== Connected to {connection}')
|
print(f'=== Connected to {connection}')
|
||||||
self.peer = Peer(connection)
|
self.peer = Peer(connection)
|
||||||
@@ -158,7 +159,8 @@ class GattlinkHubBridge(GattlinkL2capEndpoint, Device.Listener):
|
|||||||
def on_disconnection(self, reason):
|
def on_disconnection(self, reason):
|
||||||
print(
|
print(
|
||||||
color(
|
color(
|
||||||
f'!!! Disconnected from {self.peer}, reason={HCI_Constant.error_name(reason)}',
|
f'!!! Disconnected from {self.peer}, '
|
||||||
|
f'reason={HCI_Constant.error_name(reason)}',
|
||||||
'red',
|
'red',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -189,7 +191,7 @@ class GattlinkHubBridge(GattlinkL2capEndpoint, Device.Listener):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
# Called by asyncio when a UDP datagram is received
|
# Called by asyncio when a UDP datagram is received
|
||||||
def datagram_received(self, data, address):
|
def datagram_received(self, data, _address):
|
||||||
print(color(f'<<< [UDP]: {len(data)} bytes', 'green'))
|
print(color(f'<<< [UDP]: {len(data)} bytes', 'green'))
|
||||||
|
|
||||||
if self.l2cap_channel:
|
if self.l2cap_channel:
|
||||||
@@ -209,6 +211,7 @@ class GattlinkNodeBridge(GattlinkL2capEndpoint, Device.Listener):
|
|||||||
self.tx_socket = None
|
self.tx_socket = None
|
||||||
self.tx_subscriber = None
|
self.tx_subscriber = None
|
||||||
self.rx_characteristic = None
|
self.rx_characteristic = None
|
||||||
|
self.transport = None
|
||||||
|
|
||||||
# Register as a listener
|
# Register as a listener
|
||||||
device.listener = self
|
device.listener = self
|
||||||
@@ -264,7 +267,7 @@ class GattlinkNodeBridge(GattlinkL2capEndpoint, Device.Listener):
|
|||||||
self.transport = transport
|
self.transport = transport
|
||||||
|
|
||||||
# Called by asyncio when a UDP datagram is received
|
# Called by asyncio when a UDP datagram is received
|
||||||
def datagram_received(self, data, address):
|
def datagram_received(self, data, _address):
|
||||||
print(color(f'<<< [UDP]: {len(data)} bytes', 'green'))
|
print(color(f'<<< [UDP]: {len(data)} bytes', 'green'))
|
||||||
|
|
||||||
if self.l2cap_channel:
|
if self.l2cap_channel:
|
||||||
@@ -276,7 +279,7 @@ class GattlinkNodeBridge(GattlinkL2capEndpoint, Device.Listener):
|
|||||||
asyncio.create_task(self.device.notify_subscribers(self.tx_characteristic))
|
asyncio.create_task(self.device.notify_subscribers(self.tx_characteristic))
|
||||||
|
|
||||||
# Called when a write to the RX characteristic has been received
|
# Called when a write to the RX characteristic has been received
|
||||||
def on_rx_write(self, connection, data):
|
def on_rx_write(self, _connection, data):
|
||||||
print(color(f'<<< [GATT RX]: {len(data)} bytes', 'cyan'))
|
print(color(f'<<< [GATT RX]: {len(data)} bytes', 'cyan'))
|
||||||
print(color('>>> [UDP]', 'magenta'))
|
print(color('>>> [UDP]', 'magenta'))
|
||||||
self.tx_socket.sendto(data)
|
self.tx_socket.sendto(data)
|
||||||
@@ -284,7 +287,8 @@ class GattlinkNodeBridge(GattlinkL2capEndpoint, Device.Listener):
|
|||||||
# Called when the subscription to the TX characteristic has changed
|
# Called when the subscription to the TX characteristic has changed
|
||||||
def on_tx_subscription(self, peer, enabled):
|
def on_tx_subscription(self, peer, enabled):
|
||||||
print(
|
print(
|
||||||
f'### [GATT TX] subscription from {peer}: {"enabled" if enabled else "disabled"}'
|
f'### [GATT TX] subscription from {peer}: '
|
||||||
|
f'{"enabled" if enabled else "disabled"}'
|
||||||
)
|
)
|
||||||
if enabled:
|
if enabled:
|
||||||
self.tx_subscriber = peer
|
self.tx_subscriber = peer
|
||||||
@@ -335,7 +339,9 @@ async def run(
|
|||||||
|
|
||||||
# Create a UDP to TX bridge (receive from TX, send to UDP)
|
# Create a UDP to TX bridge (receive from TX, send to UDP)
|
||||||
bridge.tx_socket, _ = await loop.create_datagram_endpoint(
|
bridge.tx_socket, _ = await loop.create_datagram_endpoint(
|
||||||
lambda: asyncio.DatagramProtocol(), remote_addr=(send_host, send_port)
|
# pylint: disable-next=unnecessary-lambda
|
||||||
|
lambda: asyncio.DatagramProtocol(),
|
||||||
|
remote_addr=(send_host, send_port),
|
||||||
)
|
)
|
||||||
|
|
||||||
await device.power_on()
|
await device.power_on()
|
||||||
|
|||||||
+7
-2
@@ -35,10 +35,13 @@ logger = logging.getLogger(__name__)
|
|||||||
async def async_main():
|
async def async_main():
|
||||||
if len(sys.argv) < 3:
|
if len(sys.argv) < 3:
|
||||||
print(
|
print(
|
||||||
'Usage: hci_bridge.py <host-transport-spec> <controller-transport-spec> [command-short-circuit-list]'
|
'Usage: hci_bridge.py <host-transport-spec> <controller-transport-spec> '
|
||||||
|
'[command-short-circuit-list]'
|
||||||
)
|
)
|
||||||
print(
|
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'
|
'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
|
return
|
||||||
|
|
||||||
@@ -82,6 +85,8 @@ async def async_main():
|
|||||||
# Return a packet with 'respond to sender' set to True
|
# Return a packet with 'respond to sender' set to True
|
||||||
return (response.to_bytes(), True)
|
return (response.to_bytes(), True)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
_ = HCI_Bridge(
|
_ = HCI_Bridge(
|
||||||
hci_host_source,
|
hci_host_source,
|
||||||
hci_host_sink,
|
hci_host_sink,
|
||||||
|
|||||||
@@ -16,9 +16,9 @@
|
|||||||
# Imports
|
# Imports
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
import asyncio
|
import asyncio
|
||||||
import click
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import click
|
||||||
from colors import color
|
from colors import color
|
||||||
|
|
||||||
from bumble.transport import open_transport_or_link
|
from bumble.transport import open_transport_or_link
|
||||||
@@ -89,7 +89,8 @@ class ServerBridge:
|
|||||||
# Connect to the TCP server
|
# Connect to the TCP server
|
||||||
print(
|
print(
|
||||||
color(
|
color(
|
||||||
f'### Connecting to TCP {self.bridge.tcp_host}:{self.bridge.tcp_port}...',
|
f'### Connecting to TCP {self.bridge.tcp_host}:'
|
||||||
|
f'{self.bridge.tcp_port}...',
|
||||||
'yellow',
|
'yellow',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -98,8 +99,8 @@ class ServerBridge:
|
|||||||
def __init__(self, pipe):
|
def __init__(self, pipe):
|
||||||
self.pipe = pipe
|
self.pipe = pipe
|
||||||
|
|
||||||
def connection_lost(self, error):
|
def connection_lost(self, exc):
|
||||||
print(color(f'!!! TCP connection lost: {error}', 'red'))
|
print(color(f'!!! TCP connection lost: {exc}', 'red'))
|
||||||
if self.pipe.l2cap_channel is not None:
|
if self.pipe.l2cap_channel is not None:
|
||||||
asyncio.create_task(self.pipe.l2cap_channel.disconnect())
|
asyncio.create_task(self.pipe.l2cap_channel.disconnect())
|
||||||
|
|
||||||
@@ -178,8 +179,8 @@ class ClientBridge:
|
|||||||
|
|
||||||
# Called when a TCP connection is established
|
# Called when a TCP connection is established
|
||||||
async def on_tcp_connection(reader, writer):
|
async def on_tcp_connection(reader, writer):
|
||||||
peername = writer.get_extra_info('peername')
|
peer_name = writer.get_extra_info('peer_name')
|
||||||
print(color(f'<<< TCP connection from {peername}', 'magenta'))
|
print(color(f'<<< TCP connection from {peer_name}', 'magenta'))
|
||||||
|
|
||||||
def on_coc_sdu(sdu):
|
def on_coc_sdu(sdu):
|
||||||
print(color(f'<<< [L2CAP SDU]: {len(sdu)} bytes', 'cyan'))
|
print(color(f'<<< [L2CAP SDU]: {len(sdu)} bytes', 'cyan'))
|
||||||
@@ -346,4 +347,4 @@ def client(context, bluetooth_address, tcp_host, tcp_port):
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
|
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
cli(obj={})
|
cli(obj={}) # pylint: disable=no-value-for-parameter
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
# Imports
|
# Imports
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
import sys
|
import sys
|
||||||
import websockets
|
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
import asyncio
|
import asyncio
|
||||||
@@ -25,6 +24,7 @@ import uuid
|
|||||||
import os
|
import os
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
from colors import color
|
from colors import color
|
||||||
|
import websockets
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Logging
|
# Logging
|
||||||
@@ -98,7 +98,11 @@ class Connection:
|
|||||||
self.address = address
|
self.address = address
|
||||||
|
|
||||||
def __str__(self):
|
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]})'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
@@ -139,8 +143,8 @@ class Room:
|
|||||||
|
|
||||||
# Parse the message to decide how to handle it
|
# Parse the message to decide how to handle it
|
||||||
if message.startswith('@'):
|
if message.startswith('@'):
|
||||||
# This is a targetted message
|
# This is a targeted message
|
||||||
await self.on_targetted_message(connection, message)
|
await self.on_targeted_message(connection, message)
|
||||||
elif message.startswith('/'):
|
elif message.startswith('/'):
|
||||||
# This is an RPC request
|
# This is an RPC request
|
||||||
await self.on_rpc_request(connection, message)
|
await self.on_rpc_request(connection, message)
|
||||||
@@ -169,7 +173,7 @@ class Room:
|
|||||||
|
|
||||||
await connection.send_message(result or 'result:{}')
|
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)
|
target, *payload = message.split(' ', 1)
|
||||||
if not payload:
|
if not payload:
|
||||||
return error_to_json('missing arguments')
|
return error_to_json('missing arguments')
|
||||||
@@ -178,7 +182,8 @@ class Room:
|
|||||||
|
|
||||||
# Determine what targets to send to
|
# Determine what targets to send to
|
||||||
if target == '*':
|
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]
|
connections = [c for c in self.connections if c != connection]
|
||||||
else:
|
else:
|
||||||
connections = self.find_connections_by_address(target)
|
connections = self.find_connections_by_address(target)
|
||||||
@@ -216,9 +221,10 @@ class Relay:
|
|||||||
def start(self):
|
def start(self):
|
||||||
logger.info(f'Starting Relay on port {self.port}')
|
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)
|
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
|
pass
|
||||||
|
|
||||||
async def serve(self, websocket, path):
|
async def serve(self, websocket, path):
|
||||||
@@ -265,7 +271,7 @@ def main():
|
|||||||
|
|
||||||
# Setup logger
|
# Setup logger
|
||||||
if args.log_config:
|
if args.log_config:
|
||||||
from logging import config
|
from logging import config # pylint: disable=import-outside-toplevel
|
||||||
|
|
||||||
config.fileConfig(args.log_config)
|
config.fileConfig(args.log_config)
|
||||||
else:
|
else:
|
||||||
|
|||||||
+28
-22
@@ -86,15 +86,17 @@ class Delegate(PairingDelegate):
|
|||||||
while True:
|
while True:
|
||||||
response = await aioconsole.ainput(color('>>> Accept? ', 'yellow'))
|
response = await aioconsole.ainput(color('>>> Accept? ', 'yellow'))
|
||||||
response = response.lower().strip()
|
response = response.lower().strip()
|
||||||
|
|
||||||
if response == 'yes':
|
if response == 'yes':
|
||||||
return True
|
return True
|
||||||
elif response == 'no':
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
# Accept silently
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def compare_numbers(self, number, digits):
|
if response == 'no':
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Accept silently
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def compare_numbers(self, number, digits=6):
|
||||||
await self.update_peer_name()
|
await self.update_peer_name()
|
||||||
|
|
||||||
# Wait a bit to allow some of the log lines to print before we prompt
|
# Wait a bit to allow some of the log lines to print before we prompt
|
||||||
@@ -111,9 +113,11 @@ class Delegate(PairingDelegate):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
response = response.lower().strip()
|
response = response.lower().strip()
|
||||||
|
|
||||||
if response == 'yes':
|
if response == 'yes':
|
||||||
return True
|
return True
|
||||||
elif response == 'no':
|
|
||||||
|
if response == 'no':
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def get_number(self):
|
async def get_number(self):
|
||||||
@@ -132,7 +136,7 @@ class Delegate(PairingDelegate):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def display_number(self, number, digits):
|
async def display_number(self, number, digits=6):
|
||||||
await self.update_peer_name()
|
await self.update_peer_name()
|
||||||
|
|
||||||
# Wait a bit to allow some of the log lines to print before we prompt
|
# Wait a bit to allow some of the log lines to print before we prompt
|
||||||
@@ -149,17 +153,19 @@ class Delegate(PairingDelegate):
|
|||||||
async def get_peer_name(peer, mode):
|
async def get_peer_name(peer, mode):
|
||||||
if mode == 'classic':
|
if mode == 'classic':
|
||||||
return await peer.request_name()
|
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(
|
# Try to get the peer name from GATT
|
||||||
GATT_DEVICE_NAME_CHARACTERISTIC, services[0]
|
services = await peer.discover_service(GATT_GENERIC_ACCESS_SERVICE)
|
||||||
)
|
if not services:
|
||||||
if values:
|
return None
|
||||||
return values[0].decode('utf-8')
|
|
||||||
|
values = await peer.read_characteristics_by_uuid(
|
||||||
|
GATT_DEVICE_NAME_CHARACTERISTIC, services[0]
|
||||||
|
)
|
||||||
|
if values:
|
||||||
|
return values[0].decode('utf-8')
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -172,12 +178,12 @@ def read_with_error(connection):
|
|||||||
|
|
||||||
if AUTHENTICATION_ERROR_RETURNED[0]:
|
if AUTHENTICATION_ERROR_RETURNED[0]:
|
||||||
return bytes([1])
|
return bytes([1])
|
||||||
else:
|
|
||||||
AUTHENTICATION_ERROR_RETURNED[0] = True
|
AUTHENTICATION_ERROR_RETURNED[0] = True
|
||||||
raise ATT_Error(ATT_INSUFFICIENT_AUTHENTICATION_ERROR)
|
raise ATT_Error(ATT_INSUFFICIENT_AUTHENTICATION_ERROR)
|
||||||
|
|
||||||
|
|
||||||
def write_with_error(connection, value):
|
def write_with_error(connection, _value):
|
||||||
if not connection.is_encrypted:
|
if not connection.is_encrypted:
|
||||||
raise ATT_Error(ATT_INSUFFICIENT_ENCRYPTION_ERROR)
|
raise ATT_Error(ATT_INSUFFICIENT_ENCRYPTION_ERROR)
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -92,7 +92,8 @@ class AdvertisementPrinter:
|
|||||||
|
|
||||||
print(
|
print(
|
||||||
f'>>> {color(address, address_color)} '
|
f'>>> {color(address, address_color)} '
|
||||||
f'[{color(address_type_string, type_color)}]{address_qualifier}{resolution_qualifier}:{separator}'
|
f'[{color(address_type_string, type_color)}]{address_qualifier}'
|
||||||
|
f'{resolution_qualifier}:{separator}'
|
||||||
f'{phy_info}'
|
f'{phy_info}'
|
||||||
f'RSSI:{advertisement.rssi:4} {rssi_bar}{separator}'
|
f'RSSI:{advertisement.rssi:4} {rssi_bar}{separator}'
|
||||||
f'{advertisement.data.to_string(separator)}\n'
|
f'{advertisement.data.to_string(separator)}\n'
|
||||||
|
|||||||
+11
-13
@@ -27,7 +27,8 @@ from bumble.helpers import PacketTracer
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class SnoopPacketReader:
|
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_H1 = 1001
|
||||||
@@ -47,10 +48,7 @@ class SnoopPacketReader:
|
|||||||
(self.version_number, self.data_link_type) = struct.unpack(
|
(self.version_number, self.data_link_type) = struct.unpack(
|
||||||
'>II', source.read(8)
|
'>II', source.read(8)
|
||||||
)
|
)
|
||||||
if (
|
if self.data_link_type not in (self.DATALINK_H4, self.DATALINK_H1):
|
||||||
self.data_link_type != self.DATALINK_H4
|
|
||||||
and self.data_link_type != self.DATALINK_H1
|
|
||||||
):
|
|
||||||
raise ValueError(f'datalink type {self.data_link_type} not supported')
|
raise ValueError(f'datalink type {self.data_link_type} not supported')
|
||||||
|
|
||||||
def next_packet(self):
|
def next_packet(self):
|
||||||
@@ -62,9 +60,9 @@ class SnoopPacketReader:
|
|||||||
original_length,
|
original_length,
|
||||||
included_length,
|
included_length,
|
||||||
packet_flags,
|
packet_flags,
|
||||||
cumulative_drops,
|
_cumulative_drops,
|
||||||
timestamp_seconds,
|
_timestamp_seconds,
|
||||||
timestamp_microsecond,
|
_timestamp_microsecond,
|
||||||
) = struct.unpack('>IIIIII', header)
|
) = struct.unpack('>IIIIII', header)
|
||||||
|
|
||||||
# Abort on truncated packets
|
# Abort on truncated packets
|
||||||
@@ -90,8 +88,8 @@ class SnoopPacketReader:
|
|||||||
packet_flags & 1,
|
packet_flags & 1,
|
||||||
bytes([packet_type]) + self.source.read(included_length),
|
bytes([packet_type]) + self.source.read(included_length),
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
return (packet_flags & 1, self.source.read(included_length))
|
return (packet_flags & 1, self.source.read(included_length))
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -105,13 +103,14 @@ class SnoopPacketReader:
|
|||||||
help='Format of the input file',
|
help='Format of the input file',
|
||||||
)
|
)
|
||||||
@click.argument('filename')
|
@click.argument('filename')
|
||||||
|
# pylint: disable=redefined-builtin
|
||||||
def main(format, filename):
|
def main(format, filename):
|
||||||
input = open(filename, 'rb')
|
input = open(filename, 'rb')
|
||||||
if format == 'h4':
|
if format == 'h4':
|
||||||
packet_reader = PacketReader(input)
|
packet_reader = PacketReader(input)
|
||||||
|
|
||||||
def read_next_packet():
|
def read_next_packet():
|
||||||
(0, packet_reader.next_packet())
|
return (0, packet_reader.next_packet())
|
||||||
|
|
||||||
else:
|
else:
|
||||||
packet_reader = SnoopPacketReader(input)
|
packet_reader = SnoopPacketReader(input)
|
||||||
@@ -128,9 +127,8 @@ def main(format, filename):
|
|||||||
|
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
print(color(f'!!! {error}', 'red'))
|
print(color(f'!!! {error}', 'red'))
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main() # pylint: disable=no-value-for-parameter
|
||||||
|
|||||||
+8
-7
@@ -28,12 +28,12 @@
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
import sys
|
|
||||||
import click
|
import click
|
||||||
import usb1
|
import usb1
|
||||||
from bumble.transport.usb import load_libusb
|
|
||||||
from colors import color
|
from colors import color
|
||||||
|
|
||||||
|
from bumble.transport.usb import load_libusb
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Constants
|
# Constants
|
||||||
@@ -95,9 +95,9 @@ def show_device_details(device):
|
|||||||
print(f' Configuration {configuration.getConfigurationValue()}')
|
print(f' Configuration {configuration.getConfigurationValue()}')
|
||||||
for interface in configuration:
|
for interface in configuration:
|
||||||
for setting in interface:
|
for setting in interface:
|
||||||
alternateSetting = setting.getAlternateSetting()
|
alternate_setting = setting.getAlternateSetting()
|
||||||
suffix = (
|
suffix = (
|
||||||
f'/{alternateSetting}' if interface.getNumSettings() > 1 else ''
|
f'/{alternate_setting}' if interface.getNumSettings() > 1 else ''
|
||||||
)
|
)
|
||||||
(class_string, subclass_string) = get_class_info(
|
(class_string, subclass_string) = get_class_info(
|
||||||
setting.getClass(), setting.getSubClass(), setting.getProtocol()
|
setting.getClass(), setting.getSubClass(), setting.getProtocol()
|
||||||
@@ -112,7 +112,8 @@ def show_device_details(device):
|
|||||||
else 'IN'
|
else 'IN'
|
||||||
)
|
)
|
||||||
print(
|
print(
|
||||||
f' Endpoint 0x{endpoint.getAddress():02X}: {endpoint_type} {endpoint_direction}'
|
f' Endpoint 0x{endpoint.getAddress():02X}: '
|
||||||
|
f'{endpoint_type} {endpoint_direction}'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -123,7 +124,7 @@ def get_class_info(cls, subclass, protocol):
|
|||||||
if class_info is None:
|
if class_info is None:
|
||||||
class_string = f'0x{cls:02X}'
|
class_string = f'0x{cls:02X}'
|
||||||
else:
|
else:
|
||||||
if type(class_info) is tuple:
|
if isinstance(class_info, tuple):
|
||||||
class_string = class_info[0]
|
class_string = class_info[0]
|
||||||
subclass_info = class_info[1].get(subclass)
|
subclass_info = class_info[1].get(subclass)
|
||||||
if subclass_info:
|
if subclass_info:
|
||||||
@@ -274,4 +275,4 @@ def main(verbose):
|
|||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main() # pylint: disable=no-value-for-parameter
|
||||||
|
|||||||
+17
-6
@@ -16,10 +16,9 @@
|
|||||||
# Imports
|
# Imports
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
import struct
|
import struct
|
||||||
import bitstruct
|
|
||||||
import logging
|
import logging
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from colors import color
|
import bitstruct
|
||||||
|
|
||||||
from .company_ids import COMPANY_IDENTIFIERS
|
from .company_ids import COMPANY_IDENTIFIERS
|
||||||
from .sdp import (
|
from .sdp import (
|
||||||
@@ -134,14 +133,15 @@ MPEG_2_4_OBJECT_TYPE_NAMES = {
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
def flags_to_list(flags, values):
|
def flags_to_list(flags, values):
|
||||||
result = []
|
result = []
|
||||||
for i in range(len(values)):
|
for i, value in enumerate(values):
|
||||||
if flags & (1 << (len(values) - i - 1)):
|
if flags & (1 << (len(values) - i - 1)):
|
||||||
result.append(values[i])
|
result.append(value)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
def make_audio_source_service_sdp_records(service_record_handle, version=(1, 3)):
|
def make_audio_source_service_sdp_records(service_record_handle, version=(1, 3)):
|
||||||
|
# pylint: disable=import-outside-toplevel
|
||||||
from .avdtp import AVDTP_PSM
|
from .avdtp import AVDTP_PSM
|
||||||
|
|
||||||
version_int = version[0] << 8 | version[1]
|
version_int = version[0] << 8 | version[1]
|
||||||
@@ -191,6 +191,7 @@ def make_audio_source_service_sdp_records(service_record_handle, version=(1, 3))
|
|||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
def make_audio_sink_service_sdp_records(service_record_handle, version=(1, 3)):
|
def make_audio_sink_service_sdp_records(service_record_handle, version=(1, 3)):
|
||||||
|
# pylint: disable=import-outside-toplevel
|
||||||
from .avdtp import AVDTP_PSM
|
from .avdtp import AVDTP_PSM
|
||||||
|
|
||||||
version_int = version[0] << 8 | version[1]
|
version_int = version[0] << 8 | version[1]
|
||||||
@@ -331,6 +332,7 @@ class SbcMediaCodecInformation(
|
|||||||
channel_modes = ['MONO', 'DUAL_CHANNEL', 'STEREO', 'JOINT_STEREO']
|
channel_modes = ['MONO', 'DUAL_CHANNEL', 'STEREO', 'JOINT_STEREO']
|
||||||
allocation_methods = ['SNR', 'Loudness']
|
allocation_methods = ['SNR', 'Loudness']
|
||||||
return '\n'.join(
|
return '\n'.join(
|
||||||
|
# pylint: disable=line-too-long
|
||||||
[
|
[
|
||||||
'SbcMediaCodecInformation(',
|
'SbcMediaCodecInformation(',
|
||||||
f' sampling_frequency: {",".join([str(x) for x in flags_to_list(self.sampling_frequency, SBC_SAMPLING_FREQUENCIES)])}',
|
f' sampling_frequency: {",".join([str(x) for x in flags_to_list(self.sampling_frequency, SBC_SAMPLING_FREQUENCIES)])}',
|
||||||
@@ -423,6 +425,7 @@ class AacMediaCodecInformation(
|
|||||||
'[7]',
|
'[7]',
|
||||||
]
|
]
|
||||||
channels = [1, 2]
|
channels = [1, 2]
|
||||||
|
# pylint: disable=line-too-long
|
||||||
return '\n'.join(
|
return '\n'.join(
|
||||||
[
|
[
|
||||||
'AacMediaCodecInformation(',
|
'AacMediaCodecInformation(',
|
||||||
@@ -455,6 +458,7 @@ class VendorSpecificMediaCodecInformation:
|
|||||||
return struct.pack('<IH', self.vendor_id, self.codec_id, self.value)
|
return struct.pack('<IH', self.vendor_id, self.codec_id, self.value)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
# pylint: disable=line-too-long
|
||||||
return '\n'.join(
|
return '\n'.join(
|
||||||
[
|
[
|
||||||
'VendorSpecificMediaCodecInformation(',
|
'VendorSpecificMediaCodecInformation(',
|
||||||
@@ -489,7 +493,13 @@ class SbcFrame:
|
|||||||
return self.sample_count / self.sampling_frequency
|
return self.sample_count / self.sampling_frequency
|
||||||
|
|
||||||
def __str__(self):
|
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)})'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -551,6 +561,7 @@ class SbcPacketSource:
|
|||||||
@property
|
@property
|
||||||
def packets(self):
|
def packets(self):
|
||||||
async def generate_packets():
|
async def generate_packets():
|
||||||
|
# pylint: disable=import-outside-toplevel
|
||||||
from .avdtp import MediaPacket # Import here to avoid a circular reference
|
from .avdtp import MediaPacket # Import here to avoid a circular reference
|
||||||
|
|
||||||
sequence_number = 0
|
sequence_number = 0
|
||||||
@@ -582,7 +593,7 @@ class SbcPacketSource:
|
|||||||
|
|
||||||
# Prepare for next packets
|
# Prepare for next packets
|
||||||
sequence_number += 1
|
sequence_number += 1
|
||||||
timestamp += sum([frame.sample_count for frame in frames])
|
timestamp += sum((frame.sample_count for frame in frames))
|
||||||
frames = [frame]
|
frames = [frame]
|
||||||
frames_size = len(frame.payload)
|
frames_size = len(frame.payload)
|
||||||
else:
|
else:
|
||||||
|
|||||||
+28
-23
@@ -22,16 +22,19 @@
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Imports
|
# Imports
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
import struct
|
||||||
from colors import color
|
from colors import color
|
||||||
from pyee import EventEmitter
|
from pyee import EventEmitter
|
||||||
|
|
||||||
from .core import *
|
from bumble.core import UUID, name_or_number
|
||||||
from .hci import *
|
from bumble.hci import HCI_Object, key_with_value
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Constants
|
# Constants
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# fmt: off
|
# fmt: off
|
||||||
|
# pylint: disable=line-too-long
|
||||||
|
|
||||||
ATT_CID = 0x04
|
ATT_CID = 0x04
|
||||||
|
|
||||||
@@ -165,21 +168,14 @@ ATT_ERROR_NAMES = {
|
|||||||
ATT_DEFAULT_MTU = 23
|
ATT_DEFAULT_MTU = 23
|
||||||
|
|
||||||
HANDLE_FIELD_SPEC = {'size': 2, 'mapper': lambda x: f'0x{x:04X}'}
|
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
|
UUID_2_FIELD_SPEC = lambda x, y: UUID.parse_uuid_2(x, y) # noqa: E731
|
||||||
|
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
# pylint: enable=line-too-long
|
||||||
|
# pylint: disable=invalid-name
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Utils
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
def key_with_value(dictionary, target_value):
|
|
||||||
for key, value in dictionary.items():
|
|
||||||
if value == target_value:
|
|
||||||
return key
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Exceptions
|
# Exceptions
|
||||||
@@ -203,6 +199,7 @@ class ATT_PDU:
|
|||||||
|
|
||||||
pdu_classes = {}
|
pdu_classes = {}
|
||||||
op_code = 0
|
op_code = 0
|
||||||
|
name = None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_bytes(pdu):
|
def from_bytes(pdu):
|
||||||
@@ -731,15 +728,15 @@ class Attribute(EventEmitter):
|
|||||||
self.permissions = permissions
|
self.permissions = permissions
|
||||||
|
|
||||||
# Convert the type to a UUID object if it isn't already
|
# 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)
|
self.type = UUID(attribute_type)
|
||||||
elif type(attribute_type) is bytes:
|
elif isinstance(attribute_type, bytes):
|
||||||
self.type = UUID.from_bytes(attribute_type)
|
self.type = UUID.from_bytes(attribute_type)
|
||||||
else:
|
else:
|
||||||
self.type = attribute_type
|
self.type = attribute_type
|
||||||
|
|
||||||
# Convert the value to a byte array
|
# Convert the value to a byte array
|
||||||
if type(value) is str:
|
if isinstance(value, str):
|
||||||
self.value = bytes(value, 'utf-8')
|
self.value = bytes(value, 'utf-8')
|
||||||
else:
|
else:
|
||||||
self.value = value
|
self.value = value
|
||||||
@@ -753,9 +750,11 @@ class Attribute(EventEmitter):
|
|||||||
def read_value(self, connection):
|
def read_value(self, connection):
|
||||||
if read := getattr(self.value, 'read', None):
|
if read := getattr(self.value, 'read', None):
|
||||||
try:
|
try:
|
||||||
value = read(connection)
|
value = read(connection) # pylint: disable=not-callable
|
||||||
except ATT_Error as error:
|
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:
|
else:
|
||||||
value = self.value
|
value = self.value
|
||||||
|
|
||||||
@@ -766,16 +765,18 @@ class Attribute(EventEmitter):
|
|||||||
|
|
||||||
if write := getattr(self.value, 'write', None):
|
if write := getattr(self.value, 'write', None):
|
||||||
try:
|
try:
|
||||||
write(connection, value)
|
write(connection, value) # pylint: disable=not-callable
|
||||||
except ATT_Error as error:
|
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:
|
else:
|
||||||
self.value = value
|
self.value = value
|
||||||
|
|
||||||
self.emit('write', connection, value)
|
self.emit('write', connection, value)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
if type(self.value) is bytes:
|
if isinstance(self.value, bytes):
|
||||||
value_str = self.value.hex()
|
value_str = self.value.hex()
|
||||||
else:
|
else:
|
||||||
value_str = str(self.value)
|
value_str = str(self.value)
|
||||||
@@ -783,4 +784,8 @@ class Attribute(EventEmitter):
|
|||||||
value_string = f', value={self.value.hex()}'
|
value_string = f', value={self.value.hex()}'
|
||||||
else:
|
else:
|
||||||
value_string = ''
|
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})'
|
||||||
|
)
|
||||||
|
|||||||
+144
-79
@@ -49,6 +49,7 @@ logger = logging.getLogger(__name__)
|
|||||||
# Constants
|
# Constants
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# fmt: off
|
# fmt: off
|
||||||
|
# pylint: disable=line-too-long
|
||||||
|
|
||||||
AVDTP_PSM = 0x0019
|
AVDTP_PSM = 0x0019
|
||||||
|
|
||||||
@@ -198,6 +199,8 @@ AVDTP_STATE_NAMES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
# pylint: enable=line-too-long
|
||||||
|
# pylint: disable=invalid-name
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -318,7 +321,18 @@ class MediaPacket:
|
|||||||
return header + self.payload
|
return header + self.payload
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'RTP(v={self.version},p={self.padding},x={self.extension},m={self.marker},pt={self.payload_type},sn={self.sequence_number},ts={self.timestamp},ssrc={self.ssrc},csrcs={self.csrc_list},payload_size={len(self.payload)})'
|
return (
|
||||||
|
f'RTP(v={self.version},'
|
||||||
|
f'p={self.padding},'
|
||||||
|
f'x={self.extension},'
|
||||||
|
f'm={self.marker},'
|
||||||
|
f'pt={self.payload_type},'
|
||||||
|
f'sn={self.sequence_number},'
|
||||||
|
f'ts={self.timestamp},'
|
||||||
|
f'ssrc={self.ssrc},'
|
||||||
|
f'csrcs={self.csrc_list},'
|
||||||
|
f'payload_size={len(self.payload)})'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -369,7 +383,7 @@ class MediaPacketPump:
|
|||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class MessageAssembler:
|
class MessageAssembler: # pylint: disable=attribute-defined-outside-init
|
||||||
def __init__(self, callback):
|
def __init__(self, callback):
|
||||||
self.callback = callback
|
self.callback = callback
|
||||||
self.reset()
|
self.reset()
|
||||||
@@ -390,16 +404,16 @@ class MessageAssembler:
|
|||||||
message_type = pdu[0] & 3
|
message_type = pdu[0] & 3
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'transaction_label={transaction_label}, packet_type={Protocol.packet_type_name(packet_type)}, message_type={Message.message_type_name(message_type)}'
|
f'transaction_label={transaction_label}, '
|
||||||
|
f'packet_type={Protocol.packet_type_name(packet_type)}, '
|
||||||
|
f'message_type={Message.message_type_name(message_type)}'
|
||||||
)
|
)
|
||||||
if (
|
if packet_type in (Protocol.SINGLE_PACKET, Protocol.START_PACKET):
|
||||||
packet_type == Protocol.SINGLE_PACKET
|
|
||||||
or packet_type == Protocol.START_PACKET
|
|
||||||
):
|
|
||||||
if self.message is not None:
|
if self.message is not None:
|
||||||
# The previous message has not been terminated
|
# The previous message has not been terminated
|
||||||
logger.warning(
|
logger.warning(
|
||||||
'received a start or single packet when expecting an end or continuation'
|
'received a start or single packet when expecting an end or '
|
||||||
|
'continuation'
|
||||||
)
|
)
|
||||||
self.reset()
|
self.reset()
|
||||||
|
|
||||||
@@ -413,23 +427,22 @@ class MessageAssembler:
|
|||||||
else:
|
else:
|
||||||
self.number_of_signal_packets = pdu[2]
|
self.number_of_signal_packets = pdu[2]
|
||||||
self.message = pdu[3:]
|
self.message = pdu[3:]
|
||||||
elif (
|
elif packet_type in (Protocol.CONTINUE_PACKET, Protocol.END_PACKET):
|
||||||
packet_type == Protocol.CONTINUE_PACKET
|
|
||||||
or packet_type == Protocol.END_PACKET
|
|
||||||
):
|
|
||||||
if self.packet_count == 0:
|
if self.packet_count == 0:
|
||||||
logger.warning('unexpected continuation')
|
logger.warning('unexpected continuation')
|
||||||
return
|
return
|
||||||
|
|
||||||
if transaction_label != self.transaction_label:
|
if transaction_label != self.transaction_label:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f'transaction label mismatch: expected {self.transaction_label}, received {transaction_label}'
|
f'transaction label mismatch: expected {self.transaction_label}, '
|
||||||
|
f'received {transaction_label}'
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if message_type != self.message_type:
|
if message_type != self.message_type:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f'message type mismatch: expected {self.message_type}, received {message_type}'
|
f'message type mismatch: expected {self.message_type}, '
|
||||||
|
f'received {message_type}'
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -438,7 +451,9 @@ class MessageAssembler:
|
|||||||
if packet_type == Protocol.END_PACKET:
|
if packet_type == Protocol.END_PACKET:
|
||||||
if self.packet_count != self.number_of_signal_packets:
|
if self.packet_count != self.number_of_signal_packets:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f'incomplete fragmented message: expected {self.number_of_signal_packets} packets, received {self.packet_count}'
|
'incomplete fragmented message: '
|
||||||
|
f'expected {self.number_of_signal_packets} packets, '
|
||||||
|
f'received {self.packet_count}'
|
||||||
)
|
)
|
||||||
self.reset()
|
self.reset()
|
||||||
return
|
return
|
||||||
@@ -447,7 +462,9 @@ class MessageAssembler:
|
|||||||
else:
|
else:
|
||||||
if self.packet_count > self.number_of_signal_packets:
|
if self.packet_count > self.number_of_signal_packets:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f'too many packets: expected {self.number_of_signal_packets}, received {self.packet_count}'
|
'too many packets: '
|
||||||
|
f'expected {self.number_of_signal_packets}, '
|
||||||
|
f'received {self.packet_count}'
|
||||||
)
|
)
|
||||||
self.reset()
|
self.reset()
|
||||||
return
|
return
|
||||||
@@ -515,7 +532,7 @@ class ServiceCapabilities:
|
|||||||
self.service_category = service_category
|
self.service_category = service_category
|
||||||
self.service_capabilities_bytes = service_capabilities_bytes
|
self.service_capabilities_bytes = service_capabilities_bytes
|
||||||
|
|
||||||
def to_string(self, details=[]):
|
def to_string(self, details=[]): # pylint: disable=dangerous-default-value
|
||||||
attributes = ','.join(
|
attributes = ','.join(
|
||||||
[name_or_number(AVDTP_SERVICE_CATEGORY_NAMES, self.service_category)]
|
[name_or_number(AVDTP_SERVICE_CATEGORY_NAMES, self.service_category)]
|
||||||
+ details
|
+ details
|
||||||
@@ -562,10 +579,16 @@ class MediaCodecCapabilities(ServiceCapabilities):
|
|||||||
self.media_codec_information = media_codec_information
|
self.media_codec_information = media_codec_information
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
codec_info = (
|
||||||
|
self.media_codec_information.hex()
|
||||||
|
if isinstance(self.media_codec_information, bytes)
|
||||||
|
else str(self.media_codec_information)
|
||||||
|
)
|
||||||
|
|
||||||
details = [
|
details = [
|
||||||
f'media_type={name_or_number(AVDTP_MEDIA_TYPE_NAMES, self.media_type)}',
|
f'media_type={name_or_number(AVDTP_MEDIA_TYPE_NAMES, self.media_type)}',
|
||||||
f'codec={name_or_number(A2DP_CODEC_TYPE_NAMES, self.media_codec_type)}',
|
f'codec={name_or_number(A2DP_CODEC_TYPE_NAMES, self.media_codec_type)}',
|
||||||
f'codec_info={self.media_codec_information.hex() if type(self.media_codec_information) is bytes else str(self.media_codec_information)}',
|
f'codec_info={codec_info}',
|
||||||
]
|
]
|
||||||
return self.to_string(details)
|
return self.to_string(details)
|
||||||
|
|
||||||
@@ -591,7 +614,7 @@ class EndPointInfo:
|
|||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class Message:
|
class Message: # pylint:disable=attribute-defined-outside-init
|
||||||
COMMAND = 0
|
COMMAND = 0
|
||||||
GENERAL_REJECT = 1
|
GENERAL_REJECT = 1
|
||||||
RESPONSE_ACCEPT = 2
|
RESPONSE_ACCEPT = 2
|
||||||
@@ -611,11 +634,11 @@ class Message:
|
|||||||
return name_or_number(Message.MESSAGE_TYPE_NAMES, message_type)
|
return name_or_number(Message.MESSAGE_TYPE_NAMES, message_type)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def subclass(cls):
|
def subclass(subclass):
|
||||||
# Infer the signal identifier and message subtype from the class name
|
# Infer the signal identifier and message subtype from the class name
|
||||||
name = cls.__name__
|
name = subclass.__name__
|
||||||
if name == 'General_Reject':
|
if name == 'General_Reject':
|
||||||
cls.signal_identifier = 0
|
subclass.signal_identifier = 0
|
||||||
signal_identifier_str = None
|
signal_identifier_str = None
|
||||||
message_type = Message.COMMAND
|
message_type = Message.COMMAND
|
||||||
elif name.endswith('_Command'):
|
elif name.endswith('_Command'):
|
||||||
@@ -630,22 +653,23 @@ class Message:
|
|||||||
else:
|
else:
|
||||||
raise ValueError('invalid class name')
|
raise ValueError('invalid class name')
|
||||||
|
|
||||||
cls.message_type = message_type
|
subclass.message_type = message_type
|
||||||
|
|
||||||
if signal_identifier_str is not None:
|
if signal_identifier_str is not None:
|
||||||
for (name, signal_identifier) in AVDTP_SIGNAL_IDENTIFIERS.items():
|
for (name, signal_identifier) in AVDTP_SIGNAL_IDENTIFIERS.items():
|
||||||
if name.lower().endswith(signal_identifier_str.lower()):
|
if name.lower().endswith(signal_identifier_str.lower()):
|
||||||
cls.signal_identifier = signal_identifier
|
subclass.signal_identifier = signal_identifier
|
||||||
break
|
break
|
||||||
|
|
||||||
# Register the subclass
|
# Register the subclass
|
||||||
Message.subclasses.setdefault(cls.signal_identifier, {})[
|
Message.subclasses.setdefault(subclass.signal_identifier, {})[
|
||||||
cls.message_type
|
subclass.message_type
|
||||||
] = cls
|
] = subclass
|
||||||
|
|
||||||
return cls
|
return subclass
|
||||||
|
|
||||||
# Factory method to create a subclass based on the signal identifier and message type
|
# Factory method to create a subclass based on the signal identifier and message
|
||||||
|
# type
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create(signal_identifier, message_type, payload):
|
def create(signal_identifier, message_type, payload):
|
||||||
# Look for a registered subclass
|
# Look for a registered subclass
|
||||||
@@ -676,18 +700,23 @@ class Message:
|
|||||||
self.payload = payload
|
self.payload = payload
|
||||||
|
|
||||||
def to_string(self, details):
|
def to_string(self, details):
|
||||||
base = f'{color(f"{name_or_number(AVDTP_SIGNAL_NAMES, self.signal_identifier)}_{Message.message_type_name(self.message_type)}", "yellow")}'
|
base = color(
|
||||||
|
f'{name_or_number(AVDTP_SIGNAL_NAMES, self.signal_identifier)}_'
|
||||||
|
f'{Message.message_type_name(self.message_type)}',
|
||||||
|
'yellow',
|
||||||
|
)
|
||||||
|
|
||||||
if details:
|
if details:
|
||||||
if type(details) is str:
|
if isinstance(details, str):
|
||||||
return f'{base}: {details}'
|
return f'{base}: {details}'
|
||||||
else:
|
|
||||||
return (
|
return (
|
||||||
base
|
base
|
||||||
+ ':\n'
|
+ ':\n'
|
||||||
+ '\n'.join([' ' + color(detail, 'cyan') for detail in details])
|
+ '\n'.join([' ' + color(detail, 'cyan') for detail in details])
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
return base
|
return base
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.to_string(self.payload.hex())
|
return self.to_string(self.payload.hex())
|
||||||
@@ -703,8 +732,8 @@ class Simple_Command(Message):
|
|||||||
self.acp_seid = self.payload[0] >> 2
|
self.acp_seid = self.payload[0] >> 2
|
||||||
|
|
||||||
def __init__(self, seid):
|
def __init__(self, seid):
|
||||||
|
super().__init__(payload=bytes([seid << 2]))
|
||||||
self.acp_seid = seid
|
self.acp_seid = seid
|
||||||
self.payload = bytes([seid << 2])
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.to_string([f'ACP SEID: {self.acp_seid}'])
|
return self.to_string([f'ACP SEID: {self.acp_seid}'])
|
||||||
@@ -720,8 +749,8 @@ class Simple_Reject(Message):
|
|||||||
self.error_code = self.payload[0]
|
self.error_code = self.payload[0]
|
||||||
|
|
||||||
def __init__(self, error_code):
|
def __init__(self, error_code):
|
||||||
|
super().__init__(payload=bytes([error_code]))
|
||||||
self.error_code = error_code
|
self.error_code = error_code
|
||||||
self.payload = bytes([self.error_code])
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
details = [f'error_code: {name_or_number(AVDTP_ERROR_NAMES, self.error_code)}']
|
details = [f'error_code: {name_or_number(AVDTP_ERROR_NAMES, self.error_code)}']
|
||||||
@@ -752,13 +781,14 @@ class Discover_Response(Message):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, endpoints):
|
def __init__(self, endpoints):
|
||||||
|
super().__init__(payload=b''.join([bytes(endpoint) for endpoint in endpoints]))
|
||||||
self.endpoints = endpoints
|
self.endpoints = endpoints
|
||||||
self.payload = b''.join([bytes(endpoint) for endpoint in endpoints])
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
details = []
|
details = []
|
||||||
for endpoint in self.endpoints:
|
for endpoint in self.endpoints:
|
||||||
details.extend(
|
details.extend(
|
||||||
|
# pylint: disable=line-too-long
|
||||||
[
|
[
|
||||||
f'ACP SEID: {endpoint.seid}',
|
f'ACP SEID: {endpoint.seid}',
|
||||||
f' in_use: {endpoint.in_use}',
|
f' in_use: {endpoint.in_use}',
|
||||||
@@ -788,8 +818,10 @@ class Get_Capabilities_Response(Message):
|
|||||||
self.capabilities = ServiceCapabilities.parse_capabilities(self.payload)
|
self.capabilities = ServiceCapabilities.parse_capabilities(self.payload)
|
||||||
|
|
||||||
def __init__(self, capabilities):
|
def __init__(self, capabilities):
|
||||||
|
super().__init__(
|
||||||
|
payload=ServiceCapabilities.serialize_capabilities(capabilities)
|
||||||
|
)
|
||||||
self.capabilities = capabilities
|
self.capabilities = capabilities
|
||||||
self.payload = ServiceCapabilities.serialize_capabilities(capabilities)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
details = [str(capability) for capability in self.capabilities]
|
details = [str(capability) for capability in self.capabilities]
|
||||||
@@ -841,12 +873,13 @@ class Set_Configuration_Command(Message):
|
|||||||
self.capabilities = ServiceCapabilities.parse_capabilities(self.payload[2:])
|
self.capabilities = ServiceCapabilities.parse_capabilities(self.payload[2:])
|
||||||
|
|
||||||
def __init__(self, acp_seid, int_seid, capabilities):
|
def __init__(self, acp_seid, int_seid, capabilities):
|
||||||
|
super().__init__(
|
||||||
|
payload=bytes([acp_seid << 2, int_seid << 2])
|
||||||
|
+ ServiceCapabilities.serialize_capabilities(capabilities)
|
||||||
|
)
|
||||||
self.acp_seid = acp_seid
|
self.acp_seid = acp_seid
|
||||||
self.int_seid = int_seid
|
self.int_seid = int_seid
|
||||||
self.capabilities = capabilities
|
self.capabilities = capabilities
|
||||||
self.payload = bytes(
|
|
||||||
[acp_seid << 2, int_seid << 2]
|
|
||||||
) + ServiceCapabilities.serialize_capabilities(capabilities)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
details = [f'ACP SEID: {self.acp_seid}', f'INT SEID: {self.int_seid}'] + [
|
details = [f'ACP SEID: {self.acp_seid}', f'INT SEID: {self.int_seid}'] + [
|
||||||
@@ -875,14 +908,20 @@ class Set_Configuration_Reject(Message):
|
|||||||
self.error_code = self.payload[1]
|
self.error_code = self.payload[1]
|
||||||
|
|
||||||
def __init__(self, service_category, error_code):
|
def __init__(self, service_category, error_code):
|
||||||
|
super().__init__(payload=bytes([service_category, error_code]))
|
||||||
self.service_category = service_category
|
self.service_category = service_category
|
||||||
self.error_code = error_code
|
self.error_code = error_code
|
||||||
self.payload = bytes([service_category, self.error_code])
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
details = [
|
details = [
|
||||||
f'service_category: {name_or_number(AVDTP_SERVICE_CATEGORY_NAMES, self.service_category)}',
|
(
|
||||||
f'error_code: {name_or_number(AVDTP_ERROR_NAMES, self.error_code)}',
|
'service_category: '
|
||||||
|
f'{name_or_number(AVDTP_SERVICE_CATEGORY_NAMES, self.service_category)}'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'error_code: '
|
||||||
|
f'{name_or_number(AVDTP_ERROR_NAMES, self.error_code)}'
|
||||||
|
),
|
||||||
]
|
]
|
||||||
return self.to_string(details)
|
return self.to_string(details)
|
||||||
|
|
||||||
@@ -906,8 +945,10 @@ class Get_Configuration_Response(Message):
|
|||||||
self.capabilities = ServiceCapabilities.parse_capabilities(self.payload)
|
self.capabilities = ServiceCapabilities.parse_capabilities(self.payload)
|
||||||
|
|
||||||
def __init__(self, capabilities):
|
def __init__(self, capabilities):
|
||||||
|
super().__init__(
|
||||||
|
payload=ServiceCapabilities.serialize_capabilities(capabilities)
|
||||||
|
)
|
||||||
self.capabilities = capabilities
|
self.capabilities = capabilities
|
||||||
self.payload = ServiceCapabilities.serialize_capabilities(capabilities)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
details = [str(capability) for capability in self.capabilities]
|
details = [str(capability) for capability in self.capabilities]
|
||||||
@@ -930,6 +971,7 @@ class Reconfigure_Command(Message):
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
def init_from_payload(self):
|
def init_from_payload(self):
|
||||||
|
# pylint: disable=attribute-defined-outside-init
|
||||||
self.acp_seid = self.payload[0] >> 2
|
self.acp_seid = self.payload[0] >> 2
|
||||||
self.capabilities = ServiceCapabilities.parse_capabilities(self.payload[1:])
|
self.capabilities = ServiceCapabilities.parse_capabilities(self.payload[1:])
|
||||||
|
|
||||||
@@ -991,8 +1033,8 @@ class Start_Command(Message):
|
|||||||
self.acp_seids = [x >> 2 for x in self.payload]
|
self.acp_seids = [x >> 2 for x in self.payload]
|
||||||
|
|
||||||
def __init__(self, seids):
|
def __init__(self, seids):
|
||||||
|
super().__init__(payload=bytes([seid << 2 for seid in seids]))
|
||||||
self.acp_seids = seids
|
self.acp_seids = seids
|
||||||
self.payload = bytes([seid << 2 for seid in self.acp_seids])
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.to_string([f'ACP SEIDs: {self.acp_seids}'])
|
return self.to_string([f'ACP SEIDs: {self.acp_seids}'])
|
||||||
@@ -1018,9 +1060,9 @@ class Start_Reject(Message):
|
|||||||
self.error_code = self.payload[1]
|
self.error_code = self.payload[1]
|
||||||
|
|
||||||
def __init__(self, acp_seid, error_code):
|
def __init__(self, acp_seid, error_code):
|
||||||
|
super().__init__(payload=bytes([acp_seid << 2, error_code]))
|
||||||
self.acp_seid = acp_seid
|
self.acp_seid = acp_seid
|
||||||
self.error_code = error_code
|
self.error_code = error_code
|
||||||
self.payload = bytes([self.acp_seid << 2, self.error_code])
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
details = [
|
details = [
|
||||||
@@ -1126,7 +1168,7 @@ class General_Reject(Message):
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
def to_string(self, details):
|
def to_string(self, details):
|
||||||
return f'{color(f"GENERAL_REJECT", "yellow")}'
|
return color('GENERAL_REJECT', 'yellow')
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -1137,6 +1179,7 @@ class DelayReport_Command(Message):
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
def init_from_payload(self):
|
def init_from_payload(self):
|
||||||
|
# pylint: disable=attribute-defined-outside-init
|
||||||
self.acp_seid = self.payload[0] >> 2
|
self.acp_seid = self.payload[0] >> 2
|
||||||
self.delay = (self.payload[1] << 8) | (self.payload[2])
|
self.delay = (self.payload[1] << 8) | (self.payload[2])
|
||||||
|
|
||||||
@@ -1206,9 +1249,11 @@ class Protocol:
|
|||||||
l2cap_channel.on('open', self.on_l2cap_channel_open)
|
l2cap_channel.on('open', self.on_l2cap_channel_open)
|
||||||
|
|
||||||
def get_local_endpoint_by_seid(self, seid):
|
def get_local_endpoint_by_seid(self, seid):
|
||||||
if seid > 0 and seid <= len(self.local_endpoints):
|
if 0 < seid <= len(self.local_endpoints):
|
||||||
return self.local_endpoints[seid - 1]
|
return self.local_endpoints[seid - 1]
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def add_source(self, codec_capabilities, packet_pump):
|
def add_source(self, codec_capabilities, packet_pump):
|
||||||
seid = len(self.local_endpoints) + 1
|
seid = len(self.local_endpoints) + 1
|
||||||
source = LocalSource(self, seid, codec_capabilities, packet_pump)
|
source = LocalSource(self, seid, codec_capabilities, packet_pump)
|
||||||
@@ -1288,12 +1333,15 @@ class Protocol:
|
|||||||
if has_media_transport and has_codec:
|
if has_media_transport and has_codec:
|
||||||
return endpoint
|
return endpoint
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def on_pdu(self, pdu):
|
def on_pdu(self, pdu):
|
||||||
self.message_assembler.on_pdu(pdu)
|
self.message_assembler.on_pdu(pdu)
|
||||||
|
|
||||||
def on_message(self, transaction_label, message):
|
def on_message(self, transaction_label, message):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'{color("<<< Received AVDTP message", "magenta")}: [{transaction_label}] {message}'
|
f'{color("<<< Received AVDTP message", "magenta")}: '
|
||||||
|
f'[{transaction_label}] {message}'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check that the identifier is not reserved
|
# Check that the identifier is not reserved
|
||||||
@@ -1311,7 +1359,12 @@ class Protocol:
|
|||||||
|
|
||||||
if message.message_type == Message.COMMAND:
|
if message.message_type == Message.COMMAND:
|
||||||
# Command
|
# Command
|
||||||
handler_name = f'on_{AVDTP_SIGNAL_NAMES.get(message.signal_identifier,"").replace("AVDTP_","").lower()}_command'
|
signal_name = (
|
||||||
|
AVDTP_SIGNAL_NAMES.get(message.signal_identifier, "")
|
||||||
|
.replace("AVDTP_", "")
|
||||||
|
.lower()
|
||||||
|
)
|
||||||
|
handler_name = f'on_{signal_name}_command'
|
||||||
handler = getattr(self, handler_name, None)
|
handler = getattr(self, handler_name, None)
|
||||||
if handler:
|
if handler:
|
||||||
try:
|
try:
|
||||||
@@ -1344,7 +1397,8 @@ class Protocol:
|
|||||||
|
|
||||||
def send_message(self, transaction_label, message):
|
def send_message(self, transaction_label, message):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'{color(">>> Sending AVDTP message", "magenta")}: [{transaction_label}] {message}'
|
f'{color(">>> Sending AVDTP message", "magenta")}: '
|
||||||
|
f'[{transaction_label}] {message}'
|
||||||
)
|
)
|
||||||
max_fragment_size = (
|
max_fragment_size = (
|
||||||
self.l2cap_channel.mtu - 3
|
self.l2cap_channel.mtu - 3
|
||||||
@@ -1398,10 +1452,7 @@ class Protocol:
|
|||||||
response = await transaction_result
|
response = await transaction_result
|
||||||
|
|
||||||
# Check for errors
|
# Check for errors
|
||||||
if (
|
if response.message_type in (Message.GENERAL_REJECT, Message.RESPONSE_REJECT):
|
||||||
response.message_type == Message.GENERAL_REJECT
|
|
||||||
or response.message_type == Message.RESPONSE_REJECT
|
|
||||||
):
|
|
||||||
raise ProtocolError(response.error_code, 'avdtp')
|
raise ProtocolError(response.error_code, 'avdtp')
|
||||||
|
|
||||||
return response
|
return response
|
||||||
@@ -1424,8 +1475,8 @@ class Protocol:
|
|||||||
async def get_capabilities(self, seid):
|
async def get_capabilities(self, seid):
|
||||||
if self.version > (1, 2):
|
if self.version > (1, 2):
|
||||||
return await self.send_command(Get_All_Capabilities_Command(seid))
|
return await self.send_command(Get_All_Capabilities_Command(seid))
|
||||||
else:
|
|
||||||
return await self.send_command(Get_Capabilities_Command(seid))
|
return await self.send_command(Get_Capabilities_Command(seid))
|
||||||
|
|
||||||
async def set_configuration(self, acp_seid, int_seid, capabilities):
|
async def set_configuration(self, acp_seid, int_seid, capabilities):
|
||||||
return await self.send_command(
|
return await self.send_command(
|
||||||
@@ -1451,7 +1502,7 @@ class Protocol:
|
|||||||
async def abort(self, seid):
|
async def abort(self, seid):
|
||||||
return await self.send_command(Abort_Command(seid))
|
return await self.send_command(Abort_Command(seid))
|
||||||
|
|
||||||
def on_discover_command(self, command):
|
def on_discover_command(self, _command):
|
||||||
endpoint_infos = [
|
endpoint_infos = [
|
||||||
EndPointInfo(endpoint.seid, 0, endpoint.media_type, endpoint.tsep)
|
EndPointInfo(endpoint.seid, 0, endpoint.media_type, endpoint.tsep)
|
||||||
for endpoint in self.local_endpoints
|
for endpoint in self.local_endpoints
|
||||||
@@ -1689,7 +1740,7 @@ class Stream:
|
|||||||
self.change_state(AVDTP_OPEN_STATE)
|
self.change_state(AVDTP_OPEN_STATE)
|
||||||
|
|
||||||
async def close(self):
|
async def close(self):
|
||||||
if self.state not in {AVDTP_OPEN_STATE, AVDTP_STREAMING_STATE}:
|
if self.state not in (AVDTP_OPEN_STATE, AVDTP_STREAMING_STATE):
|
||||||
raise InvalidStateError('current state is not OPEN or STREAMING')
|
raise InvalidStateError('current state is not OPEN or STREAMING')
|
||||||
|
|
||||||
logger.debug('closing local endpoint')
|
logger.debug('closing local endpoint')
|
||||||
@@ -1718,13 +1769,14 @@ class Stream:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
self.change_state(AVDTP_CONFIGURED_STATE)
|
self.change_state(AVDTP_CONFIGURED_STATE)
|
||||||
|
return None
|
||||||
|
|
||||||
def on_get_configuration_command(self, configuration):
|
def on_get_configuration_command(self, configuration):
|
||||||
if self.state not in {
|
if self.state not in (
|
||||||
AVDTP_CONFIGURED_STATE,
|
AVDTP_CONFIGURED_STATE,
|
||||||
AVDTP_OPEN_STATE,
|
AVDTP_OPEN_STATE,
|
||||||
AVDTP_STREAMING_STATE,
|
AVDTP_STREAMING_STATE,
|
||||||
}:
|
):
|
||||||
return Get_Configuration_Reject(AVDTP_BAD_STATE_ERROR)
|
return Get_Configuration_Reject(AVDTP_BAD_STATE_ERROR)
|
||||||
|
|
||||||
return self.local_endpoint.on_get_configuration_command(configuration)
|
return self.local_endpoint.on_get_configuration_command(configuration)
|
||||||
@@ -1737,6 +1789,8 @@ class Stream:
|
|||||||
if result is not None:
|
if result is not None:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def on_open_command(self):
|
def on_open_command(self):
|
||||||
if self.state != AVDTP_CONFIGURED_STATE:
|
if self.state != AVDTP_CONFIGURED_STATE:
|
||||||
return Open_Reject(AVDTP_BAD_STATE_ERROR)
|
return Open_Reject(AVDTP_BAD_STATE_ERROR)
|
||||||
@@ -1749,6 +1803,7 @@ class Stream:
|
|||||||
self.protocol.channel_acceptor = self
|
self.protocol.channel_acceptor = self
|
||||||
|
|
||||||
self.change_state(AVDTP_OPEN_STATE)
|
self.change_state(AVDTP_OPEN_STATE)
|
||||||
|
return None
|
||||||
|
|
||||||
def on_start_command(self):
|
def on_start_command(self):
|
||||||
if self.state != AVDTP_OPEN_STATE:
|
if self.state != AVDTP_OPEN_STATE:
|
||||||
@@ -1764,6 +1819,7 @@ class Stream:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
self.change_state(AVDTP_STREAMING_STATE)
|
self.change_state(AVDTP_STREAMING_STATE)
|
||||||
|
return None
|
||||||
|
|
||||||
def on_suspend_command(self):
|
def on_suspend_command(self):
|
||||||
if self.state != AVDTP_STREAMING_STATE:
|
if self.state != AVDTP_STREAMING_STATE:
|
||||||
@@ -1774,9 +1830,10 @@ class Stream:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
self.change_state(AVDTP_OPEN_STATE)
|
self.change_state(AVDTP_OPEN_STATE)
|
||||||
|
return None
|
||||||
|
|
||||||
def on_close_command(self):
|
def on_close_command(self):
|
||||||
if self.state not in {AVDTP_OPEN_STATE, AVDTP_STREAMING_STATE}:
|
if self.state not in (AVDTP_OPEN_STATE, AVDTP_STREAMING_STATE):
|
||||||
return Open_Reject(AVDTP_BAD_STATE_ERROR)
|
return Open_Reject(AVDTP_BAD_STATE_ERROR)
|
||||||
|
|
||||||
result = self.local_endpoint.on_close_command()
|
result = self.local_endpoint.on_close_command()
|
||||||
@@ -1792,6 +1849,8 @@ class Stream:
|
|||||||
# TODO: set a timer as we wait for the RTP channel to be closed
|
# TODO: set a timer as we wait for the RTP channel to be closed
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def on_abort_command(self):
|
def on_abort_command(self):
|
||||||
if self.rtp_channel is None:
|
if self.rtp_channel is None:
|
||||||
# No need to wait
|
# No need to wait
|
||||||
@@ -1819,7 +1878,7 @@ class Stream:
|
|||||||
self.local_endpoint.in_use = 0
|
self.local_endpoint.in_use = 0
|
||||||
self.rtp_channel = None
|
self.rtp_channel = None
|
||||||
|
|
||||||
if self.state in {AVDTP_CLOSING_STATE, AVDTP_ABORTING_STATE}:
|
if self.state in (AVDTP_CLOSING_STATE, AVDTP_ABORTING_STATE):
|
||||||
self.change_state(AVDTP_IDLE_STATE)
|
self.change_state(AVDTP_IDLE_STATE)
|
||||||
else:
|
else:
|
||||||
logger.warning('unexpected channel close while not CLOSING or ABORTING')
|
logger.warning('unexpected channel close while not CLOSING or ABORTING')
|
||||||
@@ -1839,7 +1898,10 @@ class Stream:
|
|||||||
local_endpoint.in_use = 1
|
local_endpoint.in_use = 1
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'Stream({self.local_endpoint.seid} -> {self.remote_endpoint.seid} {self.state_name(self.state)})'
|
return (
|
||||||
|
f'Stream({self.local_endpoint.seid} -> '
|
||||||
|
f'{self.remote_endpoint.seid} {self.state_name(self.state)})'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -1852,12 +1914,14 @@ class StreamEndPoint:
|
|||||||
self.capabilities = capabilities
|
self.capabilities = capabilities
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
media_type = f'{name_or_number(AVDTP_MEDIA_TYPE_NAMES, self.media_type)}'
|
||||||
|
tsep = f'{name_or_number(AVDTP_TSEP_NAMES, self.tsep)}'
|
||||||
return '\n'.join(
|
return '\n'.join(
|
||||||
[
|
[
|
||||||
'SEP(',
|
'SEP(',
|
||||||
f' seid={self.seid}',
|
f' seid={self.seid}',
|
||||||
f' media_type={name_or_number(AVDTP_MEDIA_TYPE_NAMES, self.media_type)}',
|
f' media_type={media_type}',
|
||||||
f' tsep={name_or_number(AVDTP_TSEP_NAMES, self.tsep)}',
|
f' tsep={tsep}',
|
||||||
f' in_use={self.in_use}',
|
f' in_use={self.in_use}',
|
||||||
' capabilities=[',
|
' capabilities=[',
|
||||||
'\n'.join([f' {x}' for x in self.capabilities]),
|
'\n'.join([f' {x}' for x in self.capabilities]),
|
||||||
@@ -1902,11 +1966,11 @@ class DiscoveredStreamEndPoint(StreamEndPoint, StreamEndPointProxy):
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class LocalStreamEndPoint(StreamEndPoint):
|
class LocalStreamEndPoint(StreamEndPoint):
|
||||||
def __init__(
|
def __init__(
|
||||||
self, protocol, seid, media_type, tsep, capabilities, configuration=[]
|
self, protocol, seid, media_type, tsep, capabilities, configuration=None
|
||||||
):
|
):
|
||||||
super().__init__(seid, media_type, tsep, 0, capabilities)
|
super().__init__(seid, media_type, tsep, 0, capabilities)
|
||||||
self.protocol = protocol
|
self.protocol = protocol
|
||||||
self.configuration = configuration
|
self.configuration = configuration if configuration is not None else []
|
||||||
self.stream = None
|
self.stream = None
|
||||||
|
|
||||||
async def start(self):
|
async def start(self):
|
||||||
@@ -1968,14 +2032,14 @@ class LocalSource(LocalStreamEndPoint, EventEmitter):
|
|||||||
async def start(self):
|
async def start(self):
|
||||||
if self.packet_pump:
|
if self.packet_pump:
|
||||||
return await self.packet_pump.start(self.stream.rtp_channel)
|
return await self.packet_pump.start(self.stream.rtp_channel)
|
||||||
else:
|
|
||||||
self.emit('start', self.stream.rtp_channel)
|
self.emit('start', self.stream.rtp_channel)
|
||||||
|
|
||||||
async def stop(self):
|
async def stop(self):
|
||||||
if self.packet_pump:
|
if self.packet_pump:
|
||||||
return await self.packet_pump.stop()
|
return await self.packet_pump.stop()
|
||||||
else:
|
|
||||||
self.emit('stop')
|
self.emit('stop')
|
||||||
|
|
||||||
def on_set_configuration_command(self, configuration):
|
def on_set_configuration_command(self, configuration):
|
||||||
# For now, blindly accept the configuration
|
# For now, blindly accept the configuration
|
||||||
@@ -2018,6 +2082,7 @@ class LocalSink(LocalStreamEndPoint, EventEmitter):
|
|||||||
def on_avdtp_packet(self, packet):
|
def on_avdtp_packet(self, packet):
|
||||||
rtp_packet = MediaPacket.from_bytes(packet)
|
rtp_packet = MediaPacket.from_bytes(packet)
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'{color("<<< RTP Packet:", "green")} {rtp_packet} {rtp_packet.payload[:16].hex()}'
|
f'{color("<<< RTP Packet:", "green")} '
|
||||||
|
f'{rtp_packet} {rtp_packet.payload[:16].hex()}'
|
||||||
)
|
)
|
||||||
self.emit('rtp_packet', rtp_packet)
|
self.emit('rtp_packet', rtp_packet)
|
||||||
|
|||||||
+26
-25
@@ -17,6 +17,7 @@
|
|||||||
# the `generate_company_id_list.py` script
|
# the `generate_company_id_list.py` script
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# pylint: disable=line-too-long
|
||||||
COMPANY_IDENTIFIERS = {
|
COMPANY_IDENTIFIERS = {
|
||||||
0x0000: "Ericsson Technology Licensing",
|
0x0000: "Ericsson Technology Licensing",
|
||||||
0x0001: "Nokia Mobile Phones",
|
0x0001: "Nokia Mobile Phones",
|
||||||
@@ -196,28 +197,28 @@ COMPANY_IDENTIFIERS = {
|
|||||||
0x00AF: "Cinetix",
|
0x00AF: "Cinetix",
|
||||||
0x00B0: "Passif Semiconductor Corp",
|
0x00B0: "Passif Semiconductor Corp",
|
||||||
0x00B1: "Saris Cycling Group, Inc",
|
0x00B1: "Saris Cycling Group, Inc",
|
||||||
0x00B2: "Bekey A/S",
|
0x00B2: "Bekey A/S",
|
||||||
0x00B3: "Clarinox Technologies Pty. Ltd.",
|
0x00B3: "Clarinox Technologies Pty. Ltd.",
|
||||||
0x00B4: "BDE Technology Co., Ltd.",
|
0x00B4: "BDE Technology Co., Ltd.",
|
||||||
0x00B5: "Swirl Networks",
|
0x00B5: "Swirl Networks",
|
||||||
0x00B6: "Meso international",
|
0x00B6: "Meso international",
|
||||||
0x00B7: "TreLab Ltd",
|
0x00B7: "TreLab Ltd",
|
||||||
0x00B8: "Qualcomm Innovation Center, Inc. (QuIC)",
|
0x00B8: "Qualcomm Innovation Center, Inc. (QuIC)",
|
||||||
0x00B9: "Johnson Controls, Inc.",
|
0x00B9: "Johnson Controls, Inc.",
|
||||||
0x00BA: "Starkey Laboratories Inc.",
|
0x00BA: "Starkey Laboratories Inc.",
|
||||||
0x00BB: "S-Power Electronics Limited",
|
0x00BB: "S-Power Electronics Limited",
|
||||||
0x00BC: "Ace Sensor Inc",
|
0x00BC: "Ace Sensor Inc",
|
||||||
0x00BD: "Aplix Corporation",
|
0x00BD: "Aplix Corporation",
|
||||||
0x00BE: "AAMP of America",
|
0x00BE: "AAMP of America",
|
||||||
0x00BF: "Stalmart Technology Limited",
|
0x00BF: "Stalmart Technology Limited",
|
||||||
0x00C0: "AMICCOM Electronics Corporation",
|
0x00C0: "AMICCOM Electronics Corporation",
|
||||||
0x00C1: "Shenzhen Excelsecu Data Technology Co.,Ltd",
|
0x00C1: "Shenzhen Excelsecu Data Technology Co.,Ltd",
|
||||||
0x00C2: "Geneq Inc.",
|
0x00C2: "Geneq Inc.",
|
||||||
0x00C3: "adidas AG",
|
0x00C3: "adidas AG",
|
||||||
0x00C4: "LG Electronics",
|
0x00C4: "LG Electronics",
|
||||||
0x00C5: "Onset Computer Corporation",
|
0x00C5: "Onset Computer Corporation",
|
||||||
0x00C6: "Selfly BV",
|
0x00C6: "Selfly BV",
|
||||||
0x00C7: "Quuppa Oy.",
|
0x00C7: "Quuppa Oy.",
|
||||||
0x00C8: "GeLo Inc",
|
0x00C8: "GeLo Inc",
|
||||||
0x00C9: "Evluma",
|
0x00C9: "Evluma",
|
||||||
0x00CA: "MC10",
|
0x00CA: "MC10",
|
||||||
@@ -249,10 +250,10 @@ COMPANY_IDENTIFIERS = {
|
|||||||
0x00E4: "Laird Connectivity, Inc. formerly L.S. Research Inc.",
|
0x00E4: "Laird Connectivity, Inc. formerly L.S. Research Inc.",
|
||||||
0x00E5: "Eden Software Consultants Ltd.",
|
0x00E5: "Eden Software Consultants Ltd.",
|
||||||
0x00E6: "Freshtemp",
|
0x00E6: "Freshtemp",
|
||||||
0x00E7: "KS Technologies",
|
0x00E7: "KS Technologies",
|
||||||
0x00E8: "ACTS Technologies",
|
0x00E8: "ACTS Technologies",
|
||||||
0x00E9: "Vtrack Systems",
|
0x00E9: "Vtrack Systems",
|
||||||
0x00EA: "Nielsen-Kellerman Company",
|
0x00EA: "Nielsen-Kellerman Company",
|
||||||
0x00EB: "Server Technology Inc.",
|
0x00EB: "Server Technology Inc.",
|
||||||
0x00EC: "BioResearch Associates",
|
0x00EC: "BioResearch Associates",
|
||||||
0x00ED: "Jolly Logic, LLC",
|
0x00ED: "Jolly Logic, LLC",
|
||||||
|
|||||||
+109
-56
@@ -19,9 +19,37 @@ import logging
|
|||||||
import asyncio
|
import asyncio
|
||||||
import itertools
|
import itertools
|
||||||
import random
|
import random
|
||||||
|
import struct
|
||||||
|
from colors import color
|
||||||
|
from bumble.core import BT_CENTRAL_ROLE, BT_PERIPHERAL_ROLE
|
||||||
|
|
||||||
|
from bumble.hci import (
|
||||||
|
HCI_ACL_DATA_PACKET,
|
||||||
|
HCI_COMMAND_DISALLOWED_ERROR,
|
||||||
|
HCI_COMMAND_PACKET,
|
||||||
|
HCI_COMMAND_STATUS_PENDING,
|
||||||
|
HCI_CONNECTION_TIMEOUT_ERROR,
|
||||||
|
HCI_EVENT_PACKET,
|
||||||
|
HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR,
|
||||||
|
HCI_LE_1M_PHY,
|
||||||
|
HCI_SUCCESS,
|
||||||
|
HCI_UNKNOWN_HCI_COMMAND_ERROR,
|
||||||
|
HCI_VERSION_BLUETOOTH_CORE_5_0,
|
||||||
|
Address,
|
||||||
|
HCI_AclDataPacket,
|
||||||
|
HCI_AclDataPacketAssembler,
|
||||||
|
HCI_Command_Complete_Event,
|
||||||
|
HCI_Command_Status_Event,
|
||||||
|
HCI_Disconnection_Complete_Event,
|
||||||
|
HCI_Encryption_Change_Event,
|
||||||
|
HCI_LE_Advertising_Report_Event,
|
||||||
|
HCI_LE_Connection_Complete_Event,
|
||||||
|
HCI_LE_Read_Remote_Features_Complete_Event,
|
||||||
|
HCI_Number_Of_Completed_Packets_Event,
|
||||||
|
HCI_Object,
|
||||||
|
HCI_Packet,
|
||||||
|
)
|
||||||
|
|
||||||
from .hci import *
|
|
||||||
from .l2cap import *
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Logging
|
# Logging
|
||||||
@@ -83,13 +111,19 @@ class Controller:
|
|||||||
self.manufacturer_name = 0xFFFF
|
self.manufacturer_name = 0xFFFF
|
||||||
self.hc_le_data_packet_length = 27
|
self.hc_le_data_packet_length = 27
|
||||||
self.hc_total_num_le_data_packets = 64
|
self.hc_total_num_le_data_packets = 64
|
||||||
|
self.event_mask = 0
|
||||||
|
self.event_mask_page_2 = 0
|
||||||
self.supported_commands = bytes.fromhex(
|
self.supported_commands = bytes.fromhex(
|
||||||
'2000800000c000000000e40000002822000000000000040000f7ffff7f00000030f0f9ff01008004000000000000000000000000000000000000000000000000'
|
'2000800000c000000000e40000002822000000000000040000f7ffff7f000000'
|
||||||
|
'30f0f9ff01008004000000000000000000000000000000000000000000000000'
|
||||||
)
|
)
|
||||||
|
self.le_event_mask = 0
|
||||||
|
self.advertising_parameters = None
|
||||||
self.le_features = bytes.fromhex('ff49010000000000')
|
self.le_features = bytes.fromhex('ff49010000000000')
|
||||||
self.le_states = bytes.fromhex('ffff3fffff030000')
|
self.le_states = bytes.fromhex('ffff3fffff030000')
|
||||||
self.advertising_channel_tx_power = 0
|
self.advertising_channel_tx_power = 0
|
||||||
self.filter_accept_list_size = 8
|
self.filter_accept_list_size = 8
|
||||||
|
self.filter_duplicates = False
|
||||||
self.resolving_list_size = 8
|
self.resolving_list_size = 8
|
||||||
self.supported_max_tx_octets = 27
|
self.supported_max_tx_octets = 27
|
||||||
self.supported_max_tx_time = 10000 # microseconds
|
self.supported_max_tx_time = 10000 # microseconds
|
||||||
@@ -133,7 +167,8 @@ class Controller:
|
|||||||
@host.setter
|
@host.setter
|
||||||
def host(self, host):
|
def host(self, host):
|
||||||
'''
|
'''
|
||||||
Sets the host (sink) for this controller, and set this controller as the controller (sink) for the host
|
Sets the host (sink) for this controller, and set this controller as the
|
||||||
|
controller (sink) for the host
|
||||||
'''
|
'''
|
||||||
self.set_packet_sink(host)
|
self.set_packet_sink(host)
|
||||||
if host:
|
if host:
|
||||||
@@ -151,7 +186,7 @@ class Controller:
|
|||||||
|
|
||||||
@public_address.setter
|
@public_address.setter
|
||||||
def public_address(self, address):
|
def public_address(self, address):
|
||||||
if type(address) is str:
|
if isinstance(address, str):
|
||||||
address = Address(address)
|
address = Address(address)
|
||||||
self._public_address = address
|
self._public_address = address
|
||||||
|
|
||||||
@@ -161,7 +196,7 @@ class Controller:
|
|||||||
|
|
||||||
@random_address.setter
|
@random_address.setter
|
||||||
def random_address(self, address):
|
def random_address(self, address):
|
||||||
if type(address) is str:
|
if isinstance(address, str):
|
||||||
address = Address(address)
|
address = Address(address)
|
||||||
self._random_address = address
|
self._random_address = address
|
||||||
logger.debug(f'new random address: {address}')
|
logger.debug(f'new random address: {address}')
|
||||||
@@ -175,7 +210,8 @@ class Controller:
|
|||||||
|
|
||||||
def on_hci_packet(self, packet):
|
def on_hci_packet(self, packet):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'{color("<<<", "blue")} [{self.name}] {color("HOST -> CONTROLLER", "blue")}: {packet}'
|
f'{color("<<<", "blue")} [{self.name}] '
|
||||||
|
f'{color("HOST -> CONTROLLER", "blue")}: {packet}'
|
||||||
)
|
)
|
||||||
|
|
||||||
# If the packet is a command, invoke the handler for this packet
|
# If the packet is a command, invoke the handler for this packet
|
||||||
@@ -192,7 +228,7 @@ class Controller:
|
|||||||
handler_name = f'on_{command.name.lower()}'
|
handler_name = f'on_{command.name.lower()}'
|
||||||
handler = getattr(self, handler_name, self.on_hci_command)
|
handler = getattr(self, handler_name, self.on_hci_command)
|
||||||
result = handler(command)
|
result = handler(command)
|
||||||
if type(result) is bytes:
|
if isinstance(result, bytes):
|
||||||
self.send_hci_packet(
|
self.send_hci_packet(
|
||||||
HCI_Command_Complete_Event(
|
HCI_Command_Complete_Event(
|
||||||
num_hci_command_packets=1,
|
num_hci_command_packets=1,
|
||||||
@@ -201,7 +237,7 @@ class Controller:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_hci_event_packet(self, event):
|
def on_hci_event_packet(self, _event):
|
||||||
logger.warning('!!! unexpected event packet')
|
logger.warning('!!! unexpected event packet')
|
||||||
|
|
||||||
def on_hci_acl_data_packet(self, packet):
|
def on_hci_acl_data_packet(self, packet):
|
||||||
@@ -218,7 +254,8 @@ class Controller:
|
|||||||
|
|
||||||
def send_hci_packet(self, packet):
|
def send_hci_packet(self, packet):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'{color(">>>", "green")} [{self.name}] {color("CONTROLLER -> HOST", "green")}: {packet}'
|
f'{color(">>>", "green")} [{self.name}] '
|
||||||
|
f'{color("CONTROLLER -> HOST", "green")}: {packet}'
|
||||||
)
|
)
|
||||||
if self.host:
|
if self.host:
|
||||||
self.host.on_packet(packet.to_bytes())
|
self.host.on_packet(packet.to_bytes())
|
||||||
@@ -312,7 +349,7 @@ class Controller:
|
|||||||
# Remove the connection
|
# Remove the connection
|
||||||
del self.peripheral_connections[peer_address]
|
del self.peripheral_connections[peer_address]
|
||||||
else:
|
else:
|
||||||
logger.warn(f'!!! No peripheral connection found for {peer_address}')
|
logger.warning(f'!!! No peripheral connection found for {peer_address}')
|
||||||
|
|
||||||
def on_link_peripheral_connection_complete(
|
def on_link_peripheral_connection_complete(
|
||||||
self, le_create_connection_command, status
|
self, le_create_connection_command, status
|
||||||
@@ -339,6 +376,7 @@ class Controller:
|
|||||||
|
|
||||||
# Say that the connection has completed
|
# Say that the connection has completed
|
||||||
self.send_hci_packet(
|
self.send_hci_packet(
|
||||||
|
# pylint: disable=line-too-long
|
||||||
HCI_LE_Connection_Complete_Event(
|
HCI_LE_Connection_Complete_Event(
|
||||||
status=status,
|
status=status,
|
||||||
connection_handle=connection.handle if connection else 0,
|
connection_handle=connection.handle if connection else 0,
|
||||||
@@ -391,9 +429,9 @@ class Controller:
|
|||||||
# Remove the connection
|
# Remove the connection
|
||||||
del self.central_connections[peer_address]
|
del self.central_connections[peer_address]
|
||||||
else:
|
else:
|
||||||
logger.warn(f'!!! No central connection found for {peer_address}')
|
logger.warning(f'!!! No central connection found for {peer_address}')
|
||||||
|
|
||||||
def on_link_encrypted(self, peer_address, rand, ediv, ltk):
|
def on_link_encrypted(self, peer_address, _rand, _ediv, _ltk):
|
||||||
# For now, just setup the encryption without asking the host
|
# For now, just setup the encryption without asking the host
|
||||||
if connection := self.find_connection_by_address(peer_address):
|
if connection := self.find_connection_by_address(peer_address):
|
||||||
self.send_hci_packet(
|
self.send_hci_packet(
|
||||||
@@ -505,7 +543,7 @@ class Controller:
|
|||||||
command.connection_handle
|
command.connection_handle
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
logger.warn('connection not found')
|
logger.warning('connection not found')
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.link:
|
if self.link:
|
||||||
@@ -521,7 +559,7 @@ class Controller:
|
|||||||
self.event_mask = command.event_mask
|
self.event_mask = command.event_mask
|
||||||
return bytes([HCI_SUCCESS])
|
return bytes([HCI_SUCCESS])
|
||||||
|
|
||||||
def on_hci_reset_command(self, command):
|
def on_hci_reset_command(self, _command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 2, Part E - 7.3.2 Reset Command
|
See Bluetooth spec Vol 2, Part E - 7.3.2 Reset Command
|
||||||
'''
|
'''
|
||||||
@@ -543,7 +581,7 @@ class Controller:
|
|||||||
pass
|
pass
|
||||||
return bytes([HCI_SUCCESS])
|
return bytes([HCI_SUCCESS])
|
||||||
|
|
||||||
def on_hci_read_local_name_command(self, command):
|
def on_hci_read_local_name_command(self, _command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 2, Part E - 7.3.12 Read Local Name Command
|
See Bluetooth spec Vol 2, Part E - 7.3.12 Read Local Name Command
|
||||||
'''
|
'''
|
||||||
@@ -553,21 +591,22 @@ class Controller:
|
|||||||
|
|
||||||
return bytes([HCI_SUCCESS]) + local_name
|
return bytes([HCI_SUCCESS]) + local_name
|
||||||
|
|
||||||
def on_hci_read_class_of_device_command(self, command):
|
def on_hci_read_class_of_device_command(self, _command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 2, Part E - 7.3.25 Read Class of Device Command
|
See Bluetooth spec Vol 2, Part E - 7.3.25 Read Class of Device Command
|
||||||
'''
|
'''
|
||||||
return bytes([HCI_SUCCESS, 0, 0, 0])
|
return bytes([HCI_SUCCESS, 0, 0, 0])
|
||||||
|
|
||||||
def on_hci_write_class_of_device_command(self, command):
|
def on_hci_write_class_of_device_command(self, _command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 2, Part E - 7.3.26 Write Class of Device Command
|
See Bluetooth spec Vol 2, Part E - 7.3.26 Write Class of Device Command
|
||||||
'''
|
'''
|
||||||
return bytes([HCI_SUCCESS])
|
return bytes([HCI_SUCCESS])
|
||||||
|
|
||||||
def on_hci_read_synchronous_flow_control_enable_command(self, command):
|
def on_hci_read_synchronous_flow_control_enable_command(self, _command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 2, Part E - 7.3.36 Read Synchronous Flow Control Enable Command
|
See Bluetooth spec Vol 2, Part E - 7.3.36 Read Synchronous Flow Control Enable
|
||||||
|
Command
|
||||||
'''
|
'''
|
||||||
if self.sync_flow_control:
|
if self.sync_flow_control:
|
||||||
ret = 1
|
ret = 1
|
||||||
@@ -577,7 +616,8 @@ class Controller:
|
|||||||
|
|
||||||
def on_hci_write_synchronous_flow_control_enable_command(self, command):
|
def on_hci_write_synchronous_flow_control_enable_command(self, command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 2, Part E - 7.3.37 Write Synchronous Flow Control Enable Command
|
See Bluetooth spec Vol 2, Part E - 7.3.37 Write Synchronous Flow Control Enable
|
||||||
|
Command
|
||||||
'''
|
'''
|
||||||
ret = HCI_SUCCESS
|
ret = HCI_SUCCESS
|
||||||
if command.synchronous_flow_control_enable == 1:
|
if command.synchronous_flow_control_enable == 1:
|
||||||
@@ -588,7 +628,7 @@ class Controller:
|
|||||||
ret = HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR
|
ret = HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR
|
||||||
return bytes([ret])
|
return bytes([ret])
|
||||||
|
|
||||||
def on_hci_write_simple_pairing_mode_command(self, command):
|
def on_hci_write_simple_pairing_mode_command(self, _command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 2, Part E - 7.3.59 Write Simple Pairing Mode Command
|
See Bluetooth spec Vol 2, Part E - 7.3.59 Write Simple Pairing Mode Command
|
||||||
'''
|
'''
|
||||||
@@ -601,13 +641,13 @@ class Controller:
|
|||||||
self.event_mask_page_2 = command.event_mask_page_2
|
self.event_mask_page_2 = command.event_mask_page_2
|
||||||
return bytes([HCI_SUCCESS])
|
return bytes([HCI_SUCCESS])
|
||||||
|
|
||||||
def on_hci_read_le_host_support_command(self, command):
|
def on_hci_read_le_host_support_command(self, _command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 2, Part E - 7.3.78 Write LE Host Support Command
|
See Bluetooth spec Vol 2, Part E - 7.3.78 Write LE Host Support Command
|
||||||
'''
|
'''
|
||||||
return bytes([HCI_SUCCESS, 1, 0])
|
return bytes([HCI_SUCCESS, 1, 0])
|
||||||
|
|
||||||
def on_hci_write_le_host_support_command(self, command):
|
def on_hci_write_le_host_support_command(self, _command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 2, Part E - 7.3.79 Write LE Host Support Command
|
See Bluetooth spec Vol 2, Part E - 7.3.79 Write LE Host Support Command
|
||||||
'''
|
'''
|
||||||
@@ -616,12 +656,13 @@ class Controller:
|
|||||||
|
|
||||||
def on_hci_write_authenticated_payload_timeout_command(self, command):
|
def on_hci_write_authenticated_payload_timeout_command(self, command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 2, Part E - 7.3.94 Write Authenticated Payload Timeout Command
|
See Bluetooth spec Vol 2, Part E - 7.3.94 Write Authenticated Payload Timeout
|
||||||
|
Command
|
||||||
'''
|
'''
|
||||||
# TODO
|
# TODO
|
||||||
return struct.pack('<BH', HCI_SUCCESS, command.connection_handle)
|
return struct.pack('<BH', HCI_SUCCESS, command.connection_handle)
|
||||||
|
|
||||||
def on_hci_read_local_version_information_command(self, command):
|
def on_hci_read_local_version_information_command(self, _command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 2, Part E - 7.4.1 Read Local Version Information Command
|
See Bluetooth spec Vol 2, Part E - 7.4.1 Read Local Version Information Command
|
||||||
'''
|
'''
|
||||||
@@ -635,19 +676,19 @@ class Controller:
|
|||||||
self.lmp_subversion,
|
self.lmp_subversion,
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_hci_read_local_supported_commands_command(self, command):
|
def on_hci_read_local_supported_commands_command(self, _command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 2, Part E - 7.4.2 Read Local Supported Commands Command
|
See Bluetooth spec Vol 2, Part E - 7.4.2 Read Local Supported Commands Command
|
||||||
'''
|
'''
|
||||||
return bytes([HCI_SUCCESS]) + self.supported_commands
|
return bytes([HCI_SUCCESS]) + self.supported_commands
|
||||||
|
|
||||||
def on_hci_read_local_supported_features_command(self, command):
|
def on_hci_read_local_supported_features_command(self, _command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 2, Part E - 7.4.3 Read Local Supported Features Command
|
See Bluetooth spec Vol 2, Part E - 7.4.3 Read Local Supported Features Command
|
||||||
'''
|
'''
|
||||||
return bytes([HCI_SUCCESS]) + self.lmp_features
|
return bytes([HCI_SUCCESS]) + self.lmp_features
|
||||||
|
|
||||||
def on_hci_read_bd_addr_command(self, command):
|
def on_hci_read_bd_addr_command(self, _command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 2, Part E - 7.4.6 Read BD_ADDR Command
|
See Bluetooth spec Vol 2, Part E - 7.4.6 Read BD_ADDR Command
|
||||||
'''
|
'''
|
||||||
@@ -665,7 +706,7 @@ class Controller:
|
|||||||
self.le_event_mask = command.le_event_mask
|
self.le_event_mask = command.le_event_mask
|
||||||
return bytes([HCI_SUCCESS])
|
return bytes([HCI_SUCCESS])
|
||||||
|
|
||||||
def on_hci_le_read_buffer_size_command(self, command):
|
def on_hci_le_read_buffer_size_command(self, _command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 2, Part E - 7.8.2 LE Read Buffer Size Command
|
See Bluetooth spec Vol 2, Part E - 7.8.2 LE Read Buffer Size Command
|
||||||
'''
|
'''
|
||||||
@@ -676,9 +717,10 @@ class Controller:
|
|||||||
self.hc_total_num_le_data_packets,
|
self.hc_total_num_le_data_packets,
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_hci_le_read_local_supported_features_command(self, command):
|
def on_hci_le_read_local_supported_features_command(self, _command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 2, Part E - 7.8.3 LE Read Local Supported Features Command
|
See Bluetooth spec Vol 2, Part E - 7.8.3 LE Read Local Supported Features
|
||||||
|
Command
|
||||||
'''
|
'''
|
||||||
return bytes([HCI_SUCCESS]) + self.le_features
|
return bytes([HCI_SUCCESS]) + self.le_features
|
||||||
|
|
||||||
@@ -696,9 +738,10 @@ class Controller:
|
|||||||
self.advertising_parameters = command
|
self.advertising_parameters = command
|
||||||
return bytes([HCI_SUCCESS])
|
return bytes([HCI_SUCCESS])
|
||||||
|
|
||||||
def on_hci_le_read_advertising_channel_tx_power_command(self, command):
|
def on_hci_le_read_advertising_channel_tx_power_command(self, _command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 2, Part E - 7.8.6 LE Read Advertising Channel Tx Power Command
|
See Bluetooth spec Vol 2, Part E - 7.8.6 LE Read Advertising Channel Tx Power
|
||||||
|
Command
|
||||||
'''
|
'''
|
||||||
return bytes([HCI_SUCCESS, self.advertising_channel_tx_power])
|
return bytes([HCI_SUCCESS, self.advertising_channel_tx_power])
|
||||||
|
|
||||||
@@ -779,33 +822,36 @@ class Controller:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_hci_le_create_connection_cancel_command(self, command):
|
def on_hci_le_create_connection_cancel_command(self, _command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 2, Part E - 7.8.13 LE Create Connection Cancel Command
|
See Bluetooth spec Vol 2, Part E - 7.8.13 LE Create Connection Cancel Command
|
||||||
'''
|
'''
|
||||||
return bytes([HCI_SUCCESS])
|
return bytes([HCI_SUCCESS])
|
||||||
|
|
||||||
def on_hci_le_read_filter_accept_list_size_command(self, command):
|
def on_hci_le_read_filter_accept_list_size_command(self, _command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 2, Part E - 7.8.14 LE Read Filter Accept List Size Command
|
See Bluetooth spec Vol 2, Part E - 7.8.14 LE Read Filter Accept List Size
|
||||||
|
Command
|
||||||
'''
|
'''
|
||||||
return bytes([HCI_SUCCESS, self.filter_accept_list_size])
|
return bytes([HCI_SUCCESS, self.filter_accept_list_size])
|
||||||
|
|
||||||
def on_hci_le_clear_filter_accept_list_command(self, command):
|
def on_hci_le_clear_filter_accept_list_command(self, _command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 2, Part E - 7.8.15 LE Clear Filter Accept List Command
|
See Bluetooth spec Vol 2, Part E - 7.8.15 LE Clear Filter Accept List Command
|
||||||
'''
|
'''
|
||||||
return bytes([HCI_SUCCESS])
|
return bytes([HCI_SUCCESS])
|
||||||
|
|
||||||
def on_hci_le_add_device_to_filter_accept_list_command(self, command):
|
def on_hci_le_add_device_to_filter_accept_list_command(self, _command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 2, Part E - 7.8.16 LE Add Device To Filter Accept List Command
|
See Bluetooth spec Vol 2, Part E - 7.8.16 LE Add Device To Filter Accept List
|
||||||
|
Command
|
||||||
'''
|
'''
|
||||||
return bytes([HCI_SUCCESS])
|
return bytes([HCI_SUCCESS])
|
||||||
|
|
||||||
def on_hci_le_remove_device_from_filter_accept_list_command(self, command):
|
def on_hci_le_remove_device_from_filter_accept_list_command(self, _command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 2, Part E - 7.8.17 LE Remove Device From Filter Accept List Command
|
See Bluetooth spec Vol 2, Part E - 7.8.17 LE Remove Device From Filter Accept
|
||||||
|
List Command
|
||||||
'''
|
'''
|
||||||
return bytes([HCI_SUCCESS])
|
return bytes([HCI_SUCCESS])
|
||||||
|
|
||||||
@@ -832,7 +878,7 @@ class Controller:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_hci_le_rand_command(self, command):
|
def on_hci_le_rand_command(self, _command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 2, Part E - 7.8.23 LE Rand Command
|
See Bluetooth spec Vol 2, Part E - 7.8.23 LE Rand Command
|
||||||
'''
|
'''
|
||||||
@@ -849,7 +895,7 @@ class Controller:
|
|||||||
command.connection_handle
|
command.connection_handle
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
logger.warn('connection not found')
|
logger.warning('connection not found')
|
||||||
return bytes([HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR])
|
return bytes([HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR])
|
||||||
|
|
||||||
# Notify that the connection is now encrypted
|
# Notify that the connection is now encrypted
|
||||||
@@ -869,15 +915,18 @@ class Controller:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_hci_le_read_supported_states_command(self, command):
|
return None
|
||||||
|
|
||||||
|
def on_hci_le_read_supported_states_command(self, _command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 2, Part E - 7.8.27 LE Read Supported States Command
|
See Bluetooth spec Vol 2, Part E - 7.8.27 LE Read Supported States Command
|
||||||
'''
|
'''
|
||||||
return bytes([HCI_SUCCESS]) + self.le_states
|
return bytes([HCI_SUCCESS]) + self.le_states
|
||||||
|
|
||||||
def on_hci_le_read_suggested_default_data_length_command(self, command):
|
def on_hci_le_read_suggested_default_data_length_command(self, _command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 2, Part E - 7.8.34 LE Read Suggested Default Data Length Command
|
See Bluetooth spec Vol 2, Part E - 7.8.34 LE Read Suggested Default Data Length
|
||||||
|
Command
|
||||||
'''
|
'''
|
||||||
return struct.pack(
|
return struct.pack(
|
||||||
'<BHH',
|
'<BHH',
|
||||||
@@ -888,33 +937,35 @@ class Controller:
|
|||||||
|
|
||||||
def on_hci_le_write_suggested_default_data_length_command(self, command):
|
def on_hci_le_write_suggested_default_data_length_command(self, command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 2, Part E - 7.8.35 LE Write Suggested Default Data Length Command
|
See Bluetooth spec Vol 2, Part E - 7.8.35 LE Write Suggested Default Data Length
|
||||||
|
Command
|
||||||
'''
|
'''
|
||||||
self.suggested_max_tx_octets, self.suggested_max_tx_time = struct.unpack(
|
self.suggested_max_tx_octets, self.suggested_max_tx_time = struct.unpack(
|
||||||
'<HH', command.parameters[:4]
|
'<HH', command.parameters[:4]
|
||||||
)
|
)
|
||||||
return bytes([HCI_SUCCESS])
|
return bytes([HCI_SUCCESS])
|
||||||
|
|
||||||
def on_hci_le_read_local_p_256_public_key_command(self, command):
|
def on_hci_le_read_local_p_256_public_key_command(self, _command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 2, Part E - 7.8.36 LE Read P-256 Public Key Command
|
See Bluetooth spec Vol 2, Part E - 7.8.36 LE Read P-256 Public Key Command
|
||||||
'''
|
'''
|
||||||
# TODO create key and send HCI_LE_Read_Local_P-256_Public_Key_Complete event
|
# TODO create key and send HCI_LE_Read_Local_P-256_Public_Key_Complete event
|
||||||
return bytes([HCI_SUCCESS])
|
return bytes([HCI_SUCCESS])
|
||||||
|
|
||||||
def on_hci_le_add_device_to_resolving_list_command(self, command):
|
def on_hci_le_add_device_to_resolving_list_command(self, _command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 2, Part E - 7.8.38 LE Add Device To Resolving List Command
|
See Bluetooth spec Vol 2, Part E - 7.8.38 LE Add Device To Resolving List
|
||||||
|
Command
|
||||||
'''
|
'''
|
||||||
return bytes([HCI_SUCCESS])
|
return bytes([HCI_SUCCESS])
|
||||||
|
|
||||||
def on_hci_le_clear_resolving_list_command(self, command):
|
def on_hci_le_clear_resolving_list_command(self, _command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 2, Part E - 7.8.40 LE Clear Resolving List Command
|
See Bluetooth spec Vol 2, Part E - 7.8.40 LE Clear Resolving List Command
|
||||||
'''
|
'''
|
||||||
return bytes([HCI_SUCCESS])
|
return bytes([HCI_SUCCESS])
|
||||||
|
|
||||||
def on_hci_le_read_resolving_list_size_command(self, command):
|
def on_hci_le_read_resolving_list_size_command(self, _command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 2, Part E - 7.8.41 LE Read Resolving List Size Command
|
See Bluetooth spec Vol 2, Part E - 7.8.41 LE Read Resolving List Size Command
|
||||||
'''
|
'''
|
||||||
@@ -922,7 +973,8 @@ class Controller:
|
|||||||
|
|
||||||
def on_hci_le_set_address_resolution_enable_command(self, command):
|
def on_hci_le_set_address_resolution_enable_command(self, command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 2, Part E - 7.8.44 LE Set Address Resolution Enable Command
|
See Bluetooth spec Vol 2, Part E - 7.8.44 LE Set Address Resolution Enable
|
||||||
|
Command
|
||||||
'''
|
'''
|
||||||
ret = HCI_SUCCESS
|
ret = HCI_SUCCESS
|
||||||
if command.address_resolution_enable == 1:
|
if command.address_resolution_enable == 1:
|
||||||
@@ -935,12 +987,13 @@ class Controller:
|
|||||||
|
|
||||||
def on_hci_le_set_resolvable_private_address_timeout_command(self, command):
|
def on_hci_le_set_resolvable_private_address_timeout_command(self, command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 2, Part E - 7.8.45 LE Set Resolvable Private Address Timeout Command
|
See Bluetooth spec Vol 2, Part E - 7.8.45 LE Set Resolvable Private Address
|
||||||
|
Timeout Command
|
||||||
'''
|
'''
|
||||||
self.le_rpa_timeout = command.rpa_timeout
|
self.le_rpa_timeout = command.rpa_timeout
|
||||||
return bytes([HCI_SUCCESS])
|
return bytes([HCI_SUCCESS])
|
||||||
|
|
||||||
def on_hci_le_read_maximum_data_length_command(self, command):
|
def on_hci_le_read_maximum_data_length_command(self, _command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 2, Part E - 7.8.46 LE Read Maximum Data Length Command
|
See Bluetooth spec Vol 2, Part E - 7.8.46 LE Read Maximum Data Length Command
|
||||||
'''
|
'''
|
||||||
|
|||||||
+80
-60
@@ -100,7 +100,7 @@ class ProtocolError(BaseError):
|
|||||||
"""Protocol Error"""
|
"""Protocol Error"""
|
||||||
|
|
||||||
|
|
||||||
class TimeoutError(Exception):
|
class TimeoutError(Exception): # pylint: disable=redefined-builtin
|
||||||
"""Timeout Error"""
|
"""Timeout Error"""
|
||||||
|
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ class InvalidStateError(Exception):
|
|||||||
"""Invalid State Error"""
|
"""Invalid State Error"""
|
||||||
|
|
||||||
|
|
||||||
class ConnectionError(BaseError):
|
class ConnectionError(BaseError): # pylint: disable=redefined-builtin
|
||||||
"""Connection Error"""
|
"""Connection Error"""
|
||||||
|
|
||||||
FAILURE = 0x01
|
FAILURE = 0x01
|
||||||
@@ -148,7 +148,7 @@ class UUID:
|
|||||||
UUIDS = [] # Registry of all instances created
|
UUIDS = [] # Registry of all instances created
|
||||||
|
|
||||||
def __init__(self, uuid_str_or_int, name=None):
|
def __init__(self, uuid_str_or_int, name=None):
|
||||||
if type(uuid_str_or_int) is int:
|
if isinstance(uuid_str_or_int, int):
|
||||||
self.uuid_bytes = struct.pack('<H', uuid_str_or_int)
|
self.uuid_bytes = struct.pack('<H', uuid_str_or_int)
|
||||||
else:
|
else:
|
||||||
if len(uuid_str_or_int) == 36:
|
if len(uuid_str_or_int) == 36:
|
||||||
@@ -168,7 +168,8 @@ class UUID:
|
|||||||
self.name = name
|
self.name = name
|
||||||
|
|
||||||
def register(self):
|
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:
|
for uuid in self.UUIDS:
|
||||||
if self == uuid:
|
if self == uuid:
|
||||||
if uuid.name is None:
|
if uuid.name is None:
|
||||||
@@ -180,14 +181,14 @@ class UUID:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_bytes(cls, uuid_bytes, name=None):
|
def from_bytes(cls, uuid_bytes, name=None):
|
||||||
if len(uuid_bytes) in {2, 4, 16}:
|
if len(uuid_bytes) in (2, 4, 16):
|
||||||
self = cls.__new__(cls)
|
self = cls.__new__(cls)
|
||||||
self.uuid_bytes = uuid_bytes
|
self.uuid_bytes = uuid_bytes
|
||||||
self.name = name
|
self.name = name
|
||||||
|
|
||||||
return self.register()
|
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
|
@classmethod
|
||||||
def from_16_bits(cls, uuid_16, name=None):
|
def from_16_bits(cls, uuid_16, name=None):
|
||||||
@@ -198,20 +199,21 @@ class UUID:
|
|||||||
return cls.from_bytes(struct.pack('<I', uuid_32), name)
|
return cls.from_bytes(struct.pack('<I', uuid_32), name)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def parse_uuid(cls, bytes, offset):
|
def parse_uuid(cls, uuid_as_bytes, offset):
|
||||||
return len(bytes), cls.from_bytes(bytes[offset:])
|
return len(uuid_as_bytes), cls.from_bytes(uuid_as_bytes[offset:])
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def parse_uuid_2(cls, bytes, offset):
|
def parse_uuid_2(cls, uuid_as_bytes, offset):
|
||||||
return offset + 2, cls.from_bytes(bytes[offset : offset + 2])
|
return offset + 2, cls.from_bytes(uuid_as_bytes[offset : offset + 2])
|
||||||
|
|
||||||
def to_bytes(self, force_128=False):
|
def to_bytes(self, force_128=False):
|
||||||
if len(self.uuid_bytes) == 16 or not force_128:
|
if len(self.uuid_bytes) == 16 or not force_128:
|
||||||
return self.uuid_bytes
|
return self.uuid_bytes
|
||||||
elif len(self.uuid_bytes) == 4:
|
|
||||||
|
if len(self.uuid_bytes) == 4:
|
||||||
return self.uuid_bytes + UUID.BASE_UUID
|
return self.uuid_bytes + UUID.BASE_UUID
|
||||||
else:
|
|
||||||
return self.uuid_bytes + bytes([0, 0]) + UUID.BASE_UUID
|
return self.uuid_bytes + bytes([0, 0]) + UUID.BASE_UUID
|
||||||
|
|
||||||
def to_pdu_bytes(self):
|
def to_pdu_bytes(self):
|
||||||
'''
|
'''
|
||||||
@@ -225,16 +227,16 @@ class UUID:
|
|||||||
def to_hex_str(self):
|
def to_hex_str(self):
|
||||||
if len(self.uuid_bytes) == 2 or len(self.uuid_bytes) == 4:
|
if len(self.uuid_bytes) == 2 or len(self.uuid_bytes) == 4:
|
||||||
return bytes(reversed(self.uuid_bytes)).hex().upper()
|
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[12:16])).hex(),
|
||||||
bytes(reversed(self.uuid_bytes[10:12])).hex(),
|
bytes(reversed(self.uuid_bytes[10:12])).hex(),
|
||||||
bytes(reversed(self.uuid_bytes[8:10])).hex(),
|
bytes(reversed(self.uuid_bytes[8:10])).hex(),
|
||||||
bytes(reversed(self.uuid_bytes[6:8])).hex(),
|
bytes(reversed(self.uuid_bytes[6:8])).hex(),
|
||||||
bytes(reversed(self.uuid_bytes[0:6])).hex(),
|
bytes(reversed(self.uuid_bytes[0:6])).hex(),
|
||||||
]
|
]
|
||||||
).upper()
|
).upper()
|
||||||
|
|
||||||
def __bytes__(self):
|
def __bytes__(self):
|
||||||
return self.to_bytes()
|
return self.to_bytes()
|
||||||
@@ -242,7 +244,8 @@ class UUID:
|
|||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
if isinstance(other, UUID):
|
if isinstance(other, UUID):
|
||||||
return self.to_bytes(force_128=True) == other.to_bytes(force_128=True)
|
return self.to_bytes(force_128=True) == other.to_bytes(force_128=True)
|
||||||
elif type(other) is str:
|
|
||||||
|
if isinstance(other, str):
|
||||||
return UUID(other) == self
|
return UUID(other) == self
|
||||||
|
|
||||||
return False
|
return False
|
||||||
@@ -252,11 +255,11 @@ class UUID:
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if len(self.uuid_bytes) == 2:
|
if len(self.uuid_bytes) == 2:
|
||||||
v = struct.unpack('<H', self.uuid_bytes)[0]
|
uuid = struct.unpack('<H', self.uuid_bytes)[0]
|
||||||
result = f'UUID-16:{v:04X}'
|
result = f'UUID-16:{uuid:04X}'
|
||||||
elif len(self.uuid_bytes) == 4:
|
elif len(self.uuid_bytes) == 4:
|
||||||
v = struct.unpack('<I', self.uuid_bytes)[0]
|
uuid = struct.unpack('<I', self.uuid_bytes)[0]
|
||||||
result = f'UUID-32:{v:08X}'
|
result = f'UUID-32:{uuid:08X}'
|
||||||
else:
|
else:
|
||||||
result = '-'.join(
|
result = '-'.join(
|
||||||
[
|
[
|
||||||
@@ -267,10 +270,11 @@ class UUID:
|
|||||||
bytes(reversed(self.uuid_bytes[0:6])).hex(),
|
bytes(reversed(self.uuid_bytes[0:6])).hex(),
|
||||||
]
|
]
|
||||||
).upper()
|
).upper()
|
||||||
|
|
||||||
if self.name is not None:
|
if self.name is not None:
|
||||||
return result + f' ({self.name})'
|
return result + f' ({self.name})'
|
||||||
else:
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return str(self)
|
return str(self)
|
||||||
@@ -280,6 +284,7 @@ class UUID:
|
|||||||
# Common UUID constants
|
# Common UUID constants
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# fmt: off
|
# fmt: off
|
||||||
|
# pylint: disable=line-too-long
|
||||||
|
|
||||||
# Protocol Identifiers
|
# Protocol Identifiers
|
||||||
BT_SDP_PROTOCOL_ID = UUID.from_16_bits(0x0001, 'SDP')
|
BT_SDP_PROTOCOL_ID = UUID.from_16_bits(0x0001, 'SDP')
|
||||||
@@ -386,6 +391,7 @@ BT_HDP_SOURCE_SERVICE = UUID.from_16_bits(0x1401,
|
|||||||
BT_HDP_SINK_SERVICE = UUID.from_16_bits(0x1402, 'HDP Sink')
|
BT_HDP_SINK_SERVICE = UUID.from_16_bits(0x1402, 'HDP Sink')
|
||||||
|
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
# pylint: enable=line-too-long
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -393,6 +399,7 @@ BT_HDP_SINK_SERVICE = UUID.from_16_bits(0x1402,
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class DeviceClass:
|
class DeviceClass:
|
||||||
# fmt: off
|
# fmt: off
|
||||||
|
# pylint: disable=line-too-long
|
||||||
|
|
||||||
# Major Service Classes (flags combined with OR)
|
# Major Service Classes (flags combined with OR)
|
||||||
LIMITED_DISCOVERABLE_MODE_SERVICE_CLASS = (1 << 0)
|
LIMITED_DISCOVERABLE_MODE_SERVICE_CLASS = (1 << 0)
|
||||||
@@ -562,6 +569,7 @@ class DeviceClass:
|
|||||||
}
|
}
|
||||||
|
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
# pylint: enable=line-too-long
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def split_class_of_device(class_of_device):
|
def split_class_of_device(class_of_device):
|
||||||
@@ -600,6 +608,7 @@ class DeviceClass:
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class AdvertisingData:
|
class AdvertisingData:
|
||||||
# fmt: off
|
# fmt: off
|
||||||
|
# pylint: disable=line-too-long
|
||||||
|
|
||||||
# This list is only partial, it still needs to be filled in from the spec
|
# This list is only partial, it still needs to be filled in from the spec
|
||||||
FLAGS = 0x01
|
FLAGS = 0x01
|
||||||
@@ -713,8 +722,11 @@ class AdvertisingData:
|
|||||||
BR_EDR_HOST_FLAG = 0x10
|
BR_EDR_HOST_FLAG = 0x10
|
||||||
|
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
# pylint: enable=line-too-long
|
||||||
|
|
||||||
def __init__(self, ad_structures=[]):
|
def __init__(self, ad_structures=None):
|
||||||
|
if ad_structures is None:
|
||||||
|
ad_structures = []
|
||||||
self.ad_structures = ad_structures[:]
|
self.ad_structures = ad_structures[:]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -814,53 +826,65 @@ class AdvertisingData:
|
|||||||
|
|
||||||
return f'[{ad_type_str}]: {ad_data_str}'
|
return f'[{ad_type_str}]: {ad_data_str}'
|
||||||
|
|
||||||
|
# pylint: disable=too-many-return-statements
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def ad_data_to_object(ad_type, ad_data):
|
def ad_data_to_object(ad_type, ad_data):
|
||||||
if ad_type in {
|
if ad_type in (
|
||||||
AdvertisingData.COMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
|
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,
|
AdvertisingData.LIST_OF_16_BIT_SERVICE_SOLICITATION_UUIDS,
|
||||||
}:
|
):
|
||||||
return AdvertisingData.uuid_list_to_objects(ad_data, 2)
|
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.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,
|
AdvertisingData.LIST_OF_32_BIT_SERVICE_SOLICITATION_UUIDS,
|
||||||
}:
|
):
|
||||||
return AdvertisingData.uuid_list_to_objects(ad_data, 4)
|
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.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,
|
AdvertisingData.LIST_OF_128_BIT_SERVICE_SOLICITATION_UUIDS,
|
||||||
}:
|
):
|
||||||
return AdvertisingData.uuid_list_to_objects(ad_data, 16)
|
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:])
|
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:])
|
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:])
|
return (UUID.from_bytes(ad_data[:16]), ad_data[16:])
|
||||||
elif ad_type in {
|
|
||||||
|
if ad_type in (
|
||||||
AdvertisingData.SHORTENED_LOCAL_NAME,
|
AdvertisingData.SHORTENED_LOCAL_NAME,
|
||||||
AdvertisingData.COMPLETE_LOCAL_NAME,
|
AdvertisingData.COMPLETE_LOCAL_NAME,
|
||||||
AdvertisingData.URI,
|
AdvertisingData.URI,
|
||||||
}:
|
):
|
||||||
return ad_data.decode("utf-8")
|
return ad_data.decode("utf-8")
|
||||||
elif ad_type in {AdvertisingData.TX_POWER_LEVEL, AdvertisingData.FLAGS}:
|
|
||||||
|
if ad_type in (AdvertisingData.TX_POWER_LEVEL, AdvertisingData.FLAGS):
|
||||||
return ad_data[0]
|
return ad_data[0]
|
||||||
elif ad_type in {
|
|
||||||
|
if ad_type in (
|
||||||
AdvertisingData.APPEARANCE,
|
AdvertisingData.APPEARANCE,
|
||||||
AdvertisingData.ADVERTISING_INTERVAL,
|
AdvertisingData.ADVERTISING_INTERVAL,
|
||||||
}:
|
):
|
||||||
return struct.unpack('<H', ad_data)[0]
|
return struct.unpack('<H', ad_data)[0]
|
||||||
elif ad_type == AdvertisingData.CLASS_OF_DEVICE:
|
|
||||||
|
if ad_type == AdvertisingData.CLASS_OF_DEVICE:
|
||||||
return struct.unpack('<I', bytes([*ad_data, 0]))[0]
|
return struct.unpack('<I', bytes([*ad_data, 0]))[0]
|
||||||
elif ad_type == AdvertisingData.PERIPHERAL_CONNECTION_INTERVAL_RANGE:
|
|
||||||
|
if ad_type == AdvertisingData.PERIPHERAL_CONNECTION_INTERVAL_RANGE:
|
||||||
return struct.unpack('<HH', ad_data)
|
return struct.unpack('<HH', ad_data)
|
||||||
elif ad_type == AdvertisingData.MANUFACTURER_SPECIFIC_DATA:
|
|
||||||
|
if ad_type == AdvertisingData.MANUFACTURER_SPECIFIC_DATA:
|
||||||
return (struct.unpack_from('<H', ad_data, 0)[0], ad_data[2:])
|
return (struct.unpack_from('<H', ad_data, 0)[0], ad_data[2:])
|
||||||
else:
|
|
||||||
return ad_data
|
return ad_data
|
||||||
|
|
||||||
def append(self, data):
|
def append(self, data):
|
||||||
offset = 0
|
offset = 0
|
||||||
@@ -888,15 +912,11 @@ class AdvertisingData:
|
|||||||
return [
|
return [
|
||||||
process_ad_data(ad[1]) for ad in self.ad_structures if ad[0] == type_id
|
process_ad_data(ad[1]) for ad in self.ad_structures if ad[0] == type_id
|
||||||
]
|
]
|
||||||
else:
|
|
||||||
return next(
|
return next(
|
||||||
(
|
(process_ad_data(ad[1]) for ad in self.ad_structures if ad[0] == type_id),
|
||||||
process_ad_data(ad[1])
|
None,
|
||||||
for ad in self.ad_structures
|
)
|
||||||
if ad[0] == type_id
|
|
||||||
),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
|
|
||||||
def __bytes__(self):
|
def __bytes__(self):
|
||||||
return b''.join(
|
return b''.join(
|
||||||
|
|||||||
+15
-9
@@ -125,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
|
See Bluetooth spec Vol 3, Part H - 2.2.2 Random Address Hash function ah
|
||||||
'''
|
'''
|
||||||
@@ -136,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
|
p1 = bytes([iat, rat]) + preq + pres
|
||||||
@@ -149,7 +150,8 @@ def c1(k, r, preq, pres, iat, rat, ia, ra):
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
def s1(k, r1, r2):
|
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])
|
return e(k, r2[0:8] + r1[0:8])
|
||||||
@@ -170,7 +172,8 @@ def aes_cmac(m, k):
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
def f4(u, v, x, z):
|
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(
|
return bytes(
|
||||||
reversed(
|
reversed(
|
||||||
@@ -182,7 +185,8 @@ def f4(u, v, x, z):
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
def f5(w, n1, n2, a1, a2):
|
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
|
NOTE: this returns a tuple: (MacKey, LTK) in little-endian byte order
|
||||||
'''
|
'''
|
||||||
@@ -222,9 +226,10 @@ def f5(w, n1, n2, a1, a2):
|
|||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
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(
|
return bytes(
|
||||||
reversed(
|
reversed(
|
||||||
@@ -244,7 +249,8 @@ def f6(w, n1, n2, r, io_cap, a1, a2):
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
def g2(u, v, x, y):
|
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(
|
return int.from_bytes(
|
||||||
aes_cmac(
|
aes_cmac(
|
||||||
|
|||||||
+312
-124
@@ -16,29 +16,130 @@
|
|||||||
# Imports
|
# Imports
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
|
import functools
|
||||||
import json
|
import json
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from contextlib import asynccontextmanager, AsyncExitStack
|
from contextlib import asynccontextmanager, AsyncExitStack
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from colors import color
|
||||||
|
|
||||||
from .hci import *
|
from .att import ATT_CID, ATT_DEFAULT_MTU, ATT_PDU
|
||||||
|
from .gatt import Characteristic, Descriptor, Service
|
||||||
|
from .hci import (
|
||||||
|
HCI_CENTRAL_ROLE,
|
||||||
|
HCI_COMMAND_STATUS_PENDING,
|
||||||
|
HCI_CONNECTION_REJECTED_DUE_TO_LIMITED_RESOURCES_ERROR,
|
||||||
|
HCI_DISPLAY_ONLY_IO_CAPABILITY,
|
||||||
|
HCI_DISPLAY_YES_NO_IO_CAPABILITY,
|
||||||
|
HCI_EXTENDED_INQUIRY_MODE,
|
||||||
|
HCI_GENERAL_INQUIRY_LAP,
|
||||||
|
HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR,
|
||||||
|
HCI_KEYBOARD_ONLY_IO_CAPABILITY,
|
||||||
|
HCI_LE_1M_PHY,
|
||||||
|
HCI_LE_1M_PHY_BIT,
|
||||||
|
HCI_LE_2M_PHY,
|
||||||
|
HCI_LE_2M_PHY_LE_SUPPORTED_FEATURE,
|
||||||
|
HCI_LE_CLEAR_RESOLVING_LIST_COMMAND,
|
||||||
|
HCI_LE_CODED_PHY,
|
||||||
|
HCI_LE_CODED_PHY_BIT,
|
||||||
|
HCI_LE_CODED_PHY_LE_SUPPORTED_FEATURE,
|
||||||
|
HCI_LE_EXTENDED_ADVERTISING_LE_SUPPORTED_FEATURE,
|
||||||
|
HCI_LE_EXTENDED_CREATE_CONNECTION_COMMAND,
|
||||||
|
HCI_LE_READ_PHY_COMMAND,
|
||||||
|
HCI_MITM_NOT_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS,
|
||||||
|
HCI_MITM_NOT_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS,
|
||||||
|
HCI_MITM_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS,
|
||||||
|
HCI_MITM_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS,
|
||||||
|
HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
|
||||||
|
HCI_R2_PAGE_SCAN_REPETITION_MODE,
|
||||||
|
HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR,
|
||||||
|
HCI_SUCCESS,
|
||||||
|
HCI_WRITE_LE_HOST_SUPPORT_COMMAND,
|
||||||
|
Address,
|
||||||
|
HCI_Accept_Connection_Request_Command,
|
||||||
|
HCI_Authentication_Requested_Command,
|
||||||
|
HCI_Command_Status_Event,
|
||||||
|
HCI_Constant,
|
||||||
|
HCI_Create_Connection_Cancel_Command,
|
||||||
|
HCI_Create_Connection_Command,
|
||||||
|
HCI_Disconnect_Command,
|
||||||
|
HCI_Encryption_Change_Event,
|
||||||
|
HCI_Error,
|
||||||
|
HCI_IO_Capability_Request_Reply_Command,
|
||||||
|
HCI_Inquiry_Cancel_Command,
|
||||||
|
HCI_Inquiry_Command,
|
||||||
|
HCI_LE_Add_Device_To_Resolving_List_Command,
|
||||||
|
HCI_LE_Advertising_Report_Event,
|
||||||
|
HCI_LE_Clear_Resolving_List_Command,
|
||||||
|
HCI_LE_Connection_Update_Command,
|
||||||
|
HCI_LE_Create_Connection_Cancel_Command,
|
||||||
|
HCI_LE_Create_Connection_Command,
|
||||||
|
HCI_LE_Enable_Encryption_Command,
|
||||||
|
HCI_LE_Extended_Advertising_Report_Event,
|
||||||
|
HCI_LE_Extended_Create_Connection_Command,
|
||||||
|
HCI_LE_Read_PHY_Command,
|
||||||
|
HCI_LE_Set_Advertising_Data_Command,
|
||||||
|
HCI_LE_Set_Advertising_Enable_Command,
|
||||||
|
HCI_LE_Set_Advertising_Parameters_Command,
|
||||||
|
HCI_LE_Set_Default_PHY_Command,
|
||||||
|
HCI_LE_Set_Extended_Scan_Enable_Command,
|
||||||
|
HCI_LE_Set_Extended_Scan_Parameters_Command,
|
||||||
|
HCI_LE_Set_PHY_Command,
|
||||||
|
HCI_LE_Set_Random_Address_Command,
|
||||||
|
HCI_LE_Set_Scan_Enable_Command,
|
||||||
|
HCI_LE_Set_Scan_Parameters_Command,
|
||||||
|
HCI_LE_Set_Scan_Response_Data_Command,
|
||||||
|
HCI_Read_BD_ADDR_Command,
|
||||||
|
HCI_Read_RSSI_Command,
|
||||||
|
HCI_Reject_Connection_Request_Command,
|
||||||
|
HCI_Remote_Name_Request_Command,
|
||||||
|
HCI_Set_Connection_Encryption_Command,
|
||||||
|
HCI_StatusError,
|
||||||
|
HCI_User_Confirmation_Request_Negative_Reply_Command,
|
||||||
|
HCI_User_Confirmation_Request_Reply_Command,
|
||||||
|
HCI_User_Passkey_Request_Negative_Reply_Command,
|
||||||
|
HCI_User_Passkey_Request_Reply_Command,
|
||||||
|
HCI_Write_Class_Of_Device_Command,
|
||||||
|
HCI_Write_Extended_Inquiry_Response_Command,
|
||||||
|
HCI_Write_Inquiry_Mode_Command,
|
||||||
|
HCI_Write_LE_Host_Support_Command,
|
||||||
|
HCI_Write_Local_Name_Command,
|
||||||
|
HCI_Write_Scan_Enable_Command,
|
||||||
|
HCI_Write_Secure_Connections_Host_Support_Command,
|
||||||
|
HCI_Write_Simple_Pairing_Mode_Command,
|
||||||
|
OwnAddressType,
|
||||||
|
phy_list_to_bits,
|
||||||
|
)
|
||||||
from .host import Host
|
from .host import Host
|
||||||
from .gatt import *
|
|
||||||
from .gap import GenericAccessService
|
from .gap import GenericAccessService
|
||||||
from .core import AdvertisingData, BT_CENTRAL_ROLE, BT_PERIPHERAL_ROLE
|
from .core import (
|
||||||
|
BT_BR_EDR_TRANSPORT,
|
||||||
|
BT_CENTRAL_ROLE,
|
||||||
|
BT_LE_TRANSPORT,
|
||||||
|
BT_PERIPHERAL_ROLE,
|
||||||
|
AdvertisingData,
|
||||||
|
CommandTimeoutError,
|
||||||
|
ConnectionPHY,
|
||||||
|
InvalidStateError,
|
||||||
|
)
|
||||||
from .utils import (
|
from .utils import (
|
||||||
AsyncRunner,
|
AsyncRunner,
|
||||||
CompositeEventEmitter,
|
CompositeEventEmitter,
|
||||||
setup_event_forwarding,
|
setup_event_forwarding,
|
||||||
composite_listener,
|
composite_listener,
|
||||||
)
|
)
|
||||||
|
from .keys import (
|
||||||
|
KeyStore,
|
||||||
|
PairingKeys,
|
||||||
|
)
|
||||||
from . import gatt_client
|
from . import gatt_client
|
||||||
from . import gatt_server
|
from . import gatt_server
|
||||||
from . import smp
|
from . import smp
|
||||||
from . import sdp
|
from . import sdp
|
||||||
from . import l2cap
|
from . import l2cap
|
||||||
from . import keys
|
from . import core
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Logging
|
# Logging
|
||||||
@@ -49,6 +150,7 @@ logger = logging.getLogger(__name__)
|
|||||||
# Constants
|
# Constants
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# fmt: off
|
# fmt: off
|
||||||
|
# pylint: disable=line-too-long
|
||||||
|
|
||||||
DEVICE_MIN_SCAN_INTERVAL = 25
|
DEVICE_MIN_SCAN_INTERVAL = 25
|
||||||
DEVICE_MAX_SCAN_INTERVAL = 10240
|
DEVICE_MAX_SCAN_INTERVAL = 10240
|
||||||
@@ -81,6 +183,7 @@ DEVICE_DEFAULT_L2CAP_COC_MPS = l2cap.L2CAP_LE_CREDIT_BASED_CONN
|
|||||||
DEVICE_DEFAULT_L2CAP_COC_MAX_CREDITS = l2cap.L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_INITIAL_CREDITS
|
DEVICE_DEFAULT_L2CAP_COC_MAX_CREDITS = l2cap.L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_INITIAL_CREDITS
|
||||||
|
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
# pylint: enable=line-too-long
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -98,9 +201,13 @@ class Advertisement:
|
|||||||
def from_advertising_report(cls, report):
|
def from_advertising_report(cls, report):
|
||||||
if isinstance(report, HCI_LE_Advertising_Report_Event.Report):
|
if isinstance(report, HCI_LE_Advertising_Report_Event.Report):
|
||||||
return LegacyAdvertisement.from_advertising_report(report)
|
return LegacyAdvertisement.from_advertising_report(report)
|
||||||
elif isinstance(report, HCI_LE_Extended_Advertising_Report_Event.Report):
|
|
||||||
|
if isinstance(report, HCI_LE_Extended_Advertising_Report_Event.Report):
|
||||||
return ExtendedAdvertisement.from_advertising_report(report)
|
return ExtendedAdvertisement.from_advertising_report(report)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
# pylint: disable=line-too-long
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
address,
|
address,
|
||||||
@@ -145,17 +252,17 @@ class LegacyAdvertisement(Advertisement):
|
|||||||
rssi=report.rssi,
|
rssi=report.rssi,
|
||||||
is_legacy=True,
|
is_legacy=True,
|
||||||
is_connectable=report.event_type
|
is_connectable=report.event_type
|
||||||
in {
|
in (
|
||||||
HCI_LE_Advertising_Report_Event.ADV_IND,
|
HCI_LE_Advertising_Report_Event.ADV_IND,
|
||||||
HCI_LE_Advertising_Report_Event.ADV_DIRECT_IND,
|
HCI_LE_Advertising_Report_Event.ADV_DIRECT_IND,
|
||||||
},
|
),
|
||||||
is_directed=report.event_type
|
is_directed=report.event_type
|
||||||
== HCI_LE_Advertising_Report_Event.ADV_DIRECT_IND,
|
== HCI_LE_Advertising_Report_Event.ADV_DIRECT_IND,
|
||||||
is_scannable=report.event_type
|
is_scannable=report.event_type
|
||||||
in {
|
in (
|
||||||
HCI_LE_Advertising_Report_Event.ADV_IND,
|
HCI_LE_Advertising_Report_Event.ADV_IND,
|
||||||
HCI_LE_Advertising_Report_Event.ADV_SCAN_IND,
|
HCI_LE_Advertising_Report_Event.ADV_SCAN_IND,
|
||||||
},
|
),
|
||||||
is_scan_response=report.event_type
|
is_scan_response=report.event_type
|
||||||
== HCI_LE_Advertising_Report_Event.SCAN_RSP,
|
== HCI_LE_Advertising_Report_Event.SCAN_RSP,
|
||||||
data=report.data,
|
data=report.data,
|
||||||
@@ -167,6 +274,7 @@ class ExtendedAdvertisement(Advertisement):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def from_advertising_report(cls, report):
|
def from_advertising_report(cls, report):
|
||||||
# fmt: off
|
# fmt: off
|
||||||
|
# pylint: disable=line-too-long
|
||||||
return cls(
|
return cls(
|
||||||
address = report.address,
|
address = report.address,
|
||||||
rssi = report.rssi,
|
rssi = report.rssi,
|
||||||
@@ -231,6 +339,7 @@ class AdvertisementDataAccumulator:
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class AdvertisingType(IntEnum):
|
class AdvertisingType(IntEnum):
|
||||||
# fmt: off
|
# fmt: off
|
||||||
|
# pylint: disable=line-too-long
|
||||||
UNDIRECTED_CONNECTABLE_SCANNABLE = 0x00 # Undirected, connectable, scannable
|
UNDIRECTED_CONNECTABLE_SCANNABLE = 0x00 # Undirected, connectable, scannable
|
||||||
DIRECTED_CONNECTABLE_HIGH_DUTY = 0x01 # Directed, connectable, non-scannable
|
DIRECTED_CONNECTABLE_HIGH_DUTY = 0x01 # Directed, connectable, non-scannable
|
||||||
UNDIRECTED_SCANNABLE = 0x02 # Undirected, non-connectable, scannable
|
UNDIRECTED_SCANNABLE = 0x02 # Undirected, non-connectable, scannable
|
||||||
@@ -240,33 +349,33 @@ class AdvertisingType(IntEnum):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def has_data(self):
|
def has_data(self):
|
||||||
return self in {
|
return self in (
|
||||||
AdvertisingType.UNDIRECTED_CONNECTABLE_SCANNABLE,
|
AdvertisingType.UNDIRECTED_CONNECTABLE_SCANNABLE,
|
||||||
AdvertisingType.UNDIRECTED_SCANNABLE,
|
AdvertisingType.UNDIRECTED_SCANNABLE,
|
||||||
AdvertisingType.UNDIRECTED,
|
AdvertisingType.UNDIRECTED,
|
||||||
}
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_connectable(self):
|
def is_connectable(self):
|
||||||
return self in {
|
return self in (
|
||||||
AdvertisingType.UNDIRECTED_CONNECTABLE_SCANNABLE,
|
AdvertisingType.UNDIRECTED_CONNECTABLE_SCANNABLE,
|
||||||
AdvertisingType.DIRECTED_CONNECTABLE_HIGH_DUTY,
|
AdvertisingType.DIRECTED_CONNECTABLE_HIGH_DUTY,
|
||||||
AdvertisingType.DIRECTED_CONNECTABLE_LOW_DUTY,
|
AdvertisingType.DIRECTED_CONNECTABLE_LOW_DUTY,
|
||||||
}
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_scannable(self):
|
def is_scannable(self):
|
||||||
return self in {
|
return self in (
|
||||||
AdvertisingType.UNDIRECTED_CONNECTABLE_SCANNABLE,
|
AdvertisingType.UNDIRECTED_CONNECTABLE_SCANNABLE,
|
||||||
AdvertisingType.UNDIRECTED_SCANNABLE,
|
AdvertisingType.UNDIRECTED_SCANNABLE,
|
||||||
}
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_directed(self):
|
def is_directed(self):
|
||||||
return self in {
|
return self in (
|
||||||
AdvertisingType.DIRECTED_CONNECTABLE_HIGH_DUTY,
|
AdvertisingType.DIRECTED_CONNECTABLE_HIGH_DUTY,
|
||||||
AdvertisingType.DIRECTED_CONNECTABLE_LOW_DUTY,
|
AdvertisingType.DIRECTED_CONNECTABLE_LOW_DUTY,
|
||||||
}
|
)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -304,13 +413,13 @@ class Peer:
|
|||||||
async def discover_service(self, uuid):
|
async def discover_service(self, uuid):
|
||||||
return await self.gatt_client.discover_service(uuid)
|
return await self.gatt_client.discover_service(uuid)
|
||||||
|
|
||||||
async def discover_services(self, uuids=[]):
|
async def discover_services(self, uuids=()):
|
||||||
return await self.gatt_client.discover_services(uuids)
|
return await self.gatt_client.discover_services(uuids)
|
||||||
|
|
||||||
async def discover_included_services(self, service):
|
async def discover_included_services(self, service):
|
||||||
return await self.gatt_client.discover_included_services(service)
|
return await self.gatt_client.discover_included_services(service)
|
||||||
|
|
||||||
async def discover_characteristics(self, uuids=[], service=None):
|
async def discover_characteristics(self, uuids=(), service=None):
|
||||||
return await self.gatt_client.discover_characteristics(
|
return await self.gatt_client.discover_characteristics(
|
||||||
uuids=uuids, service=service
|
uuids=uuids, service=service
|
||||||
)
|
)
|
||||||
@@ -369,7 +478,7 @@ class Peer:
|
|||||||
async def __aenter__(self):
|
async def __aenter__(self):
|
||||||
await self.discover_services()
|
await self.discover_services()
|
||||||
for service in self.services:
|
for service in self.services:
|
||||||
await self.discover_characteristics()
|
await service.discover_characteristics()
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@@ -460,7 +569,8 @@ class Connection(CompositeEventEmitter):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def incomplete(cls, device, peer_address):
|
def incomplete(cls, device, peer_address):
|
||||||
"""
|
"""
|
||||||
Instantiate an incomplete connection (ie. one waiting for a HCI Connection Complete event).
|
Instantiate an incomplete connection (ie. one waiting for a HCI Connection
|
||||||
|
Complete event).
|
||||||
Once received it shall be completed using the `.complete` method.
|
Once received it shall be completed using the `.complete` method.
|
||||||
"""
|
"""
|
||||||
return cls(
|
return cls(
|
||||||
@@ -576,13 +686,17 @@ class Connection(CompositeEventEmitter):
|
|||||||
if exc_type is None:
|
if exc_type is None:
|
||||||
try:
|
try:
|
||||||
await self.disconnect()
|
await self.disconnect()
|
||||||
except HCI_StatusError as e:
|
except HCI_StatusError as error:
|
||||||
# Invalid parameter means the connection is no longer valid
|
# Invalid parameter means the connection is no longer valid
|
||||||
if e.error_code != HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR:
|
if error.error_code != HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'Connection(handle=0x{self.handle:04X}, role={self.role_name}, address={self.peer_address})'
|
return (
|
||||||
|
f'Connection(handle=0x{self.handle:04X}, '
|
||||||
|
f'role={self.role_name}, '
|
||||||
|
f'address={self.peer_address})'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -645,7 +759,8 @@ class DeviceConfiguration:
|
|||||||
self.irk = bytes.fromhex(irk)
|
self.irk = bytes.fromhex(irk)
|
||||||
else:
|
else:
|
||||||
# Construct an IRK from the address bytes
|
# Construct an IRK from the address bytes
|
||||||
# NOTE: this is not secure, but will always give the same IRK for the same address
|
# NOTE: this is not secure, but will always give the same IRK for the same
|
||||||
|
# address
|
||||||
address_bytes = bytes(self.address)
|
address_bytes = bytes(self.address)
|
||||||
self.irk = (address_bytes * 3)[:16]
|
self.irk = (address_bytes * 3)[:16]
|
||||||
|
|
||||||
@@ -655,7 +770,7 @@ class DeviceConfiguration:
|
|||||||
self.advertising_data = bytes.fromhex(advertising_data)
|
self.advertising_data = bytes.fromhex(advertising_data)
|
||||||
|
|
||||||
def load_from_file(self, filename):
|
def load_from_file(self, filename):
|
||||||
with open(filename, 'r') as file:
|
with open(filename, 'r', encoding='utf-8') as file:
|
||||||
self.load_from_dict(json.load(file))
|
self.load_from_dict(json.load(file))
|
||||||
|
|
||||||
|
|
||||||
@@ -691,7 +806,8 @@ def with_connection_from_address(function):
|
|||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
# Decorator that tries to convert the first argument from a bluetooth address to a connection
|
# Decorator that tries to convert the first argument from a bluetooth address to a
|
||||||
|
# connection
|
||||||
def try_with_connection_from_address(function):
|
def try_with_connection_from_address(function):
|
||||||
@functools.wraps(function)
|
@functools.wraps(function)
|
||||||
def wrapper(self, address, *args, **kwargs):
|
def wrapper(self, address, *args, **kwargs):
|
||||||
@@ -816,7 +932,7 @@ class Device(CompositeEventEmitter):
|
|||||||
self.advertising_data = config.advertising_data
|
self.advertising_data = config.advertising_data
|
||||||
self.advertising_interval_min = config.advertising_interval_min
|
self.advertising_interval_min = config.advertising_interval_min
|
||||||
self.advertising_interval_max = config.advertising_interval_max
|
self.advertising_interval_max = config.advertising_interval_max
|
||||||
self.keystore = keys.KeyStore.create_for_device(config)
|
self.keystore = KeyStore.create_for_device(config)
|
||||||
self.irk = config.irk
|
self.irk = config.irk
|
||||||
self.le_enabled = config.le_enabled
|
self.le_enabled = config.le_enabled
|
||||||
self.le_simultaneous_enabled = config.le_simultaneous_enabled
|
self.le_simultaneous_enabled = config.le_simultaneous_enabled
|
||||||
@@ -832,7 +948,7 @@ class Device(CompositeEventEmitter):
|
|||||||
descriptors = []
|
descriptors = []
|
||||||
for descriptor in characteristic.get("descriptors", []):
|
for descriptor in characteristic.get("descriptors", []):
|
||||||
new_descriptor = Descriptor(
|
new_descriptor = Descriptor(
|
||||||
descriptor_type=descriptor["descriptor_type"],
|
attribute_type=descriptor["descriptor_type"],
|
||||||
permissions=descriptor["permission"],
|
permissions=descriptor["permission"],
|
||||||
)
|
)
|
||||||
descriptors.append(new_descriptor)
|
descriptors.append(new_descriptor)
|
||||||
@@ -852,7 +968,7 @@ class Device(CompositeEventEmitter):
|
|||||||
|
|
||||||
# If an address is passed, override the address from the config
|
# If an address is passed, override the address from the config
|
||||||
if address:
|
if address:
|
||||||
if type(address) is str:
|
if isinstance(address, str):
|
||||||
address = Address(address)
|
address = Address(address)
|
||||||
self.random_address = address
|
self.random_address = address
|
||||||
|
|
||||||
@@ -914,6 +1030,8 @@ class Device(CompositeEventEmitter):
|
|||||||
if connection := self.connections.get(connection_handle):
|
if connection := self.connections.get(connection_handle):
|
||||||
return connection
|
return connection
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def find_connection_by_bd_addr(
|
def find_connection_by_bd_addr(
|
||||||
self, bd_addr, transport=None, check_address_type=False
|
self, bd_addr, transport=None, check_address_type=False
|
||||||
):
|
):
|
||||||
@@ -927,6 +1045,8 @@ class Device(CompositeEventEmitter):
|
|||||||
if transport is None or connection.transport == transport:
|
if transport is None or connection.transport == transport:
|
||||||
return connection
|
return connection
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def create_l2cap_connector(self, connection, psm):
|
def create_l2cap_connector(self, connection, psm):
|
||||||
return lambda: self.l2cap_channel_manager.connect(connection, psm)
|
return lambda: self.l2cap_channel_manager.connect(connection, psm)
|
||||||
|
|
||||||
@@ -968,9 +1088,9 @@ class Device(CompositeEventEmitter):
|
|||||||
return await asyncio.wait_for(
|
return await asyncio.wait_for(
|
||||||
self.host.send_command(command, check_result), self.command_timeout
|
self.host.send_command(command, check_result), self.command_timeout
|
||||||
)
|
)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError as error:
|
||||||
logger.warning('!!! Command timed out')
|
logger.warning('!!! Command timed out')
|
||||||
raise CommandTimeoutError()
|
raise CommandTimeoutError() from error
|
||||||
|
|
||||||
async def power_on(self):
|
async def power_on(self):
|
||||||
# Reset the controller
|
# Reset the controller
|
||||||
@@ -1017,7 +1137,9 @@ class Device(CompositeEventEmitter):
|
|||||||
|
|
||||||
# Enable address resolution
|
# Enable address resolution
|
||||||
# await self.send_command(
|
# await self.send_command(
|
||||||
# HCI_LE_Set_Address_Resolution_Enable_Command(address_resolution_enable=1)
|
# HCI_LE_Set_Address_Resolution_Enable_Command(
|
||||||
|
# address_resolution_enable=1)
|
||||||
|
# )
|
||||||
# )
|
# )
|
||||||
|
|
||||||
# Create a host-side address resolver
|
# Create a host-side address resolver
|
||||||
@@ -1171,7 +1293,7 @@ class Device(CompositeEventEmitter):
|
|||||||
raise ValueError('scan_interval out of range')
|
raise ValueError('scan_interval out of range')
|
||||||
|
|
||||||
# Reset the accumulators
|
# Reset the accumulators
|
||||||
self.advertisement_accumulator = {}
|
self.advertisement_accumulators = {}
|
||||||
|
|
||||||
# Enable scanning
|
# Enable scanning
|
||||||
if not legacy and self.supports_le_feature(
|
if not legacy and self.supports_le_feature(
|
||||||
@@ -1230,6 +1352,7 @@ class Device(CompositeEventEmitter):
|
|||||||
else HCI_LE_Set_Scan_Parameters_Command.PASSIVE_SCANNING
|
else HCI_LE_Set_Scan_Parameters_Command.PASSIVE_SCANNING
|
||||||
)
|
)
|
||||||
await self.send_command(
|
await self.send_command(
|
||||||
|
# pylint: disable=line-too-long
|
||||||
HCI_LE_Set_Scan_Parameters_Command(
|
HCI_LE_Set_Scan_Parameters_Command(
|
||||||
le_scan_type=scan_type,
|
le_scan_type=scan_type,
|
||||||
le_scan_interval=int(scan_window / 0.625),
|
le_scan_interval=int(scan_window / 0.625),
|
||||||
@@ -1376,17 +1499,19 @@ class Device(CompositeEventEmitter):
|
|||||||
):
|
):
|
||||||
'''
|
'''
|
||||||
Request a connection to a peer.
|
Request a connection to a peer.
|
||||||
When transport is BLE, this method cannot be called if there is already a pending connection.
|
When transport is BLE, this method cannot be called if there is already a
|
||||||
|
pending connection.
|
||||||
|
|
||||||
connection_parameters_preferences: (BLE only, ignored for BR/EDR)
|
connection_parameters_preferences: (BLE only, ignored for BR/EDR)
|
||||||
* None: use all PHYs with default parameters
|
* None: use all PHYs with default parameters
|
||||||
* map: each entry has a PHY as key and a ConnectionParametersPreferences object as value
|
* map: each entry has a PHY as key and a ConnectionParametersPreferences
|
||||||
|
object as value
|
||||||
|
|
||||||
own_address_type: (BLE only)
|
own_address_type: (BLE only)
|
||||||
'''
|
'''
|
||||||
|
|
||||||
# Check parameters
|
# Check parameters
|
||||||
if transport not in {BT_LE_TRANSPORT, BT_BR_EDR_TRANSPORT}:
|
if transport not in (BT_LE_TRANSPORT, BT_BR_EDR_TRANSPORT):
|
||||||
raise ValueError('invalid transport')
|
raise ValueError('invalid transport')
|
||||||
|
|
||||||
# Adjust the transport automatically if we need to
|
# Adjust the transport automatically if we need to
|
||||||
@@ -1399,7 +1524,7 @@ class Device(CompositeEventEmitter):
|
|||||||
if transport == BT_LE_TRANSPORT and self.is_le_connecting:
|
if transport == BT_LE_TRANSPORT and self.is_le_connecting:
|
||||||
raise InvalidStateError('connection already pending')
|
raise InvalidStateError('connection already pending')
|
||||||
|
|
||||||
if type(peer_address) is str:
|
if isinstance(peer_address, str):
|
||||||
try:
|
try:
|
||||||
peer_address = Address.from_string_for_transport(
|
peer_address = Address.from_string_for_transport(
|
||||||
peer_address, transport
|
peer_address, transport
|
||||||
@@ -1590,27 +1715,26 @@ class Device(CompositeEventEmitter):
|
|||||||
# Wait for the connection process to complete
|
# Wait for the connection process to complete
|
||||||
if transport == BT_LE_TRANSPORT:
|
if transport == BT_LE_TRANSPORT:
|
||||||
self.le_connecting = True
|
self.le_connecting = True
|
||||||
|
|
||||||
if timeout is None:
|
if timeout is None:
|
||||||
return await self.abort_on('flush', pending_connection)
|
return await self.abort_on('flush', pending_connection)
|
||||||
else:
|
|
||||||
try:
|
|
||||||
return await asyncio.wait_for(
|
|
||||||
asyncio.shield(pending_connection), timeout
|
|
||||||
)
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
if transport == BT_LE_TRANSPORT:
|
|
||||||
await self.send_command(
|
|
||||||
HCI_LE_Create_Connection_Cancel_Command()
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
await self.send_command(
|
|
||||||
HCI_Create_Connection_Cancel_Command(bd_addr=peer_address)
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return await self.abort_on('flush', pending_connection)
|
return await asyncio.wait_for(
|
||||||
except ConnectionError:
|
asyncio.shield(pending_connection), timeout
|
||||||
raise TimeoutError()
|
)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
if transport == BT_LE_TRANSPORT:
|
||||||
|
await self.send_command(HCI_LE_Create_Connection_Cancel_Command())
|
||||||
|
else:
|
||||||
|
await self.send_command(
|
||||||
|
HCI_Create_Connection_Cancel_Command(bd_addr=peer_address)
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return await self.abort_on('flush', pending_connection)
|
||||||
|
except ConnectionError as error:
|
||||||
|
raise core.TimeoutError() from error
|
||||||
finally:
|
finally:
|
||||||
self.remove_listener('connection', on_connection)
|
self.remove_listener('connection', on_connection)
|
||||||
self.remove_listener('connection_failure', on_connection_failure)
|
self.remove_listener('connection_failure', on_connection_failure)
|
||||||
@@ -1627,15 +1751,17 @@ class Device(CompositeEventEmitter):
|
|||||||
timeout=DEVICE_DEFAULT_CONNECT_TIMEOUT,
|
timeout=DEVICE_DEFAULT_CONNECT_TIMEOUT,
|
||||||
):
|
):
|
||||||
'''
|
'''
|
||||||
Wait and accept any incoming connection or a connection from `peer_address` when set.
|
Wait and accept any incoming connection or a connection from `peer_address` when
|
||||||
|
set.
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
* A `connect` to the same peer will also complete this call.
|
* A `connect` to the same peer will also complete this call.
|
||||||
* The `timeout` parameter is only handled while waiting for the connection request,
|
* The `timeout` parameter is only handled while waiting for the connection
|
||||||
once received and accepted, the controller shall issue a connection complete event.
|
request, once received and accepted, the controller shall issue a connection
|
||||||
|
complete event.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
if type(peer_address) is str:
|
if isinstance(peer_address, str):
|
||||||
try:
|
try:
|
||||||
peer_address = Address(peer_address)
|
peer_address = Address(peer_address)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@@ -1680,7 +1806,7 @@ class Device(CompositeEventEmitter):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
# Otherwise, result came from `on_connection_request`
|
# Otherwise, result came from `on_connection_request`
|
||||||
peer_address, class_of_device, link_type = result
|
peer_address, _class_of_device, _link_type = result
|
||||||
|
|
||||||
# Create a future so that we can wait for the connection's result
|
# Create a future so that we can wait for the connection's result
|
||||||
pending_connection = asyncio.get_running_loop().create_future()
|
pending_connection = asyncio.get_running_loop().create_future()
|
||||||
@@ -1749,9 +1875,10 @@ class Device(CompositeEventEmitter):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# BR/EDR: try to cancel to ongoing connection
|
# BR/EDR: try to cancel to ongoing connection
|
||||||
# NOTE: This API does not prevent from trying to cancel a connection which is not currently being created
|
# NOTE: This API does not prevent from trying to cancel a connection which is
|
||||||
|
# not currently being created
|
||||||
else:
|
else:
|
||||||
if type(peer_address) is str:
|
if isinstance(peer_address, str):
|
||||||
try:
|
try:
|
||||||
peer_address = Address(peer_address)
|
peer_address = Address(peer_address)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@@ -1804,7 +1931,8 @@ class Device(CompositeEventEmitter):
|
|||||||
max_ce_length=0,
|
max_ce_length=0,
|
||||||
):
|
):
|
||||||
'''
|
'''
|
||||||
NOTE: the name of the parameters may look odd, but it just follows the names used in the Bluetooth spec.
|
NOTE: the name of the parameters may look odd, but it just follows the names
|
||||||
|
used in the Bluetooth spec.
|
||||||
'''
|
'''
|
||||||
await self.send_command(
|
await self.send_command(
|
||||||
HCI_LE_Connection_Update_Command(
|
HCI_LE_Connection_Update_Command(
|
||||||
@@ -1881,8 +2009,10 @@ class Device(CompositeEventEmitter):
|
|||||||
if local_name.decode('utf-8') == name:
|
if local_name.decode('utf-8') == name:
|
||||||
peer_address.set_result(address)
|
peer_address.set_result(address)
|
||||||
|
|
||||||
|
handler = None
|
||||||
|
was_scanning = self.scanning
|
||||||
|
was_discovering = self.discovering
|
||||||
try:
|
try:
|
||||||
handler = None
|
|
||||||
if transport == BT_LE_TRANSPORT:
|
if transport == BT_LE_TRANSPORT:
|
||||||
event_name = 'advertisement'
|
event_name = 'advertisement'
|
||||||
handler = self.on(
|
handler = self.on(
|
||||||
@@ -1892,7 +2022,6 @@ class Device(CompositeEventEmitter):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
was_scanning = self.scanning
|
|
||||||
if not self.scanning:
|
if not self.scanning:
|
||||||
await self.start_scanning(filter_duplicates=True)
|
await self.start_scanning(filter_duplicates=True)
|
||||||
|
|
||||||
@@ -1905,7 +2034,6 @@ class Device(CompositeEventEmitter):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
was_discovering = self.discovering
|
|
||||||
if not self.discovering:
|
if not self.discovering:
|
||||||
await self.start_discovery()
|
await self.start_discovery()
|
||||||
else:
|
else:
|
||||||
@@ -1951,9 +2079,11 @@ class Device(CompositeEventEmitter):
|
|||||||
logger.debug('found keys in the key store')
|
logger.debug('found keys in the key store')
|
||||||
if keys.ltk:
|
if keys.ltk:
|
||||||
return keys.ltk.value
|
return keys.ltk.value
|
||||||
elif connection.role == BT_CENTRAL_ROLE and keys.ltk_central:
|
|
||||||
|
if connection.role == BT_CENTRAL_ROLE and keys.ltk_central:
|
||||||
return keys.ltk_central.value
|
return keys.ltk_central.value
|
||||||
elif connection.role == BT_PERIPHERAL_ROLE and keys.ltk_peripheral:
|
|
||||||
|
if connection.role == BT_PERIPHERAL_ROLE and keys.ltk_peripheral:
|
||||||
return keys.ltk_peripheral.value
|
return keys.ltk_peripheral.value
|
||||||
|
|
||||||
async def get_link_key(self, address):
|
async def get_link_key(self, address):
|
||||||
@@ -1986,8 +2116,9 @@ class Device(CompositeEventEmitter):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
if result.status != HCI_COMMAND_STATUS_PENDING:
|
if result.status != HCI_COMMAND_STATUS_PENDING:
|
||||||
logger.warn(
|
logger.warning(
|
||||||
f'HCI_Authentication_Requested_Command failed: {HCI_Constant.error_name(result.status)}'
|
'HCI_Authentication_Requested_Command failed: '
|
||||||
|
f'{HCI_Constant.error_name(result.status)}'
|
||||||
)
|
)
|
||||||
raise HCI_StatusError(result)
|
raise HCI_StatusError(result)
|
||||||
|
|
||||||
@@ -2050,20 +2181,23 @@ class Device(CompositeEventEmitter):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if result.status != HCI_COMMAND_STATUS_PENDING:
|
if result.status != HCI_COMMAND_STATUS_PENDING:
|
||||||
logger.warn(
|
logger.warning(
|
||||||
f'HCI_LE_Enable_Encryption_Command failed: {HCI_Constant.error_name(result.status)}'
|
'HCI_LE_Enable_Encryption_Command failed: '
|
||||||
|
f'{HCI_Constant.error_name(result.status)}'
|
||||||
)
|
)
|
||||||
raise HCI_StatusError(result)
|
raise HCI_StatusError(result)
|
||||||
else:
|
else:
|
||||||
result = await self.send_command(
|
result = await self.send_command(
|
||||||
HCI_Set_Connection_Encryption_Command(
|
HCI_Set_Connection_Encryption_Command(
|
||||||
connection_handle=connection.handle, encryption_enable=0x01 if enable else 0x00
|
connection_handle=connection.handle,
|
||||||
|
encryption_enable=0x01 if enable else 0x00,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if result.status != HCI_COMMAND_STATUS_PENDING:
|
if result.status != HCI_COMMAND_STATUS_PENDING:
|
||||||
logger.warn(
|
logger.warning(
|
||||||
f'HCI_Set_Connection_Encryption_Command failed: {HCI_Constant.error_name(result.status)}'
|
'HCI_Set_Connection_Encryption_Command failed: '
|
||||||
|
f'{HCI_Constant.error_name(result.status)}'
|
||||||
)
|
)
|
||||||
raise HCI_StatusError(result)
|
raise HCI_StatusError(result)
|
||||||
|
|
||||||
@@ -2082,7 +2216,7 @@ class Device(CompositeEventEmitter):
|
|||||||
# Set up event handlers
|
# Set up event handlers
|
||||||
pending_name = asyncio.get_running_loop().create_future()
|
pending_name = asyncio.get_running_loop().create_future()
|
||||||
|
|
||||||
peer_address = remote if type(remote) == Address else remote.peer_address
|
peer_address = remote if isinstance(remote, Address) else remote.peer_address
|
||||||
|
|
||||||
handler = self.on(
|
handler = self.on(
|
||||||
'remote_name',
|
'remote_name',
|
||||||
@@ -2103,15 +2237,17 @@ class Device(CompositeEventEmitter):
|
|||||||
result = await self.send_command(
|
result = await self.send_command(
|
||||||
HCI_Remote_Name_Request_Command(
|
HCI_Remote_Name_Request_Command(
|
||||||
bd_addr=peer_address,
|
bd_addr=peer_address,
|
||||||
page_scan_repetition_mode=HCI_Remote_Name_Request_Command.R0, # TODO investigate other options
|
# TODO investigate other options
|
||||||
|
page_scan_repetition_mode=HCI_Remote_Name_Request_Command.R0,
|
||||||
reserved=0,
|
reserved=0,
|
||||||
clock_offset=0, # TODO investigate non-0 values
|
clock_offset=0, # TODO investigate non-0 values
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if result.status != HCI_COMMAND_STATUS_PENDING:
|
if result.status != HCI_COMMAND_STATUS_PENDING:
|
||||||
logger.warn(
|
logger.warning(
|
||||||
f'HCI_Set_Connection_Encryption_Command failed: {HCI_Constant.error_name(result.status)}'
|
'HCI_Set_Connection_Encryption_Command failed: '
|
||||||
|
f'{HCI_Constant.error_name(result.status)}'
|
||||||
)
|
)
|
||||||
raise HCI_StatusError(result)
|
raise HCI_StatusError(result)
|
||||||
|
|
||||||
@@ -2133,14 +2269,14 @@ class Device(CompositeEventEmitter):
|
|||||||
def on_link_key(self, bd_addr, link_key, key_type):
|
def on_link_key(self, bd_addr, link_key, key_type):
|
||||||
# Store the keys in the key store
|
# Store the keys in the key store
|
||||||
if self.keystore:
|
if self.keystore:
|
||||||
pairing_keys = keys.PairingKeys()
|
pairing_keys = PairingKeys()
|
||||||
pairing_keys.link_key = keys.PairingKeys.Key(value=link_key)
|
pairing_keys.link_key = PairingKeys.Key(value=link_key)
|
||||||
|
|
||||||
async def store_keys():
|
async def store_keys():
|
||||||
try:
|
try:
|
||||||
await self.keystore.update(str(bd_addr), pairing_keys)
|
await self.keystore.update(str(bd_addr), pairing_keys)
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
logger.warn(f'!!! error while storing keys: {error}')
|
logger.warning(f'!!! error while storing keys: {error}')
|
||||||
|
|
||||||
self.abort_on('flush', store_keys())
|
self.abort_on('flush', store_keys())
|
||||||
|
|
||||||
@@ -2183,10 +2319,11 @@ class Device(CompositeEventEmitter):
|
|||||||
connection_parameters,
|
connection_parameters,
|
||||||
):
|
):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'*** Connection: [0x{connection_handle:04X}] {peer_address} as {HCI_Constant.role_name(role)}'
|
f'*** Connection: [0x{connection_handle:04X}] '
|
||||||
|
f'{peer_address} as {HCI_Constant.role_name(role)}'
|
||||||
)
|
)
|
||||||
if connection_handle in self.connections:
|
if connection_handle in self.connections:
|
||||||
logger.warn(
|
logger.warning(
|
||||||
'new connection reuses the same handle as a previous connection'
|
'new connection reuses the same handle as a previous connection'
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2198,10 +2335,12 @@ class Device(CompositeEventEmitter):
|
|||||||
)
|
)
|
||||||
self.connections[connection_handle] = connection
|
self.connections[connection_handle] = connection
|
||||||
|
|
||||||
# We may have an accept ongoing waiting for a connection request for `peer_address`.
|
# We may have an accept ongoing waiting for a connection request for
|
||||||
# Typically happen when using `connect` to the same `peer_address` we are waiting with
|
# `peer_address`.
|
||||||
# an `accept` for.
|
# Typically happen when using `connect` to the same `peer_address` we are
|
||||||
# In this case, set the completed `connection` to the `accept` future result.
|
# waiting for with an `accept`.
|
||||||
|
# In this case, set the completed `connection` to the `accept` future
|
||||||
|
# result.
|
||||||
if peer_address in self.classic_pending_accepts:
|
if peer_address in self.classic_pending_accepts:
|
||||||
future = self.classic_pending_accepts.pop(peer_address)
|
future = self.classic_pending_accepts.pop(peer_address)
|
||||||
future.set_result(connection)
|
future.set_result(connection)
|
||||||
@@ -2234,10 +2373,14 @@ class Device(CompositeEventEmitter):
|
|||||||
async def new_connection():
|
async def new_connection():
|
||||||
# Figure out which PHY we're connected with
|
# Figure out which PHY we're connected with
|
||||||
if self.host.supports_command(HCI_LE_READ_PHY_COMMAND):
|
if self.host.supports_command(HCI_LE_READ_PHY_COMMAND):
|
||||||
result = await asyncio.shield(self.send_command(
|
result = await asyncio.shield(
|
||||||
HCI_LE_Read_PHY_Command(connection_handle=connection_handle),
|
self.send_command(
|
||||||
check_result=True,
|
HCI_LE_Read_PHY_Command(
|
||||||
))
|
connection_handle=connection_handle
|
||||||
|
),
|
||||||
|
check_result=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
phy = ConnectionPHY(
|
phy = ConnectionPHY(
|
||||||
result.return_parameters.tx_phy, result.return_parameters.rx_phy
|
result.return_parameters.tx_phy, result.return_parameters.rx_phy
|
||||||
)
|
)
|
||||||
@@ -2332,7 +2475,8 @@ class Device(CompositeEventEmitter):
|
|||||||
@with_connection_from_handle
|
@with_connection_from_handle
|
||||||
def on_disconnection(self, connection, reason):
|
def on_disconnection(self, connection, reason):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'*** Disconnection: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}, reason={reason}'
|
f'*** Disconnection: [0x{connection.handle:04X}] '
|
||||||
|
f'{connection.peer_address} as {connection.role_name}, reason={reason}'
|
||||||
)
|
)
|
||||||
connection.emit('disconnection', reason)
|
connection.emit('disconnection', reason)
|
||||||
|
|
||||||
@@ -2345,10 +2489,11 @@ class Device(CompositeEventEmitter):
|
|||||||
# Restart advertising if auto-restart is enabled
|
# Restart advertising if auto-restart is enabled
|
||||||
if self.auto_restart_advertising:
|
if self.auto_restart_advertising:
|
||||||
logger.debug('restarting advertising')
|
logger.debug('restarting advertising')
|
||||||
self.abort_on('flush',
|
self.abort_on(
|
||||||
|
'flush',
|
||||||
self.start_advertising(
|
self.start_advertising(
|
||||||
advertising_type=self.advertising_type, auto_restart=True
|
advertising_type=self.advertising_type, auto_restart=True
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@host_event_handler
|
@host_event_handler
|
||||||
@@ -2379,7 +2524,8 @@ class Device(CompositeEventEmitter):
|
|||||||
@with_connection_from_handle
|
@with_connection_from_handle
|
||||||
def on_connection_authentication(self, connection):
|
def on_connection_authentication(self, connection):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'*** Connection Authentication: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}'
|
f'*** Connection Authentication: [0x{connection.handle:04X}] '
|
||||||
|
f'{connection.peer_address} as {connection.role_name}'
|
||||||
)
|
)
|
||||||
connection.authenticated = True
|
connection.authenticated = True
|
||||||
connection.emit('connection_authentication')
|
connection.emit('connection_authentication')
|
||||||
@@ -2388,10 +2534,25 @@ class Device(CompositeEventEmitter):
|
|||||||
@with_connection_from_handle
|
@with_connection_from_handle
|
||||||
def on_connection_authentication_failure(self, connection, error):
|
def on_connection_authentication_failure(self, connection, error):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'*** Connection Authentication Failure: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}, error={error}'
|
f'*** Connection Authentication Failure: [0x{connection.handle:04X}] '
|
||||||
|
f'{connection.peer_address} as {connection.role_name}, error={error}'
|
||||||
)
|
)
|
||||||
connection.emit('connection_authentication_failure', error)
|
connection.emit('connection_authentication_failure', error)
|
||||||
|
|
||||||
|
@host_event_handler
|
||||||
|
@with_connection_from_address
|
||||||
|
def on_ssp_complete(self, connection):
|
||||||
|
# On Secure Simple Pairing complete, in case:
|
||||||
|
# - Connection isn't already authenticated
|
||||||
|
# - AND we are not the initiator of the authentication
|
||||||
|
# We must trigger authentication to known if we are truly authenticated
|
||||||
|
if not connection.authenticating and not connection.authenticated:
|
||||||
|
logger.debug(
|
||||||
|
f'*** Trigger Connection Authentication: [0x{connection.handle:04X}] '
|
||||||
|
f'{connection.peer_address}'
|
||||||
|
)
|
||||||
|
asyncio.create_task(connection.authenticate())
|
||||||
|
|
||||||
# [Classic only]
|
# [Classic only]
|
||||||
@host_event_handler
|
@host_event_handler
|
||||||
@with_connection_from_address
|
@with_connection_from_address
|
||||||
@@ -2400,6 +2561,7 @@ class Device(CompositeEventEmitter):
|
|||||||
pairing_config = self.pairing_config_factory(connection)
|
pairing_config = self.pairing_config_factory(connection)
|
||||||
|
|
||||||
# Map the SMP IO capability to a Classic IO capability
|
# Map the SMP IO capability to a Classic IO capability
|
||||||
|
# pylint: disable=line-too-long
|
||||||
io_capability = {
|
io_capability = {
|
||||||
smp.SMP_DISPLAY_ONLY_IO_CAPABILITY: HCI_DISPLAY_ONLY_IO_CAPABILITY,
|
smp.SMP_DISPLAY_ONLY_IO_CAPABILITY: HCI_DISPLAY_ONLY_IO_CAPABILITY,
|
||||||
smp.SMP_DISPLAY_YES_NO_IO_CAPABILITY: HCI_DISPLAY_YES_NO_IO_CAPABILITY,
|
smp.SMP_DISPLAY_YES_NO_IO_CAPABILITY: HCI_DISPLAY_YES_NO_IO_CAPABILITY,
|
||||||
@@ -2445,19 +2607,18 @@ class Device(CompositeEventEmitter):
|
|||||||
# Ask what the pairing config should be for this connection
|
# Ask what the pairing config should be for this connection
|
||||||
pairing_config = self.pairing_config_factory(connection)
|
pairing_config = self.pairing_config_factory(connection)
|
||||||
|
|
||||||
can_compare = pairing_config.delegate.io_capability not in {
|
can_compare = pairing_config.delegate.io_capability not in (
|
||||||
smp.SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
|
smp.SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
|
||||||
smp.SMP_DISPLAY_ONLY_IO_CAPABILITY,
|
smp.SMP_DISPLAY_ONLY_IO_CAPABILITY,
|
||||||
}
|
)
|
||||||
|
|
||||||
# Respond
|
# Respond
|
||||||
if can_compare:
|
if can_compare:
|
||||||
|
|
||||||
async def compare_numbers():
|
async def compare_numbers():
|
||||||
numbers_match = await connection.abort_on('disconnection',
|
numbers_match = await connection.abort_on(
|
||||||
pairing_config.delegate.compare_numbers(
|
'disconnection',
|
||||||
code, digits=6
|
pairing_config.delegate.compare_numbers(code, digits=6),
|
||||||
)
|
|
||||||
)
|
)
|
||||||
if numbers_match:
|
if numbers_match:
|
||||||
await self.host.send_command(
|
await self.host.send_command(
|
||||||
@@ -2476,8 +2637,9 @@ class Device(CompositeEventEmitter):
|
|||||||
else:
|
else:
|
||||||
|
|
||||||
async def confirm():
|
async def confirm():
|
||||||
confirm = await connection.abort_on('disconnection',
|
confirm = await connection.abort_on(
|
||||||
pairing_config.delegate.confirm())
|
'disconnection', pairing_config.delegate.confirm()
|
||||||
|
)
|
||||||
if confirm:
|
if confirm:
|
||||||
await self.host.send_command(
|
await self.host.send_command(
|
||||||
HCI_User_Confirmation_Request_Reply_Command(
|
HCI_User_Confirmation_Request_Reply_Command(
|
||||||
@@ -2500,17 +2662,18 @@ class Device(CompositeEventEmitter):
|
|||||||
# Ask what the pairing config should be for this connection
|
# Ask what the pairing config should be for this connection
|
||||||
pairing_config = self.pairing_config_factory(connection)
|
pairing_config = self.pairing_config_factory(connection)
|
||||||
|
|
||||||
can_input = pairing_config.delegate.io_capability in {
|
can_input = pairing_config.delegate.io_capability in (
|
||||||
smp.SMP_KEYBOARD_ONLY_IO_CAPABILITY,
|
smp.SMP_KEYBOARD_ONLY_IO_CAPABILITY,
|
||||||
smp.SMP_KEYBOARD_DISPLAY_IO_CAPABILITY,
|
smp.SMP_KEYBOARD_DISPLAY_IO_CAPABILITY,
|
||||||
}
|
)
|
||||||
|
|
||||||
# Respond
|
# Respond
|
||||||
if can_input:
|
if can_input:
|
||||||
|
|
||||||
async def get_number():
|
async def get_number():
|
||||||
number = await connection.abort_on('disconnection',
|
number = await connection.abort_on(
|
||||||
pairing_config.delegate.get_number())
|
'disconnection', pairing_config.delegate.get_number()
|
||||||
|
)
|
||||||
if number is not None:
|
if number is not None:
|
||||||
await self.host.send_command(
|
await self.host.send_command(
|
||||||
HCI_User_Passkey_Request_Reply_Command(
|
HCI_User_Passkey_Request_Reply_Command(
|
||||||
@@ -2539,7 +2702,9 @@ class Device(CompositeEventEmitter):
|
|||||||
# Ask what the pairing config should be for this connection
|
# Ask what the pairing config should be for this connection
|
||||||
pairing_config = self.pairing_config_factory(connection)
|
pairing_config = self.pairing_config_factory(connection)
|
||||||
|
|
||||||
connection.abort_on('disconnection', pairing_config.delegate.display_number(passkey))
|
connection.abort_on(
|
||||||
|
'disconnection', pairing_config.delegate.display_number(passkey)
|
||||||
|
)
|
||||||
|
|
||||||
# [Classic only]
|
# [Classic only]
|
||||||
@host_event_handler
|
@host_event_handler
|
||||||
@@ -2571,10 +2736,15 @@ class Device(CompositeEventEmitter):
|
|||||||
@with_connection_from_handle
|
@with_connection_from_handle
|
||||||
def on_connection_encryption_change(self, connection, encryption):
|
def on_connection_encryption_change(self, connection, encryption):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'*** Connection Encryption Change: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}, encryption={encryption}'
|
f'*** Connection Encryption Change: [0x{connection.handle:04X}] '
|
||||||
|
f'{connection.peer_address} as {connection.role_name}, '
|
||||||
|
f'encryption={encryption}'
|
||||||
)
|
)
|
||||||
connection.encryption = encryption
|
connection.encryption = encryption
|
||||||
if not connection.authenticated and encryption == HCI_Encryption_Change_Event.AES_CCM:
|
if (
|
||||||
|
not connection.authenticated
|
||||||
|
and encryption == HCI_Encryption_Change_Event.AES_CCM
|
||||||
|
):
|
||||||
connection.authenticated = True
|
connection.authenticated = True
|
||||||
connection.sc = True
|
connection.sc = True
|
||||||
connection.emit('connection_encryption_change')
|
connection.emit('connection_encryption_change')
|
||||||
@@ -2583,7 +2753,9 @@ class Device(CompositeEventEmitter):
|
|||||||
@with_connection_from_handle
|
@with_connection_from_handle
|
||||||
def on_connection_encryption_failure(self, connection, error):
|
def on_connection_encryption_failure(self, connection, error):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'*** Connection Encryption Failure: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}, error={error}'
|
f'*** Connection Encryption Failure: [0x{connection.handle:04X}] '
|
||||||
|
f'{connection.peer_address} as {connection.role_name}, '
|
||||||
|
f'error={error}'
|
||||||
)
|
)
|
||||||
connection.emit('connection_encryption_failure', error)
|
connection.emit('connection_encryption_failure', error)
|
||||||
|
|
||||||
@@ -2591,7 +2763,8 @@ class Device(CompositeEventEmitter):
|
|||||||
@with_connection_from_handle
|
@with_connection_from_handle
|
||||||
def on_connection_encryption_key_refresh(self, connection):
|
def on_connection_encryption_key_refresh(self, connection):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'*** Connection Key Refresh: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}'
|
f'*** Connection Key Refresh: [0x{connection.handle:04X}] '
|
||||||
|
f'{connection.peer_address} as {connection.role_name}'
|
||||||
)
|
)
|
||||||
connection.emit('connection_encryption_key_refresh')
|
connection.emit('connection_encryption_key_refresh')
|
||||||
|
|
||||||
@@ -2599,7 +2772,9 @@ class Device(CompositeEventEmitter):
|
|||||||
@with_connection_from_handle
|
@with_connection_from_handle
|
||||||
def on_connection_parameters_update(self, connection, connection_parameters):
|
def on_connection_parameters_update(self, connection, connection_parameters):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'*** Connection Parameters Update: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}, {connection_parameters}'
|
f'*** Connection Parameters Update: [0x{connection.handle:04X}] '
|
||||||
|
f'{connection.peer_address} as {connection.role_name}, '
|
||||||
|
f'{connection_parameters}'
|
||||||
)
|
)
|
||||||
connection.parameters = connection_parameters
|
connection.parameters = connection_parameters
|
||||||
connection.emit('connection_parameters_update')
|
connection.emit('connection_parameters_update')
|
||||||
@@ -2608,7 +2783,9 @@ class Device(CompositeEventEmitter):
|
|||||||
@with_connection_from_handle
|
@with_connection_from_handle
|
||||||
def on_connection_parameters_update_failure(self, connection, error):
|
def on_connection_parameters_update_failure(self, connection, error):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'*** Connection Parameters Update Failed: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}, error={error}'
|
f'*** Connection Parameters Update Failed: [0x{connection.handle:04X}] '
|
||||||
|
f'{connection.peer_address} as {connection.role_name}, '
|
||||||
|
f'error={error}'
|
||||||
)
|
)
|
||||||
connection.emit('connection_parameters_update_failure', error)
|
connection.emit('connection_parameters_update_failure', error)
|
||||||
|
|
||||||
@@ -2616,7 +2793,9 @@ class Device(CompositeEventEmitter):
|
|||||||
@with_connection_from_handle
|
@with_connection_from_handle
|
||||||
def on_connection_phy_update(self, connection, connection_phy):
|
def on_connection_phy_update(self, connection, connection_phy):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'*** Connection PHY Update: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}, {connection_phy}'
|
f'*** Connection PHY Update: [0x{connection.handle:04X}] '
|
||||||
|
f'{connection.peer_address} as {connection.role_name}, '
|
||||||
|
f'{connection_phy}'
|
||||||
)
|
)
|
||||||
connection.phy = connection_phy
|
connection.phy = connection_phy
|
||||||
connection.emit('connection_phy_update')
|
connection.emit('connection_phy_update')
|
||||||
@@ -2625,7 +2804,9 @@ class Device(CompositeEventEmitter):
|
|||||||
@with_connection_from_handle
|
@with_connection_from_handle
|
||||||
def on_connection_phy_update_failure(self, connection, error):
|
def on_connection_phy_update_failure(self, connection, error):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'*** Connection PHY Update Failed: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}, error={error}'
|
f'*** Connection PHY Update Failed: [0x{connection.handle:04X}] '
|
||||||
|
f'{connection.peer_address} as {connection.role_name}, '
|
||||||
|
f'error={error}'
|
||||||
)
|
)
|
||||||
connection.emit('connection_phy_update_failure', error)
|
connection.emit('connection_phy_update_failure', error)
|
||||||
|
|
||||||
@@ -2633,7 +2814,9 @@ class Device(CompositeEventEmitter):
|
|||||||
@with_connection_from_handle
|
@with_connection_from_handle
|
||||||
def on_connection_att_mtu_update(self, connection, att_mtu):
|
def on_connection_att_mtu_update(self, connection, att_mtu):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'*** Connection ATT MTU Update: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}, {att_mtu}'
|
f'*** Connection ATT MTU Update: [0x{connection.handle:04X}] '
|
||||||
|
f'{connection.peer_address} as {connection.role_name}, '
|
||||||
|
f'{att_mtu}'
|
||||||
)
|
)
|
||||||
connection.att_mtu = att_mtu
|
connection.att_mtu = att_mtu
|
||||||
connection.emit('connection_att_mtu_update')
|
connection.emit('connection_att_mtu_update')
|
||||||
@@ -2644,7 +2827,8 @@ class Device(CompositeEventEmitter):
|
|||||||
self, connection, max_tx_octets, max_tx_time, max_rx_octets, max_rx_time
|
self, connection, max_tx_octets, max_tx_time, max_rx_octets, max_rx_time
|
||||||
):
|
):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'*** Connection Data Length Change: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}'
|
f'*** Connection Data Length Change: [0x{connection.handle:04X}] '
|
||||||
|
f'{connection.peer_address} as {connection.role_name}'
|
||||||
)
|
)
|
||||||
connection.data_length = (
|
connection.data_length = (
|
||||||
max_tx_octets,
|
max_tx_octets,
|
||||||
@@ -2677,14 +2861,14 @@ class Device(CompositeEventEmitter):
|
|||||||
# odd-numbered ones are server->client
|
# odd-numbered ones are server->client
|
||||||
if att_pdu.op_code & 1:
|
if att_pdu.op_code & 1:
|
||||||
if connection.gatt_client is None:
|
if connection.gatt_client is None:
|
||||||
logger.warn(
|
logger.warning(
|
||||||
color('no GATT client for connection 0x{connection_handle:04X}')
|
color('no GATT client for connection 0x{connection_handle:04X}')
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
connection.gatt_client.on_gatt_pdu(att_pdu)
|
connection.gatt_client.on_gatt_pdu(att_pdu)
|
||||||
else:
|
else:
|
||||||
if connection.gatt_server is None:
|
if connection.gatt_server is None:
|
||||||
logger.warn(
|
logger.warning(
|
||||||
color('no GATT server for connection 0x{connection_handle:04X}')
|
color('no GATT server for connection 0x{connection_handle:04X}')
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
@@ -2700,4 +2884,8 @@ class Device(CompositeEventEmitter):
|
|||||||
self.l2cap_channel_manager.on_pdu(connection, cid, pdu)
|
self.l2cap_channel_manager.on_pdu(connection, cid, pdu)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'Device(name="{self.name}", random_address="{self.random_address}"", public_address="{self.public_address}")'
|
return (
|
||||||
|
f'Device(name="{self.name}", '
|
||||||
|
f'random_address="{self.random_address}", '
|
||||||
|
f'public_address="{self.public_address}")'
|
||||||
|
)
|
||||||
|
|||||||
+51
-28
@@ -25,14 +25,15 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import asyncio
|
import asyncio
|
||||||
import enum
|
import enum
|
||||||
import types
|
import functools
|
||||||
import logging
|
import logging
|
||||||
from pyee import EventEmitter
|
import struct
|
||||||
|
from typing import Sequence
|
||||||
from colors import color
|
from colors import color
|
||||||
|
|
||||||
from .core import *
|
from .core import UUID, get_dict_key_by_value
|
||||||
from .hci import *
|
from .att import Attribute
|
||||||
from .att import *
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Logging
|
# Logging
|
||||||
@@ -43,6 +44,7 @@ logger = logging.getLogger(__name__)
|
|||||||
# Constants
|
# Constants
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# fmt: off
|
# fmt: off
|
||||||
|
# pylint: disable=line-too-long
|
||||||
|
|
||||||
GATT_REQUEST_TIMEOUT = 30 # seconds
|
GATT_REQUEST_TIMEOUT = 30 # seconds
|
||||||
|
|
||||||
@@ -177,6 +179,7 @@ GATT_BOOT_KEYBOARD_OUTPUT_REPORT_CHARACTERISTIC = UUID.from_16_bi
|
|||||||
GATT_CENTRAL_ADDRESS_RESOLUTION__CHARACTERISTIC = UUID.from_16_bits(0x2AA6, 'Central Address Resolution')
|
GATT_CENTRAL_ADDRESS_RESOLUTION__CHARACTERISTIC = UUID.from_16_bits(0x2AA6, 'Central Address Resolution')
|
||||||
|
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
# pylint: enable=line-too-long
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -203,7 +206,7 @@ class Service(Attribute):
|
|||||||
|
|
||||||
def __init__(self, uuid, characteristics: list[Characteristic], primary=True):
|
def __init__(self, uuid, characteristics: list[Characteristic], primary=True):
|
||||||
# Convert the uuid to a UUID object if it isn't already
|
# Convert the uuid to a UUID object if it isn't already
|
||||||
if type(uuid) is str:
|
if isinstance(uuid, str):
|
||||||
uuid = UUID(uuid)
|
uuid = UUID(uuid)
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
@@ -227,7 +230,12 @@ class Service(Attribute):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'Service(handle=0x{self.handle:04X}, end=0x{self.end_group_handle:04X}, uuid={self.uuid}){"" if self.primary else "*"}'
|
return (
|
||||||
|
f'Service(handle=0x{self.handle:04X}, '
|
||||||
|
f'end=0x{self.end_group_handle:04X}, '
|
||||||
|
f'uuid={self.uuid})'
|
||||||
|
f'{"" if self.primary else "*"}'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -271,15 +279,15 @@ class Characteristic(Attribute):
|
|||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def property_name(property):
|
def property_name(property_int):
|
||||||
return Characteristic.PROPERTY_NAMES.get(property, '')
|
return Characteristic.PROPERTY_NAMES.get(property_int, '')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def properties_as_string(properties):
|
def properties_as_string(properties):
|
||||||
return ','.join(
|
return ','.join(
|
||||||
[
|
[
|
||||||
Characteristic.property_name(p)
|
Characteristic.property_name(p)
|
||||||
for p in Characteristic.PROPERTY_NAMES.keys()
|
for p in Characteristic.PROPERTY_NAMES
|
||||||
if properties & p
|
if properties & p
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@@ -298,11 +306,11 @@ class Characteristic(Attribute):
|
|||||||
properties,
|
properties,
|
||||||
permissions,
|
permissions,
|
||||||
value=b'',
|
value=b'',
|
||||||
descriptors: list[Descriptor] = [],
|
descriptors: Sequence[Descriptor] = (),
|
||||||
):
|
):
|
||||||
super().__init__(uuid, permissions, value)
|
super().__init__(uuid, permissions, value)
|
||||||
self.uuid = self.type
|
self.uuid = self.type
|
||||||
if type(properties) is str:
|
if isinstance(properties, str):
|
||||||
self.properties = Characteristic.string_to_properties(properties)
|
self.properties = Characteristic.string_to_properties(properties)
|
||||||
else:
|
else:
|
||||||
self.properties = properties
|
self.properties = properties
|
||||||
@@ -313,8 +321,15 @@ class Characteristic(Attribute):
|
|||||||
if descriptor.type == descriptor_type:
|
if descriptor.type == descriptor_type:
|
||||||
return descriptor
|
return descriptor
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'Characteristic(handle=0x{self.handle:04X}, end=0x{self.end_group_handle:04X}, uuid={self.uuid}, properties={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'properties={Characteristic.properties_as_string(self.properties)})'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -335,7 +350,12 @@ class CharacteristicDeclaration(Attribute):
|
|||||||
self.characteristic = characteristic
|
self.characteristic = characteristic
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'CharacteristicDeclaration(handle=0x{self.handle:04X}, value_handle=0x{self.value_handle:04X}, uuid={self.characteristic.uuid}, properties={Characteristic.properties_as_string(self.characteristic.properties)})'
|
return (
|
||||||
|
f'CharacteristicDeclaration(handle=0x{self.handle:04X}, '
|
||||||
|
f'value_handle=0x{self.value_handle:04X}, '
|
||||||
|
f'uuid={self.characteristic.uuid}, properties='
|
||||||
|
f'{Characteristic.properties_as_string(self.characteristic.properties)})'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -395,14 +415,14 @@ class CharacteristicAdapter:
|
|||||||
return getattr(self.wrapped_characteristic, name)
|
return getattr(self.wrapped_characteristic, name)
|
||||||
|
|
||||||
def __setattr__(self, name, value):
|
def __setattr__(self, name, value):
|
||||||
if name in {
|
if name in (
|
||||||
'wrapped_characteristic',
|
'wrapped_characteristic',
|
||||||
'subscribers',
|
'subscribers',
|
||||||
'read_value',
|
'read_value',
|
||||||
'write_value',
|
'write_value',
|
||||||
'subscribe',
|
'subscribe',
|
||||||
'unsubscribe',
|
'unsubscribe',
|
||||||
}:
|
):
|
||||||
super().__setattr__(name, value)
|
super().__setattr__(name, value)
|
||||||
else:
|
else:
|
||||||
setattr(self.wrapped_characteristic, name, value)
|
setattr(self.wrapped_characteristic, name, value)
|
||||||
@@ -486,9 +506,9 @@ class PackedCharacteristicAdapter(CharacteristicAdapter):
|
|||||||
the format.
|
the format.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def __init__(self, characteristic, format):
|
def __init__(self, characteristic, pack_format):
|
||||||
super().__init__(characteristic)
|
super().__init__(characteristic)
|
||||||
self.struct = struct.Struct(format)
|
self.struct = struct.Struct(pack_format)
|
||||||
|
|
||||||
def pack(self, *values):
|
def pack(self, *values):
|
||||||
return self.struct.pack(*values)
|
return self.struct.pack(*values)
|
||||||
@@ -497,7 +517,7 @@ class PackedCharacteristicAdapter(CharacteristicAdapter):
|
|||||||
return self.struct.unpack(buffer)
|
return self.struct.unpack(buffer)
|
||||||
|
|
||||||
def encode_value(self, value):
|
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):
|
def decode_value(self, value):
|
||||||
unpacked = self.unpack(value)
|
unpacked = self.unpack(value)
|
||||||
@@ -510,14 +530,15 @@ class MappedCharacteristicAdapter(PackedCharacteristicAdapter):
|
|||||||
Adapter that packs/unpacks characteristic values according to a standard
|
Adapter that packs/unpacks characteristic values according to a standard
|
||||||
Python `struct` format.
|
Python `struct` format.
|
||||||
The adapted `read_value` and `write_value` methods return/accept aa dictionary which
|
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
|
is packed/unpacked according to format, with the arguments extracted from the
|
||||||
by key, in the same order as they occur in the `keys` parameter.
|
dictionary by key, in the same order as they occur in the `keys` parameter.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def __init__(self, characteristic, format, keys):
|
def __init__(self, characteristic, pack_format, keys):
|
||||||
super().__init__(characteristic, format)
|
super().__init__(characteristic, pack_format)
|
||||||
self.keys = keys
|
self.keys = keys
|
||||||
|
|
||||||
|
# pylint: disable=arguments-differ
|
||||||
def pack(self, values):
|
def pack(self, values):
|
||||||
return super().pack(*(values[key] for key in self.keys))
|
return super().pack(*(values[key] for key in self.keys))
|
||||||
|
|
||||||
@@ -544,16 +565,18 @@ class Descriptor(Attribute):
|
|||||||
See Vol 3, Part G - 3.3.3 Characteristic Descriptor Declarations
|
See Vol 3, Part G - 3.3.3 Characteristic Descriptor Declarations
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def __init__(self, descriptor_type, permissions, value=b''):
|
|
||||||
super().__init__(descriptor_type, permissions, value)
|
|
||||||
|
|
||||||
def __str__(self):
|
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):
|
class ClientCharacteristicConfigurationBits(enum.IntFlag):
|
||||||
'''
|
'''
|
||||||
See Vol 3, Part G - 3.3.3.3 - Table 3.11 Client Characteristic Configuration bit field definition
|
See Vol 3, Part G - 3.3.3.3 - Table 3.11 Client Characteristic Configuration bit
|
||||||
|
field definition
|
||||||
'''
|
'''
|
||||||
|
|
||||||
DEFAULT = 0x0000
|
DEFAULT = 0x0000
|
||||||
|
|||||||
+81
-35
@@ -28,9 +28,31 @@ import logging
|
|||||||
import struct
|
import struct
|
||||||
|
|
||||||
from colors import color
|
from colors import color
|
||||||
|
from pyee import EventEmitter
|
||||||
|
|
||||||
from .att import *
|
from .hci import HCI_Constant
|
||||||
from .core import InvalidStateError, ProtocolError, TimeoutError
|
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,
|
||||||
|
)
|
||||||
|
from . import core
|
||||||
|
from .core import UUID, InvalidStateError, ProtocolError
|
||||||
from .gatt import (
|
from .gatt import (
|
||||||
GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
|
GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
|
||||||
GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
|
GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
|
||||||
@@ -40,7 +62,6 @@ from .gatt import (
|
|||||||
Characteristic,
|
Characteristic,
|
||||||
ClientCharacteristicConfigurationBits,
|
ClientCharacteristicConfigurationBits,
|
||||||
)
|
)
|
||||||
from .hci import *
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Logging
|
# Logging
|
||||||
@@ -76,16 +97,17 @@ class AttributeProxy(EventEmitter):
|
|||||||
return value_bytes
|
return value_bytes
|
||||||
|
|
||||||
def __str__(self):
|
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):
|
class ServiceProxy(AttributeProxy):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_client(cls, client, service_uuid):
|
def from_client(service_class, client, service_uuid):
|
||||||
# The service and its characteristics are considered to have already been discovered
|
# The service and its characteristics are considered to have already been
|
||||||
|
# discovered
|
||||||
services = client.get_services_by_uuid(service_uuid)
|
services = client.get_services_by_uuid(service_uuid)
|
||||||
service = services[0] if services else None
|
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):
|
def __init__(self, client, handle, end_group_handle, uuid, primary=True):
|
||||||
attribute_type = (
|
attribute_type = (
|
||||||
@@ -97,7 +119,7 @@ class ServiceProxy(AttributeProxy):
|
|||||||
self.uuid = uuid
|
self.uuid = uuid
|
||||||
self.characteristics = []
|
self.characteristics = []
|
||||||
|
|
||||||
async def discover_characteristics(self, uuids=[]):
|
async def discover_characteristics(self, uuids=()):
|
||||||
return await self.client.discover_characteristics(uuids, self)
|
return await self.client.discover_characteristics(uuids, self)
|
||||||
|
|
||||||
def get_characteristics_by_uuid(self, uuid):
|
def get_characteristics_by_uuid(self, uuid):
|
||||||
@@ -121,6 +143,8 @@ class CharacteristicProxy(AttributeProxy):
|
|||||||
if descriptor.type == descriptor_type:
|
if descriptor.type == descriptor_type:
|
||||||
return descriptor
|
return descriptor
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
async def discover_descriptors(self):
|
async def discover_descriptors(self):
|
||||||
return await self.client.discover_descriptors(self)
|
return await self.client.discover_descriptors(self)
|
||||||
|
|
||||||
@@ -148,7 +172,11 @@ class CharacteristicProxy(AttributeProxy):
|
|||||||
return await self.client.unsubscribe(self, subscriber)
|
return await self.client.unsubscribe(self, subscriber)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'Characteristic(handle=0x{self.handle:04X}, uuid={self.uuid}, properties={Characteristic.properties_as_string(self.properties)})'
|
return (
|
||||||
|
f'Characteristic(handle=0x{self.handle:04X}, '
|
||||||
|
f'uuid={self.uuid}, '
|
||||||
|
f'properties={Characteristic.properties_as_string(self.properties)})'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DescriptorProxy(AttributeProxy):
|
class DescriptorProxy(AttributeProxy):
|
||||||
@@ -214,9 +242,9 @@ class Client:
|
|||||||
response = await asyncio.wait_for(
|
response = await asyncio.wait_for(
|
||||||
self.pending_response, GATT_REQUEST_TIMEOUT
|
self.pending_response, GATT_REQUEST_TIMEOUT
|
||||||
)
|
)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError as error:
|
||||||
logger.warning(color('!!! GATT Request timeout', 'red'))
|
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:
|
finally:
|
||||||
self.pending_request = None
|
self.pending_request = None
|
||||||
self.pending_response = None
|
self.pending_response = None
|
||||||
@@ -225,7 +253,8 @@ class Client:
|
|||||||
|
|
||||||
def send_confirmation(self, confirmation):
|
def send_confirmation(self, confirmation):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'GATT Confirmation from client: [0x{self.connection.handle:04X}] {confirmation}'
|
f'GATT Confirmation from client: [0x{self.connection.handle:04X}] '
|
||||||
|
f'{confirmation}'
|
||||||
)
|
)
|
||||||
self.send_gatt_pdu(confirmation.to_bytes())
|
self.send_gatt_pdu(confirmation.to_bytes())
|
||||||
|
|
||||||
@@ -300,7 +329,8 @@ class Client:
|
|||||||
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||||
# Unexpected end
|
# Unexpected end
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f'!!! unexpected error while discovering services: {HCI_Constant.error_name(response.error_code)}'
|
'!!! unexpected error while discovering services: '
|
||||||
|
f'{HCI_Constant.error_name(response.error_code)}'
|
||||||
)
|
)
|
||||||
# TODO raise appropriate exception
|
# TODO raise appropriate exception
|
||||||
return
|
return
|
||||||
@@ -352,7 +382,7 @@ class Client:
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
# Force uuid to be a UUID object
|
# Force uuid to be a UUID object
|
||||||
if type(uuid) is str:
|
if isinstance(uuid, str):
|
||||||
uuid = UUID(uuid)
|
uuid = UUID(uuid)
|
||||||
|
|
||||||
starting_handle = 0x0001
|
starting_handle = 0x0001
|
||||||
@@ -375,7 +405,8 @@ class Client:
|
|||||||
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||||
# Unexpected end
|
# Unexpected end
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f'!!! unexpected error while discovering services: {HCI_Constant.error_name(response.error_code)}'
|
'!!! unexpected error while discovering services: '
|
||||||
|
f'{HCI_Constant.error_name(response.error_code)}'
|
||||||
)
|
)
|
||||||
# TODO raise appropriate exception
|
# TODO raise appropriate exception
|
||||||
return
|
return
|
||||||
@@ -414,7 +445,7 @@ class Client:
|
|||||||
|
|
||||||
return services
|
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
|
See Vol 3, Part G - 4.5.1 Find Included Services
|
||||||
'''
|
'''
|
||||||
@@ -423,11 +454,12 @@ class Client:
|
|||||||
|
|
||||||
async def discover_characteristics(self, uuids, service):
|
async def discover_characteristics(self, uuids, service):
|
||||||
'''
|
'''
|
||||||
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
|
# 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
|
# Decide which services to discover for
|
||||||
services = [service] if service else self.services
|
services = [service] if service else self.services
|
||||||
@@ -456,7 +488,8 @@ class Client:
|
|||||||
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||||
# Unexpected end
|
# Unexpected end
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f'!!! unexpected error while discovering characteristics: {HCI_Constant.error_name(response.error_code)}'
|
'!!! unexpected error while discovering characteristics: '
|
||||||
|
f'{HCI_Constant.error_name(response.error_code)}'
|
||||||
)
|
)
|
||||||
# TODO raise appropriate exception
|
# TODO raise appropriate exception
|
||||||
return
|
return
|
||||||
@@ -532,7 +565,8 @@ class Client:
|
|||||||
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||||
# Unexpected end
|
# Unexpected end
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f'!!! unexpected error while discovering descriptors: {HCI_Constant.error_name(response.error_code)}'
|
'!!! unexpected error while discovering descriptors: '
|
||||||
|
f'{HCI_Constant.error_name(response.error_code)}'
|
||||||
)
|
)
|
||||||
# TODO raise appropriate exception
|
# TODO raise appropriate exception
|
||||||
return []
|
return []
|
||||||
@@ -585,7 +619,8 @@ class Client:
|
|||||||
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||||
# Unexpected end
|
# Unexpected end
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f'!!! unexpected error while discovering attributes: {HCI_Constant.error_name(response.error_code)}'
|
'!!! unexpected error while discovering attributes: '
|
||||||
|
f'{HCI_Constant.error_name(response.error_code)}'
|
||||||
)
|
)
|
||||||
return []
|
return []
|
||||||
break
|
break
|
||||||
@@ -607,7 +642,8 @@ class Client:
|
|||||||
return attributes
|
return attributes
|
||||||
|
|
||||||
async def subscribe(self, characteristic, subscriber=None, prefer_notify=True):
|
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 we haven't already discovered the descriptors for this characteristic,
|
||||||
|
# do it now
|
||||||
if not characteristic.descriptors_discovered:
|
if not characteristic.descriptors_discovered:
|
||||||
await self.discover_descriptors(characteristic)
|
await self.discover_descriptors(characteristic)
|
||||||
|
|
||||||
@@ -642,14 +678,16 @@ class Client:
|
|||||||
subscriber_set = subscribers.setdefault(characteristic.handle, set())
|
subscriber_set = subscribers.setdefault(characteristic.handle, set())
|
||||||
if subscriber is not None:
|
if subscriber is not None:
|
||||||
subscriber_set.add(subscriber)
|
subscriber_set.add(subscriber)
|
||||||
# Add the characteristic as a subscriber, which will result in the characteristic
|
# Add the characteristic as a subscriber, which will result in the
|
||||||
# emitting an 'update' event when a notification or indication is received
|
# characteristic emitting an 'update' event when a notification or indication
|
||||||
|
# is received
|
||||||
subscriber_set.add(characteristic)
|
subscriber_set.add(characteristic)
|
||||||
|
|
||||||
await self.write_value(cccd, struct.pack('<H', bits), with_response=True)
|
await self.write_value(cccd, struct.pack('<H', bits), with_response=True)
|
||||||
|
|
||||||
async def unsubscribe(self, characteristic, subscriber=None):
|
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:
|
if not characteristic.descriptors_discovered:
|
||||||
await self.discover_descriptors(characteristic)
|
await self.discover_descriptors(characteristic)
|
||||||
|
|
||||||
@@ -673,7 +711,7 @@ class Client:
|
|||||||
|
|
||||||
# Cleanup if we removed the last one
|
# Cleanup if we removed the last one
|
||||||
if not subscribers:
|
if not subscribers:
|
||||||
subscriber_set.remove(characteristic.handle)
|
del subscriber_set[characteristic.handle]
|
||||||
else:
|
else:
|
||||||
# Remove all subscribers for this attribute from the sets!
|
# Remove all subscribers for this attribute from the sets!
|
||||||
self.notification_subscribers.pop(characteristic.handle, None)
|
self.notification_subscribers.pop(characteristic.handle, None)
|
||||||
@@ -691,7 +729,7 @@ class Client:
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
# Send a request to read
|
# Send a request to read
|
||||||
attribute_handle = attribute if type(attribute) is int else attribute.handle
|
attribute_handle = attribute if isinstance(attribute, int) else attribute.handle
|
||||||
response = await self.send_request(
|
response = await self.send_request(
|
||||||
ATT_Read_Request(attribute_handle=attribute_handle)
|
ATT_Read_Request(attribute_handle=attribute_handle)
|
||||||
)
|
)
|
||||||
@@ -720,9 +758,9 @@ class Client:
|
|||||||
if response is None:
|
if response is None:
|
||||||
raise TimeoutError('read timeout')
|
raise TimeoutError('read timeout')
|
||||||
if response.op_code == ATT_ERROR_RESPONSE:
|
if response.op_code == ATT_ERROR_RESPONSE:
|
||||||
if (
|
if response.error_code in (
|
||||||
response.error_code == ATT_ATTRIBUTE_NOT_LONG_ERROR
|
ATT_ATTRIBUTE_NOT_LONG_ERROR,
|
||||||
or response.error_code == ATT_INVALID_OFFSET_ERROR
|
ATT_INVALID_OFFSET_ERROR,
|
||||||
):
|
):
|
||||||
break
|
break
|
||||||
raise ProtocolError(
|
raise ProtocolError(
|
||||||
@@ -773,7 +811,8 @@ class Client:
|
|||||||
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||||
# Unexpected end
|
# Unexpected end
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f'!!! unexpected error while reading characteristics: {HCI_Constant.error_name(response.error_code)}'
|
'!!! unexpected error while reading characteristics: '
|
||||||
|
f'{HCI_Constant.error_name(response.error_code)}'
|
||||||
)
|
)
|
||||||
# TODO raise appropriate exception
|
# TODO raise appropriate exception
|
||||||
return []
|
return []
|
||||||
@@ -799,13 +838,14 @@ class Client:
|
|||||||
|
|
||||||
async def write_value(self, attribute, value, with_response=False):
|
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
|
`attribute` can be an Attribute object, or a handle value
|
||||||
'''
|
'''
|
||||||
|
|
||||||
# Send a request or command to write
|
# 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:
|
if with_response:
|
||||||
response = await self.send_request(
|
response = await self.send_request(
|
||||||
ATT_Write_Request(
|
ATT_Write_Request(
|
||||||
@@ -836,7 +876,8 @@ class Client:
|
|||||||
logger.warning('!!! unexpected response, there is no pending request')
|
logger.warning('!!! unexpected response, there is no pending request')
|
||||||
return
|
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:
|
if att_pdu.op_code != ATT_ERROR_RESPONSE:
|
||||||
expected_response_name = self.pending_request.name.replace(
|
expected_response_name = self.pending_request.name.replace(
|
||||||
'_REQUEST', '_RESPONSE'
|
'_REQUEST', '_RESPONSE'
|
||||||
@@ -856,7 +897,12 @@ class Client:
|
|||||||
handler(att_pdu)
|
handler(att_pdu)
|
||||||
else:
|
else:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f'{color(f"--- Ignoring GATT Response from [0x{self.connection.handle:04X}]:", "red")} {att_pdu}'
|
color(
|
||||||
|
'--- Ignoring GATT Response from '
|
||||||
|
f'[0x{self.connection.handle:04X}]: ',
|
||||||
|
'red',
|
||||||
|
)
|
||||||
|
+ str(att_pdu)
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_att_handle_value_notification(self, notification):
|
def on_att_handle_value_notification(self, notification):
|
||||||
|
|||||||
+63
-15
@@ -26,14 +26,53 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
import struct
|
||||||
from typing import Tuple, Optional
|
from typing import Tuple, Optional
|
||||||
from pyee import EventEmitter
|
from pyee import EventEmitter
|
||||||
from colors import color
|
from colors import color
|
||||||
|
|
||||||
from .core import *
|
from .core import UUID
|
||||||
from .hci import *
|
from .att import (
|
||||||
from .att import *
|
ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
||||||
from .gatt import *
|
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_INCLUDE_ATTRIBUTE_TYPE,
|
||||||
|
GATT_MAX_ATTRIBUTE_VALUE_SIZE,
|
||||||
|
GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
|
||||||
|
GATT_REQUEST_TIMEOUT,
|
||||||
|
GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE,
|
||||||
|
Characteristic,
|
||||||
|
CharacteristicDeclaration,
|
||||||
|
CharacteristicValue,
|
||||||
|
Descriptor,
|
||||||
|
Service,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Logging
|
# Logging
|
||||||
@@ -194,6 +233,7 @@ class Server(EventEmitter):
|
|||||||
is None
|
is None
|
||||||
):
|
):
|
||||||
self.add_attribute(
|
self.add_attribute(
|
||||||
|
# pylint: disable=line-too-long
|
||||||
Descriptor(
|
Descriptor(
|
||||||
GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
|
GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
|
||||||
Attribute.READABLE | Attribute.WRITEABLE,
|
Attribute.READABLE | Attribute.WRITEABLE,
|
||||||
@@ -232,12 +272,13 @@ class Server(EventEmitter):
|
|||||||
|
|
||||||
def write_cccd(self, connection, characteristic, value):
|
def write_cccd(self, connection, characteristic, value):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'Subscription update for connection=0x{connection.handle:04X}, handle=0x{characteristic.handle:04X}: {value.hex()}'
|
f'Subscription update for connection=0x{connection.handle:04X}, '
|
||||||
|
f'handle=0x{characteristic.handle:04X}: {value.hex()}'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Sanity check
|
# Sanity check
|
||||||
if len(value) != 2:
|
if len(value) != 2:
|
||||||
logger.warn('CCCD value not 2 bytes long')
|
logger.warning('CCCD value not 2 bytes long')
|
||||||
return
|
return
|
||||||
|
|
||||||
cccds = self.subscribers.setdefault(connection.handle, {})
|
cccds = self.subscribers.setdefault(connection.handle, {})
|
||||||
@@ -349,9 +390,9 @@ class Server(EventEmitter):
|
|||||||
await asyncio.wait_for(
|
await asyncio.wait_for(
|
||||||
self.pending_confirmations[connection.handle], GATT_REQUEST_TIMEOUT
|
self.pending_confirmations[connection.handle], GATT_REQUEST_TIMEOUT
|
||||||
)
|
)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError as error:
|
||||||
logger.warning(color('!!! GATT Indicate timeout', 'red'))
|
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:
|
finally:
|
||||||
self.pending_confirmations[connection.handle] = None
|
self.pending_confirmations[connection.handle] = None
|
||||||
|
|
||||||
@@ -425,7 +466,11 @@ class Server(EventEmitter):
|
|||||||
else:
|
else:
|
||||||
# Just ignore
|
# Just ignore
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f'{color("--- Ignoring GATT Request from [0x{connection.handle:04X}]:", "red")} {att_pdu}'
|
color(
|
||||||
|
f'--- Ignoring GATT Request from [0x{connection.handle:04X}]: ',
|
||||||
|
'red',
|
||||||
|
)
|
||||||
|
+ str(att_pdu)
|
||||||
)
|
)
|
||||||
|
|
||||||
#######################################################
|
#######################################################
|
||||||
@@ -436,7 +481,10 @@ class Server(EventEmitter):
|
|||||||
Handler for requests without a more specific handler
|
Handler for requests without a more specific handler
|
||||||
'''
|
'''
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f'{color(f"--- Unsupported ATT Request from [0x{connection.handle:04X}]:", "red")} {pdu}'
|
color(
|
||||||
|
f'--- Unsupported ATT Request from [0x{connection.handle:04X}]: ', 'red'
|
||||||
|
)
|
||||||
|
+ str(pdu)
|
||||||
)
|
)
|
||||||
response = ATT_Error_Response(
|
response = ATT_Error_Response(
|
||||||
request_opcode_in_error=pdu.op_code,
|
request_opcode_in_error=pdu.op_code,
|
||||||
@@ -556,11 +604,11 @@ class Server(EventEmitter):
|
|||||||
if attributes:
|
if attributes:
|
||||||
handles_information_list = []
|
handles_information_list = []
|
||||||
for attribute in attributes:
|
for attribute in attributes:
|
||||||
if attribute.type in {
|
if attribute.type in (
|
||||||
GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
|
GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
|
||||||
GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE,
|
GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE,
|
||||||
GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
|
GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
|
||||||
}:
|
):
|
||||||
# Part of a group
|
# Part of a group
|
||||||
group_end_handle = attribute.end_group_handle
|
group_end_handle = attribute.end_group_handle
|
||||||
else:
|
else:
|
||||||
@@ -692,11 +740,11 @@ class Server(EventEmitter):
|
|||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 3, Part F - 3.4.4.9 Read by Group Type Request
|
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_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
|
||||||
GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE,
|
GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE,
|
||||||
GATT_INCLUDE_ATTRIBUTE_TYPE,
|
GATT_INCLUDE_ATTRIBUTE_TYPE,
|
||||||
}:
|
):
|
||||||
response = ATT_Error_Response(
|
response = ATT_Error_Response(
|
||||||
request_opcode_in_error=request.op_code,
|
request_opcode_in_error=request.op_code,
|
||||||
attribute_handle_in_error=request.starting_handle,
|
attribute_handle_in_error=request.starting_handle,
|
||||||
@@ -814,7 +862,7 @@ class Server(EventEmitter):
|
|||||||
except Exception as error:
|
except Exception as error:
|
||||||
logger.warning(f'!!! ignoring exception: {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
|
See Bluetooth spec Vol 3, Part F - 3.4.7.3 Handle Value Confirmation
|
||||||
'''
|
'''
|
||||||
|
|||||||
+165
-106
@@ -21,7 +21,16 @@ import logging
|
|||||||
import functools
|
import functools
|
||||||
from colors import color
|
from colors import color
|
||||||
|
|
||||||
from .core import *
|
from .core import (
|
||||||
|
BT_BR_EDR_TRANSPORT,
|
||||||
|
AdvertisingData,
|
||||||
|
DeviceClass,
|
||||||
|
ProtocolError,
|
||||||
|
bit_flags_to_strings,
|
||||||
|
name_or_number,
|
||||||
|
padded_bytes,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Logging
|
# Logging
|
||||||
@@ -43,8 +52,8 @@ def key_with_value(dictionary, target_value):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def indent_lines(str):
|
def indent_lines(string):
|
||||||
return '\n'.join([' ' + line for line in str.split('\n')])
|
return '\n'.join([' ' + line for line in string.split('\n')])
|
||||||
|
|
||||||
|
|
||||||
def map_null_terminated_utf8_string(utf8_bytes):
|
def map_null_terminated_utf8_string(utf8_bytes):
|
||||||
@@ -63,25 +72,32 @@ def map_class_of_device(class_of_device):
|
|||||||
major_device_class,
|
major_device_class,
|
||||||
minor_device_class,
|
minor_device_class,
|
||||||
) = DeviceClass.split_class_of_device(class_of_device)
|
) = DeviceClass.split_class_of_device(class_of_device)
|
||||||
return f'[{class_of_device:06X}] Services({",".join(DeviceClass.service_class_labels(service_classes))}),Class({DeviceClass.major_device_class_name(major_device_class)}|{DeviceClass.minor_device_class_name(major_device_class, minor_device_class)})'
|
return (
|
||||||
|
f'[{class_of_device:06X}] Services('
|
||||||
|
f'{",".join(DeviceClass.service_class_labels(service_classes))}),'
|
||||||
|
f'Class({DeviceClass.major_device_class_name(major_device_class)}|'
|
||||||
|
f'{DeviceClass.minor_device_class_name(major_device_class, minor_device_class)}'
|
||||||
|
')'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def phy_list_to_bits(phys):
|
def phy_list_to_bits(phys):
|
||||||
if phys is None:
|
if phys is None:
|
||||||
return 0
|
return 0
|
||||||
else:
|
|
||||||
phy_bits = 0
|
phy_bits = 0
|
||||||
for phy in phys:
|
for phy in phys:
|
||||||
if phy not in HCI_LE_PHY_TYPE_TO_BIT:
|
if phy not in HCI_LE_PHY_TYPE_TO_BIT:
|
||||||
raise ValueError('invalid PHY')
|
raise ValueError('invalid PHY')
|
||||||
phy_bits |= 1 << HCI_LE_PHY_TYPE_TO_BIT[phy]
|
phy_bits |= 1 << HCI_LE_PHY_TYPE_TO_BIT[phy]
|
||||||
return phy_bits
|
return phy_bits
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Constants
|
# Constants
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# fmt: off
|
# fmt: off
|
||||||
|
# pylint: disable=line-too-long
|
||||||
|
|
||||||
# HCI Version
|
# HCI Version
|
||||||
HCI_VERSION_BLUETOOTH_CORE_1_0B = 0
|
HCI_VERSION_BLUETOOTH_CORE_1_0B = 0
|
||||||
@@ -1355,8 +1371,11 @@ HCI_LE_SUPPORTED_FEATURES_NAMES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
# pylint: enable=line-too-long
|
||||||
|
# pylint: disable=invalid-name
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
# pylint: disable-next=unnecessary-lambda
|
||||||
STATUS_SPEC = {'size': 1, 'mapper': lambda x: HCI_Constant.status_name(x)}
|
STATUS_SPEC = {'size': 1, 'mapper': lambda x: HCI_Constant.status_name(x)}
|
||||||
|
|
||||||
|
|
||||||
@@ -1418,25 +1437,25 @@ class HCI_StatusError(ProtocolError):
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class HCI_Object:
|
class HCI_Object:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def init_from_fields(object, fields, values):
|
def init_from_fields(hci_object, fields, values):
|
||||||
if type(values) is dict:
|
if isinstance(values, dict):
|
||||||
for field_name, _ in fields:
|
for field_name, _ in fields:
|
||||||
setattr(object, field_name, values[field_name])
|
setattr(hci_object, field_name, values[field_name])
|
||||||
else:
|
else:
|
||||||
for field_name, field_value in zip(fields, values):
|
for field_name, field_value in zip(fields, values):
|
||||||
setattr(object, field_name, field_value)
|
setattr(hci_object, field_name, field_value)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def init_from_bytes(object, data, offset, fields):
|
def init_from_bytes(hci_object, data, offset, fields):
|
||||||
parsed = HCI_Object.dict_from_bytes(data, offset, fields)
|
parsed = HCI_Object.dict_from_bytes(data, offset, fields)
|
||||||
HCI_Object.init_from_fields(object, parsed.keys(), parsed.values())
|
HCI_Object.init_from_fields(hci_object, parsed.keys(), parsed.values())
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def dict_from_bytes(data, offset, fields):
|
def dict_from_bytes(data, offset, fields):
|
||||||
result = collections.OrderedDict()
|
result = collections.OrderedDict()
|
||||||
for (field_name, field_type) in fields:
|
for (field_name, field_type) in fields:
|
||||||
# The field_type may be a dictionary with a mapper, parser, and/or size
|
# The field_type may be a dictionary with a mapper, parser, and/or size
|
||||||
if type(field_type) is dict:
|
if isinstance(field_type, dict):
|
||||||
if 'size' in field_type:
|
if 'size' in field_type:
|
||||||
field_type = field_type['size']
|
field_type = field_type['size']
|
||||||
elif 'parser' in field_type:
|
elif 'parser' in field_type:
|
||||||
@@ -1480,7 +1499,7 @@ class HCI_Object:
|
|||||||
# 32-bit unsigned big-endian
|
# 32-bit unsigned big-endian
|
||||||
field_value = struct.unpack_from('>I', data, offset)[0]
|
field_value = struct.unpack_from('>I', data, offset)[0]
|
||||||
offset += 4
|
offset += 4
|
||||||
elif type(field_type) is int and field_type > 4 and field_type <= 256:
|
elif isinstance(field_type, int) and 4 < field_type <= 256:
|
||||||
# Byte array (from 5 up to 256 bytes)
|
# Byte array (from 5 up to 256 bytes)
|
||||||
field_value = data[offset : offset + field_type]
|
field_value = data[offset : offset + field_type]
|
||||||
offset += field_type
|
offset += field_type
|
||||||
@@ -1494,19 +1513,20 @@ class HCI_Object:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def dict_to_bytes(object, fields):
|
def dict_to_bytes(hci_object, fields):
|
||||||
result = bytearray()
|
result = bytearray()
|
||||||
for (field_name, field_type) in fields:
|
for (field_name, field_type) in fields:
|
||||||
# The field_type may be a dictionary with a mapper, parser, serializer, and/or size
|
# The field_type may be a dictionary with a mapper, parser, serializer,
|
||||||
|
# and/or size
|
||||||
serializer = None
|
serializer = None
|
||||||
if type(field_type) is dict:
|
if isinstance(field_type, dict):
|
||||||
if 'serializer' in field_type:
|
if 'serializer' in field_type:
|
||||||
serializer = field_type['serializer']
|
serializer = field_type['serializer']
|
||||||
if 'size' in field_type:
|
if 'size' in field_type:
|
||||||
field_type = field_type['size']
|
field_type = field_type['size']
|
||||||
|
|
||||||
# Serialize the field
|
# Serialize the field
|
||||||
field_value = object[field_name]
|
field_value = hci_object[field_name]
|
||||||
if serializer:
|
if serializer:
|
||||||
field_bytes = serializer(field_value)
|
field_bytes = serializer(field_value)
|
||||||
elif field_type == 1:
|
elif field_type == 1:
|
||||||
@@ -1534,20 +1554,18 @@ class HCI_Object:
|
|||||||
# 32-bit unsigned big-endian
|
# 32-bit unsigned big-endian
|
||||||
field_bytes = struct.pack('>I', field_value)
|
field_bytes = struct.pack('>I', field_value)
|
||||||
elif field_type == '*':
|
elif field_type == '*':
|
||||||
if type(field_value) is int:
|
if isinstance(field_value, int):
|
||||||
if field_value >= 0 and field_value <= 255:
|
if 0 <= field_value <= 255:
|
||||||
field_bytes = bytes([field_value])
|
field_bytes = bytes([field_value])
|
||||||
else:
|
else:
|
||||||
raise ValueError('value too large for *-typed field')
|
raise ValueError('value too large for *-typed field')
|
||||||
else:
|
else:
|
||||||
field_bytes = bytes(field_value)
|
field_bytes = bytes(field_value)
|
||||||
elif (
|
elif isinstance(field_value, (bytes, bytearray)) or hasattr(
|
||||||
type(field_value) is bytes
|
field_value, 'to_bytes'
|
||||||
or type(field_value) is bytearray
|
|
||||||
or hasattr(field_value, 'to_bytes')
|
|
||||||
):
|
):
|
||||||
field_bytes = bytes(field_value)
|
field_bytes = bytes(field_value)
|
||||||
if type(field_type) is int and field_type > 4 and field_type <= 256:
|
if isinstance(field_type, int) and 4 < field_type <= 256:
|
||||||
# Truncate or Pad with zeros if the field is too long or too short
|
# Truncate or Pad with zeros if the field is too long or too short
|
||||||
if len(field_bytes) < field_type:
|
if len(field_bytes) < field_type:
|
||||||
field_bytes += bytes(field_type - len(field_bytes))
|
field_bytes += bytes(field_type - len(field_bytes))
|
||||||
@@ -1584,42 +1602,44 @@ class HCI_Object:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def format_field_value(value, indentation):
|
def format_field_value(value, indentation):
|
||||||
if type(value) is bytes:
|
if isinstance(value, bytes):
|
||||||
return value.hex()
|
return value.hex()
|
||||||
elif isinstance(value, HCI_Object):
|
|
||||||
|
if isinstance(value, HCI_Object):
|
||||||
return '\n' + value.to_string(indentation)
|
return '\n' + value.to_string(indentation)
|
||||||
else:
|
|
||||||
return str(value)
|
return str(value)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def format_fields(object, keys, indentation='', value_mappers={}):
|
def format_fields(hci_object, keys, indentation='', value_mappers=None):
|
||||||
if not keys:
|
if not keys:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
# Measure the widest field name
|
# Measure the widest field name
|
||||||
max_field_name_length = max(
|
max_field_name_length = max(
|
||||||
[len(key[0] if type(key) is tuple else key) for key in keys]
|
(len(key[0] if isinstance(key, tuple) else key) for key in keys)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Build array of formatted key:value pairs
|
# Build array of formatted key:value pairs
|
||||||
fields = []
|
fields = []
|
||||||
for key in keys:
|
for key in keys:
|
||||||
value_mapper = None
|
value_mapper = None
|
||||||
if type(key) is tuple:
|
if isinstance(key, tuple):
|
||||||
# The key has an associated specifier
|
# The key has an associated specifier
|
||||||
key, specifier = key
|
key, specifier = key
|
||||||
|
|
||||||
# Get the value mapper from the specifier
|
# Get the value mapper from the specifier
|
||||||
if type(specifier) is dict:
|
if isinstance(specifier, dict):
|
||||||
value_mapper = specifier.get('mapper')
|
value_mapper = specifier.get('mapper')
|
||||||
|
|
||||||
# Get the value for the field
|
# Get the value for the field
|
||||||
value = object[key]
|
value = hci_object[key]
|
||||||
|
|
||||||
# Map the value if needed
|
# Map the value if needed
|
||||||
value_mapper = value_mappers.get(key, value_mapper)
|
if value_mappers:
|
||||||
if value_mapper is not None:
|
value_mapper = value_mappers.get(key, value_mapper)
|
||||||
value = value_mapper(value)
|
if value_mapper is not None:
|
||||||
|
value = value_mapper(value)
|
||||||
|
|
||||||
# Get the string representation of the value
|
# Get the string representation of the value
|
||||||
value_str = HCI_Object.format_field_value(
|
value_str = HCI_Object.format_field_value(
|
||||||
@@ -1639,7 +1659,7 @@ class HCI_Object:
|
|||||||
self.fields = fields
|
self.fields = fields
|
||||||
self.init_from_fields(self, fields, kwargs)
|
self.init_from_fields(self, fields, kwargs)
|
||||||
|
|
||||||
def to_string(self, indentation='', value_mappers={}):
|
def to_string(self, indentation='', value_mappers=None):
|
||||||
return HCI_Object.format_fields(
|
return HCI_Object.format_fields(
|
||||||
self.__dict__, self.fields, indentation, value_mappers
|
self.__dict__, self.fields, indentation, value_mappers
|
||||||
)
|
)
|
||||||
@@ -1670,6 +1690,7 @@ class Address:
|
|||||||
RANDOM_IDENTITY_ADDRESS: 'RANDOM_IDENTITY_ADDRESS',
|
RANDOM_IDENTITY_ADDRESS: 'RANDOM_IDENTITY_ADDRESS',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# pylint: disable-next=unnecessary-lambda
|
||||||
ADDRESS_TYPE_SPEC = {'size': 1, 'mapper': lambda x: Address.address_type_name(x)}
|
ADDRESS_TYPE_SPEC = {'size': 1, 'mapper': lambda x: Address.address_type_name(x)}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -1686,7 +1707,8 @@ class Address:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_address(data, offset):
|
def parse_address(data, offset):
|
||||||
# Fix the type to a default value. This is used for parsing type-less Classic addresses
|
# Fix the type to a default value. This is used for parsing type-less Classic
|
||||||
|
# addresses
|
||||||
return Address.parse_address_with_type(
|
return Address.parse_address_with_type(
|
||||||
data, offset, Address.PUBLIC_DEVICE_ADDRESS
|
data, offset, Address.PUBLIC_DEVICE_ADDRESS
|
||||||
)
|
)
|
||||||
@@ -1705,10 +1727,10 @@ class Address:
|
|||||||
Initialize an instance. `address` may be a byte array in little-endian
|
Initialize an instance. `address` may be a byte array in little-endian
|
||||||
format, or a hex string in big-endian format (with optional ':'
|
format, or a hex string in big-endian format (with optional ':'
|
||||||
separators between the bytes).
|
separators between the bytes).
|
||||||
If the address is a string suffixed with '/P', `address_type` is ignored and the type
|
If the address is a string suffixed with '/P', `address_type` is ignored and
|
||||||
is set to PUBLIC_DEVICE_ADDRESS.
|
the type is set to PUBLIC_DEVICE_ADDRESS.
|
||||||
'''
|
'''
|
||||||
if type(address) is bytes:
|
if isinstance(address, bytes):
|
||||||
self.address_bytes = address
|
self.address_bytes = address
|
||||||
else:
|
else:
|
||||||
# Check if there's a '/P' type specifier
|
# Check if there's a '/P' type specifier
|
||||||
@@ -1731,9 +1753,9 @@ class Address:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def is_public(self):
|
def is_public(self):
|
||||||
return (
|
return self.address_type in (
|
||||||
self.address_type == self.PUBLIC_DEVICE_ADDRESS
|
self.PUBLIC_DEVICE_ADDRESS,
|
||||||
or self.address_type == self.PUBLIC_IDENTITY_ADDRESS
|
self.PUBLIC_IDENTITY_ADDRESS,
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -1742,9 +1764,9 @@ class Address:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def is_resolved(self):
|
def is_resolved(self):
|
||||||
return (
|
return self.address_type in (
|
||||||
self.address_type == self.PUBLIC_IDENTITY_ADDRESS
|
self.PUBLIC_IDENTITY_ADDRESS,
|
||||||
or self.address_type == self.RANDOM_IDENTITY_ADDRESS
|
self.RANDOM_IDENTITY_ADDRESS,
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -1776,10 +1798,10 @@ class Address:
|
|||||||
'''
|
'''
|
||||||
String representation of the address, MSB first
|
String representation of the address, MSB first
|
||||||
'''
|
'''
|
||||||
str = ':'.join([f'{x:02X}' for x in reversed(self.address_bytes)])
|
result = ':'.join([f'{x:02X}' for x in reversed(self.address_bytes)])
|
||||||
if not self.is_public:
|
if not self.is_public:
|
||||||
return str
|
return result
|
||||||
return str + '/P'
|
return result + '/P'
|
||||||
|
|
||||||
|
|
||||||
# Predefined address values
|
# Predefined address values
|
||||||
@@ -1801,9 +1823,10 @@ class OwnAddressType:
|
|||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def type_name(type):
|
def type_name(type_id):
|
||||||
return name_or_number(OwnAddressType.TYPE_NAMES, type)
|
return name_or_number(OwnAddressType.TYPE_NAMES, type_id)
|
||||||
|
|
||||||
|
# pylint: disable-next=unnecessary-lambda
|
||||||
TYPE_SPEC = {'size': 1, 'mapper': lambda x: OwnAddressType.type_name(x)}
|
TYPE_SPEC = {'size': 1, 'mapper': lambda x: OwnAddressType.type_name(x)}
|
||||||
|
|
||||||
|
|
||||||
@@ -1816,14 +1839,17 @@ class HCI_Packet:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def from_bytes(packet):
|
def from_bytes(packet):
|
||||||
packet_type = packet[0]
|
packet_type = packet[0]
|
||||||
|
|
||||||
if packet_type == HCI_COMMAND_PACKET:
|
if packet_type == HCI_COMMAND_PACKET:
|
||||||
return HCI_Command.from_bytes(packet)
|
return HCI_Command.from_bytes(packet)
|
||||||
elif packet_type == HCI_ACL_DATA_PACKET:
|
|
||||||
|
if packet_type == HCI_ACL_DATA_PACKET:
|
||||||
return HCI_AclDataPacket.from_bytes(packet)
|
return HCI_AclDataPacket.from_bytes(packet)
|
||||||
elif packet_type == HCI_EVENT_PACKET:
|
|
||||||
|
if packet_type == HCI_EVENT_PACKET:
|
||||||
return HCI_Event.from_bytes(packet)
|
return HCI_Event.from_bytes(packet)
|
||||||
else:
|
|
||||||
return HCI_CustomPacket(packet)
|
return HCI_CustomPacket(packet)
|
||||||
|
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
self.name = name
|
self.name = name
|
||||||
@@ -1850,7 +1876,7 @@ class HCI_Command(HCI_Packet):
|
|||||||
command_classes = {}
|
command_classes = {}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def command(fields=[], return_parameters_fields=[]):
|
def command(fields=(), return_parameters_fields=()):
|
||||||
'''
|
'''
|
||||||
Decorator used to declare and register subclasses
|
Decorator used to declare and register subclasses
|
||||||
'''
|
'''
|
||||||
@@ -1897,8 +1923,8 @@ class HCI_Command(HCI_Packet):
|
|||||||
HCI_Command.__init__(self, op_code, parameters)
|
HCI_Command.__init__(self, op_code, parameters)
|
||||||
HCI_Object.init_from_bytes(self, parameters, 0, fields)
|
HCI_Object.init_from_bytes(self, parameters, 0, fields)
|
||||||
return self
|
return self
|
||||||
else:
|
|
||||||
return cls.from_parameters(parameters)
|
return cls.from_parameters(parameters)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def command_name(op_code):
|
def command_name(op_code):
|
||||||
@@ -2842,6 +2868,7 @@ class HCI_LE_Set_Random_Address_Command(HCI_Command):
|
|||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@HCI_Command.command(
|
@HCI_Command.command(
|
||||||
|
# pylint: disable=line-too-long,unnecessary-lambda
|
||||||
[
|
[
|
||||||
('advertising_interval_min', 2),
|
('advertising_interval_min', 2),
|
||||||
('advertising_interval_max', 2),
|
('advertising_interval_max', 2),
|
||||||
@@ -3089,7 +3116,8 @@ class HCI_LE_Read_Remote_Features_Command(HCI_Command):
|
|||||||
class HCI_LE_Enable_Encryption_Command(HCI_Command):
|
class HCI_LE_Enable_Encryption_Command(HCI_Command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec @ 7.8.24 LE Enable Encryption Command
|
See Bluetooth spec @ 7.8.24 LE Enable Encryption Command
|
||||||
(renamed from "LE Start Encryption Command" in version prior to 5.2 of the specification)
|
(renamed from "LE Start Encryption Command" in version prior to 5.2 of the
|
||||||
|
specification)
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
@@ -3144,7 +3172,8 @@ class HCI_LE_Remote_Connection_Parameter_Request_Reply_Command(HCI_Command):
|
|||||||
)
|
)
|
||||||
class HCI_LE_Remote_Connection_Parameter_Request_Negative_Reply_Command(HCI_Command):
|
class HCI_LE_Remote_Connection_Parameter_Request_Negative_Reply_Command(HCI_Command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec @ 7.8.32 LE Remote Connection Parameter Request Negative Reply Command
|
See Bluetooth spec @ 7.8.32 LE Remote Connection Parameter Request Negative Reply
|
||||||
|
Command
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
@@ -3356,6 +3385,7 @@ class HCI_LE_Set_Advertising_Set_Random_Address_Command(HCI_Command):
|
|||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@HCI_Command.command(
|
@HCI_Command.command(
|
||||||
|
# pylint: disable=line-too-long,unnecessary-lambda
|
||||||
fields=[
|
fields=[
|
||||||
('advertising_handle', 1),
|
('advertising_handle', 1),
|
||||||
(
|
(
|
||||||
@@ -3422,6 +3452,7 @@ class HCI_LE_Set_Extended_Advertising_Parameters_Command(HCI_Command):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def advertising_properties_string(cls, properties):
|
def advertising_properties_string(cls, properties):
|
||||||
|
# pylint: disable=line-too-long
|
||||||
return f'[{",".join(bit_flags_to_strings(properties, cls.ADVERTISING_PROPERTIES_NAMES))}]'
|
return f'[{",".join(bit_flags_to_strings(properties, cls.ADVERTISING_PROPERTIES_NAMES))}]'
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -3431,6 +3462,7 @@ class HCI_LE_Set_Extended_Advertising_Parameters_Command(HCI_Command):
|
|||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@HCI_Command.command(
|
@HCI_Command.command(
|
||||||
|
# pylint: disable=line-too-long,unnecessary-lambda
|
||||||
[
|
[
|
||||||
('advertising_handle', 1),
|
('advertising_handle', 1),
|
||||||
(
|
(
|
||||||
@@ -3480,6 +3512,7 @@ class HCI_LE_Set_Extended_Advertising_Data_Command(HCI_Command):
|
|||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@HCI_Command.command(
|
@HCI_Command.command(
|
||||||
|
# pylint: disable=line-too-long,unnecessary-lambda
|
||||||
[
|
[
|
||||||
('advertising_handle', 1),
|
('advertising_handle', 1),
|
||||||
(
|
(
|
||||||
@@ -3573,9 +3606,9 @@ class HCI_LE_Set_Extended_Advertising_Enable_Command(HCI_Command):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
fields = [('enable:', self.enable)]
|
fields = [('enable:', self.enable)]
|
||||||
for i in range(len(self.advertising_handles)):
|
for i, advertising_handle in enumerate(self.advertising_handles):
|
||||||
fields.append(
|
fields.append(
|
||||||
(f'advertising_handle[{i}]: ', self.advertising_handles[i])
|
(f'advertising_handle[{i}]: ', advertising_handle)
|
||||||
)
|
)
|
||||||
fields.append((f'duration[{i}]: ', self.durations[i]))
|
fields.append((f'duration[{i}]: ', self.durations[i]))
|
||||||
fields.append(
|
fields.append(
|
||||||
@@ -3736,7 +3769,7 @@ class HCI_LE_Set_Extended_Scan_Parameters_Command(HCI_Command):
|
|||||||
)
|
)
|
||||||
fields.append(
|
fields.append(
|
||||||
(f'{scanning_phy_str}.scan_interval:', self.scan_intervals[i])
|
(f'{scanning_phy_str}.scan_interval:', self.scan_intervals[i])
|
||||||
),
|
)
|
||||||
fields.append((f'{scanning_phy_str}.scan_window: ', self.scan_windows[i]))
|
fields.append((f'{scanning_phy_str}.scan_window: ', self.scan_windows[i]))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -3871,43 +3904,43 @@ class HCI_LE_Extended_Create_Connection_Command(HCI_Command):
|
|||||||
f'{initiating_phys_str}.scan_interval: ',
|
f'{initiating_phys_str}.scan_interval: ',
|
||||||
self.scan_intervals[i],
|
self.scan_intervals[i],
|
||||||
)
|
)
|
||||||
),
|
)
|
||||||
fields.append(
|
fields.append(
|
||||||
(
|
(
|
||||||
f'{initiating_phys_str}.scan_window: ',
|
f'{initiating_phys_str}.scan_window: ',
|
||||||
self.scan_windows[i],
|
self.scan_windows[i],
|
||||||
)
|
)
|
||||||
),
|
)
|
||||||
fields.append(
|
fields.append(
|
||||||
(
|
(
|
||||||
f'{initiating_phys_str}.connection_interval_min:',
|
f'{initiating_phys_str}.connection_interval_min:',
|
||||||
self.connection_interval_mins[i],
|
self.connection_interval_mins[i],
|
||||||
)
|
)
|
||||||
),
|
)
|
||||||
fields.append(
|
fields.append(
|
||||||
(
|
(
|
||||||
f'{initiating_phys_str}.connection_interval_max:',
|
f'{initiating_phys_str}.connection_interval_max:',
|
||||||
self.connection_interval_maxs[i],
|
self.connection_interval_maxs[i],
|
||||||
)
|
)
|
||||||
),
|
)
|
||||||
fields.append(
|
fields.append(
|
||||||
(
|
(
|
||||||
f'{initiating_phys_str}.max_latency: ',
|
f'{initiating_phys_str}.max_latency: ',
|
||||||
self.max_latencies[i],
|
self.max_latencies[i],
|
||||||
)
|
)
|
||||||
),
|
)
|
||||||
fields.append(
|
fields.append(
|
||||||
(
|
(
|
||||||
f'{initiating_phys_str}.supervision_timeout: ',
|
f'{initiating_phys_str}.supervision_timeout: ',
|
||||||
self.supervision_timeouts[i],
|
self.supervision_timeouts[i],
|
||||||
)
|
)
|
||||||
),
|
)
|
||||||
fields.append(
|
fields.append(
|
||||||
(
|
(
|
||||||
f'{initiating_phys_str}.min_ce_length: ',
|
f'{initiating_phys_str}.min_ce_length: ',
|
||||||
self.min_ce_lengths[i],
|
self.min_ce_lengths[i],
|
||||||
)
|
)
|
||||||
),
|
)
|
||||||
fields.append(
|
fields.append(
|
||||||
(
|
(
|
||||||
f'{initiating_phys_str}.max_ce_length: ',
|
f'{initiating_phys_str}.max_ce_length: ',
|
||||||
@@ -3933,6 +3966,7 @@ class HCI_LE_Extended_Create_Connection_Command(HCI_Command):
|
|||||||
'privacy_mode',
|
'privacy_mode',
|
||||||
{
|
{
|
||||||
'size': 1,
|
'size': 1,
|
||||||
|
# pylint: disable-next=unnecessary-lambda
|
||||||
'mapper': lambda x: HCI_LE_Set_Privacy_Mode_Command.privacy_mode_name(
|
'mapper': lambda x: HCI_LE_Set_Privacy_Mode_Command.privacy_mode_name(
|
||||||
x
|
x
|
||||||
),
|
),
|
||||||
@@ -3979,7 +4013,7 @@ class HCI_Event(HCI_Packet):
|
|||||||
meta_event_classes = {}
|
meta_event_classes = {}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def event(fields=[]):
|
def event(fields=()):
|
||||||
'''
|
'''
|
||||||
Decorator used to declare and register subclasses
|
Decorator used to declare and register subclasses
|
||||||
'''
|
'''
|
||||||
@@ -4005,16 +4039,16 @@ class HCI_Event(HCI_Packet):
|
|||||||
return inner
|
return inner
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def registered(cls):
|
def registered(event_class):
|
||||||
cls.name = cls.__name__.upper()
|
event_class.name = event_class.__name__.upper()
|
||||||
cls.event_code = key_with_value(HCI_EVENT_NAMES, cls.name)
|
event_class.event_code = key_with_value(HCI_EVENT_NAMES, event_class.name)
|
||||||
if cls.event_code is None:
|
if event_class.event_code is None:
|
||||||
raise KeyError('event not found in HCI_EVENT_NAMES')
|
raise KeyError('event not found in HCI_EVENT_NAMES')
|
||||||
|
|
||||||
# Register a factory for this class
|
# Register a factory for this class
|
||||||
HCI_Event.event_classes[cls.event_code] = cls
|
HCI_Event.event_classes[event_class.event_code] = event_class
|
||||||
|
|
||||||
return cls
|
return event_class
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_bytes(packet):
|
def from_bytes(packet):
|
||||||
@@ -4025,7 +4059,8 @@ class HCI_Event(HCI_Packet):
|
|||||||
raise ValueError('invalid packet length')
|
raise ValueError('invalid packet length')
|
||||||
|
|
||||||
if event_code == HCI_LE_META_EVENT:
|
if event_code == HCI_LE_META_EVENT:
|
||||||
# We do this dispatch here and not in the subclass in order to avoid call loops
|
# We do this dispatch here and not in the subclass in order to avoid call
|
||||||
|
# loops
|
||||||
subevent_code = parameters[0]
|
subevent_code = parameters[0]
|
||||||
cls = HCI_Event.meta_event_classes.get(subevent_code)
|
cls = HCI_Event.meta_event_classes.get(subevent_code)
|
||||||
if cls is None:
|
if cls is None:
|
||||||
@@ -4086,7 +4121,7 @@ class HCI_LE_Meta_Event(HCI_Event):
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def event(fields=[]):
|
def event(fields=()):
|
||||||
'''
|
'''
|
||||||
Decorator used to declare and register subclasses
|
Decorator used to declare and register subclasses
|
||||||
'''
|
'''
|
||||||
@@ -4214,9 +4249,9 @@ class HCI_LE_Advertising_Report_Event(HCI_LE_Meta_Event):
|
|||||||
def event_type_string(self):
|
def event_type_string(self):
|
||||||
return HCI_LE_Advertising_Report_Event.event_type_name(self.event_type)
|
return HCI_LE_Advertising_Report_Event.event_type_name(self.event_type)
|
||||||
|
|
||||||
def to_string(self, prefix):
|
def to_string(self, indentation='', _=None):
|
||||||
return super().to_string(
|
return super().to_string(
|
||||||
prefix,
|
indentation,
|
||||||
{
|
{
|
||||||
'event_type': HCI_LE_Advertising_Report_Event.event_type_name,
|
'event_type': HCI_LE_Advertising_Report_Event.event_type_name,
|
||||||
'address_type': Address.address_type_name,
|
'address_type': Address.address_type_name,
|
||||||
@@ -4443,9 +4478,10 @@ class HCI_LE_Extended_Advertising_Report_Event(HCI_LE_Meta_Event):
|
|||||||
self.event_type
|
self.event_type
|
||||||
)
|
)
|
||||||
|
|
||||||
def to_string(self, prefix):
|
def to_string(self, indentation='', _=None):
|
||||||
|
# pylint: disable=line-too-long
|
||||||
return super().to_string(
|
return super().to_string(
|
||||||
prefix,
|
indentation,
|
||||||
{
|
{
|
||||||
'event_type': HCI_LE_Extended_Advertising_Report_Event.event_type_string,
|
'event_type': HCI_LE_Extended_Advertising_Report_Event.event_type_string,
|
||||||
'address_type': Address.address_type_name,
|
'address_type': Address.address_type_name,
|
||||||
@@ -4472,6 +4508,7 @@ class HCI_LE_Extended_Advertising_Report_Event(HCI_LE_Meta_Event):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
if legacy_pdu_type is not None:
|
if legacy_pdu_type is not None:
|
||||||
|
# pylint: disable=line-too-long
|
||||||
legacy_info_string = f'({HCI_LE_Advertising_Report_Event.event_type_name(legacy_pdu_type)})'
|
legacy_info_string = f'({HCI_LE_Advertising_Report_Event.event_type_name(legacy_pdu_type)})'
|
||||||
else:
|
else:
|
||||||
legacy_info_string = ''
|
legacy_info_string = ''
|
||||||
@@ -4587,6 +4624,7 @@ class HCI_Inquiry_Result_Event(HCI_Event):
|
|||||||
'link_type',
|
'link_type',
|
||||||
{
|
{
|
||||||
'size': 1,
|
'size': 1,
|
||||||
|
# pylint: disable-next=unnecessary-lambda
|
||||||
'mapper': lambda x: HCI_Connection_Complete_Event.link_type_name(x),
|
'mapper': lambda x: HCI_Connection_Complete_Event.link_type_name(x),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -4622,6 +4660,7 @@ class HCI_Connection_Complete_Event(HCI_Event):
|
|||||||
'link_type',
|
'link_type',
|
||||||
{
|
{
|
||||||
'size': 1,
|
'size': 1,
|
||||||
|
# pylint: disable-next=unnecessary-lambda
|
||||||
'mapper': lambda x: HCI_Connection_Complete_Event.link_type_name(x),
|
'mapper': lambda x: HCI_Connection_Complete_Event.link_type_name(x),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -4678,6 +4717,7 @@ class HCI_Remote_Name_Request_Complete_Event(HCI_Event):
|
|||||||
'encryption_enabled',
|
'encryption_enabled',
|
||||||
{
|
{
|
||||||
'size': 1,
|
'size': 1,
|
||||||
|
# pylint: disable-next=unnecessary-lambda
|
||||||
'mapper': lambda x: HCI_Encryption_Change_Event.encryption_enabled_name(
|
'mapper': lambda x: HCI_Encryption_Change_Event.encryption_enabled_name(
|
||||||
x
|
x
|
||||||
),
|
),
|
||||||
@@ -4746,16 +4786,20 @@ class HCI_Command_Complete_Event(HCI_Event):
|
|||||||
See Bluetooth spec @ 7.7.14 Command Complete Event
|
See Bluetooth spec @ 7.7.14 Command Complete Event
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
return_parameters = b''
|
||||||
|
|
||||||
def map_return_parameters(self, return_parameters):
|
def map_return_parameters(self, return_parameters):
|
||||||
# Map simple 'status' return parameters to their named constant form
|
'''Map simple 'status' return parameters to their named constant form'''
|
||||||
if type(return_parameters) is bytes and len(return_parameters) == 1:
|
|
||||||
|
if isinstance(return_parameters, bytes) and len(return_parameters) == 1:
|
||||||
# Byte-array form
|
# Byte-array form
|
||||||
return HCI_Constant.status_name(return_parameters[0])
|
return HCI_Constant.status_name(return_parameters[0])
|
||||||
elif type(return_parameters) is int:
|
|
||||||
|
if isinstance(return_parameters, int):
|
||||||
# Already converted to an integer status code
|
# Already converted to an integer status code
|
||||||
return HCI_Constant.status_name(return_parameters)
|
return HCI_Constant.status_name(return_parameters)
|
||||||
else:
|
|
||||||
return return_parameters
|
return return_parameters
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_parameters(parameters):
|
def from_parameters(parameters):
|
||||||
@@ -4766,8 +4810,12 @@ class HCI_Command_Complete_Event(HCI_Event):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Parse the return parameters
|
# Parse the return parameters
|
||||||
if type(self.return_parameters) is bytes and len(self.return_parameters) == 1:
|
if (
|
||||||
# All commands with 1-byte return parameters return a 'status' field, convert it to an integer
|
isinstance(self.return_parameters, bytes)
|
||||||
|
and len(self.return_parameters) == 1
|
||||||
|
):
|
||||||
|
# All commands with 1-byte return parameters return a 'status' field,
|
||||||
|
# convert it to an integer
|
||||||
self.return_parameters = self.return_parameters[0]
|
self.return_parameters = self.return_parameters[0]
|
||||||
else:
|
else:
|
||||||
cls = HCI_Command.command_classes.get(self.command_opcode)
|
cls = HCI_Command.command_classes.get(self.command_opcode)
|
||||||
@@ -4793,6 +4841,7 @@ class HCI_Command_Complete_Event(HCI_Event):
|
|||||||
[
|
[
|
||||||
(
|
(
|
||||||
'status',
|
'status',
|
||||||
|
# pylint: disable-next=unnecessary-lambda
|
||||||
{'size': 1, 'mapper': lambda x: HCI_Command_Status_Event.status_name(x)},
|
{'size': 1, 'mapper': lambda x: HCI_Command_Status_Event.status_name(x)},
|
||||||
),
|
),
|
||||||
('num_hci_command_packets', 1),
|
('num_hci_command_packets', 1),
|
||||||
@@ -4810,8 +4859,8 @@ class HCI_Command_Status_Event(HCI_Event):
|
|||||||
def status_name(status):
|
def status_name(status):
|
||||||
if status == HCI_Command_Status_Event.PENDING:
|
if status == HCI_Command_Status_Event.PENDING:
|
||||||
return 'PENDING'
|
return 'PENDING'
|
||||||
else:
|
|
||||||
return HCI_Constant.error_name(status)
|
return HCI_Constant.error_name(status)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -4869,10 +4918,10 @@ class HCI_Number_Of_Completed_Packets_Event(HCI_Event):
|
|||||||
color(' number_of_handles: ', 'cyan')
|
color(' number_of_handles: ', 'cyan')
|
||||||
+ f'{len(self.connection_handles)}',
|
+ f'{len(self.connection_handles)}',
|
||||||
]
|
]
|
||||||
for i in range(len(self.connection_handles)):
|
for i, connection_handle in enumerate(self.connection_handles):
|
||||||
lines.append(
|
lines.append(
|
||||||
color(f' connection_handle[{i}]: ', 'cyan')
|
color(f' connection_handle[{i}]: ', 'cyan')
|
||||||
+ f'{self.connection_handles[i]}'
|
+ f'{connection_handle}'
|
||||||
)
|
)
|
||||||
lines.append(
|
lines.append(
|
||||||
color(f' num_completed_packets[{i}]: ', 'cyan')
|
color(f' num_completed_packets[{i}]: ', 'cyan')
|
||||||
@@ -4888,6 +4937,7 @@ class HCI_Number_Of_Completed_Packets_Event(HCI_Event):
|
|||||||
('connection_handle', 2),
|
('connection_handle', 2),
|
||||||
(
|
(
|
||||||
'current_mode',
|
'current_mode',
|
||||||
|
# pylint: disable-next=unnecessary-lambda
|
||||||
{'size': 1, 'mapper': lambda x: HCI_Mode_Change_Event.mode_name(x)},
|
{'size': 1, 'mapper': lambda x: HCI_Mode_Change_Event.mode_name(x)},
|
||||||
),
|
),
|
||||||
('interval', 2),
|
('interval', 2),
|
||||||
@@ -5044,6 +5094,7 @@ class HCI_Read_Remote_Extended_Features_Complete_Event(HCI_Event):
|
|||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@HCI_Event.event(
|
@HCI_Event.event(
|
||||||
|
# pylint: disable=line-too-long
|
||||||
[
|
[
|
||||||
('status', STATUS_SPEC),
|
('status', STATUS_SPEC),
|
||||||
('connection_handle', 2),
|
('connection_handle', 2),
|
||||||
@@ -5052,6 +5103,7 @@ class HCI_Read_Remote_Extended_Features_Complete_Event(HCI_Event):
|
|||||||
'link_type',
|
'link_type',
|
||||||
{
|
{
|
||||||
'size': 1,
|
'size': 1,
|
||||||
|
# pylint: disable-next=unnecessary-lambda
|
||||||
'mapper': lambda x: HCI_Synchronous_Connection_Complete_Event.link_type_name(
|
'mapper': lambda x: HCI_Synchronous_Connection_Complete_Event.link_type_name(
|
||||||
x
|
x
|
||||||
),
|
),
|
||||||
@@ -5065,6 +5117,7 @@ class HCI_Read_Remote_Extended_Features_Complete_Event(HCI_Event):
|
|||||||
'air_mode',
|
'air_mode',
|
||||||
{
|
{
|
||||||
'size': 1,
|
'size': 1,
|
||||||
|
# pylint: disable-next=unnecessary-lambda
|
||||||
'mapper': lambda x: HCI_Synchronous_Connection_Complete_Event.air_mode_name(
|
'mapper': lambda x: HCI_Synchronous_Connection_Complete_Event.air_mode_name(
|
||||||
x
|
x
|
||||||
),
|
),
|
||||||
@@ -5229,7 +5282,7 @@ class HCI_Remote_Host_Supported_Features_Notification_Event(HCI_Event):
|
|||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class HCI_AclDataPacket(HCI_Packet):
|
class HCI_AclDataPacket:
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec @ 5.4.2 HCI ACL Data Packets
|
See Bluetooth spec @ 5.4.2 HCI ACL Data Packets
|
||||||
'''
|
'''
|
||||||
@@ -5268,7 +5321,13 @@ class HCI_AclDataPacket(HCI_Packet):
|
|||||||
return self.to_bytes()
|
return self.to_bytes()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'{color("ACL", "blue")}: handle=0x{self.connection_handle:04x}, pb={self.pb_flag}, bc={self.bc_flag}, data_total_length={self.data_total_length}, data={self.data.hex()}'
|
return (
|
||||||
|
f'{color("ACL", "blue")}: '
|
||||||
|
f'handle=0x{self.connection_handle:04x}'
|
||||||
|
f'pb={self.pb_flag}, bc={self.bc_flag}, '
|
||||||
|
f'data_total_length={self.data_total_length}, '
|
||||||
|
f'data={self.data.hex()}'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -5279,9 +5338,9 @@ class HCI_AclDataPacketAssembler:
|
|||||||
self.l2cap_pdu_length = 0
|
self.l2cap_pdu_length = 0
|
||||||
|
|
||||||
def feed_packet(self, packet):
|
def feed_packet(self, packet):
|
||||||
if (
|
if packet.pb_flag in (
|
||||||
packet.pb_flag == HCI_ACL_PB_FIRST_NON_FLUSHABLE
|
HCI_ACL_PB_FIRST_NON_FLUSHABLE,
|
||||||
or packet.pb_flag == HCI_ACL_PB_FIRST_FLUSHABLE
|
HCI_ACL_PB_FIRST_FLUSHABLE,
|
||||||
):
|
):
|
||||||
(l2cap_pdu_length,) = struct.unpack_from('<H', packet.data, 0)
|
(l2cap_pdu_length,) = struct.unpack_from('<H', packet.data, 0)
|
||||||
self.current_data = packet.data
|
self.current_data = packet.data
|
||||||
|
|||||||
+14
-12
@@ -18,10 +18,9 @@
|
|||||||
import logging
|
import logging
|
||||||
from colors import color
|
from colors import color
|
||||||
|
|
||||||
from bumble.smp import SMP_CID, SMP_Command
|
from .att import ATT_CID, ATT_PDU
|
||||||
|
from .smp import SMP_CID, SMP_Command
|
||||||
from .core import name_or_number
|
from .core import name_or_number
|
||||||
from .gatt import ATT_PDU, ATT_CID
|
|
||||||
from .l2cap import (
|
from .l2cap import (
|
||||||
L2CAP_PDU,
|
L2CAP_PDU,
|
||||||
L2CAP_CONNECTION_REQUEST,
|
L2CAP_CONNECTION_REQUEST,
|
||||||
@@ -66,6 +65,7 @@ class PacketTracer:
|
|||||||
self.psms = {} # PSM, by source_cid
|
self.psms = {} # PSM, by source_cid
|
||||||
self.peer = None # ACL stream in the other direction
|
self.peer = None # ACL stream in the other direction
|
||||||
|
|
||||||
|
# pylint: disable=too-many-nested-blocks
|
||||||
def on_acl_pdu(self, pdu):
|
def on_acl_pdu(self, pdu):
|
||||||
l2cap_pdu = L2CAP_PDU.from_bytes(pdu)
|
l2cap_pdu = L2CAP_PDU.from_bytes(pdu)
|
||||||
|
|
||||||
@@ -75,10 +75,7 @@ class PacketTracer:
|
|||||||
elif l2cap_pdu.cid == SMP_CID:
|
elif l2cap_pdu.cid == SMP_CID:
|
||||||
smp_command = SMP_Command.from_bytes(l2cap_pdu.payload)
|
smp_command = SMP_Command.from_bytes(l2cap_pdu.payload)
|
||||||
self.analyzer.emit(smp_command)
|
self.analyzer.emit(smp_command)
|
||||||
elif (
|
elif l2cap_pdu.cid in (L2CAP_SIGNALING_CID, L2CAP_LE_SIGNALING_CID):
|
||||||
l2cap_pdu.cid == L2CAP_SIGNALING_CID
|
|
||||||
or l2cap_pdu.cid == L2CAP_LE_SIGNALING_CID
|
|
||||||
):
|
|
||||||
control_frame = L2CAP_Control_Frame.from_bytes(l2cap_pdu.payload)
|
control_frame = L2CAP_Control_Frame.from_bytes(l2cap_pdu.payload)
|
||||||
self.analyzer.emit(control_frame)
|
self.analyzer.emit(control_frame)
|
||||||
|
|
||||||
@@ -95,7 +92,8 @@ class PacketTracer:
|
|||||||
# Found a pending connection
|
# Found a pending connection
|
||||||
self.psms[control_frame.destination_cid] = psm
|
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:
|
if psm == AVDTP_PSM:
|
||||||
self.avdtp_assemblers[
|
self.avdtp_assemblers[
|
||||||
control_frame.source_cid
|
control_frame.source_cid
|
||||||
@@ -117,7 +115,8 @@ class PacketTracer:
|
|||||||
self.analyzer.emit(rfcomm_frame)
|
self.analyzer.emit(rfcomm_frame)
|
||||||
elif psm == AVDTP_PSM:
|
elif psm == AVDTP_PSM:
|
||||||
self.analyzer.emit(
|
self.analyzer.emit(
|
||||||
f'{color("L2CAP", "green")} [CID={l2cap_pdu.cid}, PSM=AVDTP]: {l2cap_pdu.payload.hex()}'
|
f'{color("L2CAP", "green")} [CID={l2cap_pdu.cid}, '
|
||||||
|
f'PSM=AVDTP]: {l2cap_pdu.payload.hex()}'
|
||||||
)
|
)
|
||||||
assembler = self.avdtp_assemblers.get(l2cap_pdu.cid)
|
assembler = self.avdtp_assemblers.get(l2cap_pdu.cid)
|
||||||
if assembler:
|
if assembler:
|
||||||
@@ -125,7 +124,8 @@ class PacketTracer:
|
|||||||
else:
|
else:
|
||||||
psm_string = name_or_number(PSM_NAMES, psm)
|
psm_string = name_or_number(PSM_NAMES, psm)
|
||||||
self.analyzer.emit(
|
self.analyzer.emit(
|
||||||
f'{color("L2CAP", "green")} [CID={l2cap_pdu.cid}, PSM={psm_string}]: {l2cap_pdu.payload.hex()}'
|
f'{color("L2CAP", "green")} [CID={l2cap_pdu.cid}, '
|
||||||
|
f'PSM={psm_string}]: {l2cap_pdu.payload.hex()}'
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.analyzer.emit(l2cap_pdu)
|
self.analyzer.emit(l2cap_pdu)
|
||||||
@@ -147,7 +147,8 @@ class PacketTracer:
|
|||||||
|
|
||||||
def start_acl_stream(self, connection_handle):
|
def start_acl_stream(self, connection_handle):
|
||||||
logger.info(
|
logger.info(
|
||||||
f'[{self.label}] +++ Creating ACL stream for connection 0x{connection_handle:04X}'
|
f'[{self.label}] +++ Creating ACL stream for connection '
|
||||||
|
f'0x{connection_handle:04X}'
|
||||||
)
|
)
|
||||||
stream = PacketTracer.AclStream(self)
|
stream = PacketTracer.AclStream(self)
|
||||||
self.acl_streams[connection_handle] = stream
|
self.acl_streams[connection_handle] = stream
|
||||||
@@ -162,7 +163,8 @@ class PacketTracer:
|
|||||||
def end_acl_stream(self, connection_handle):
|
def end_acl_stream(self, connection_handle):
|
||||||
if connection_handle in self.acl_streams:
|
if connection_handle in self.acl_streams:
|
||||||
logger.info(
|
logger.info(
|
||||||
f'[{self.label}] --- Removing ACL stream for connection 0x{connection_handle:04X}'
|
f'[{self.label}] --- Removing ACL stream for connection '
|
||||||
|
f'0x{connection_handle:04X}'
|
||||||
)
|
)
|
||||||
del self.acl_streams[connection_handle]
|
del self.acl_streams[connection_handle]
|
||||||
|
|
||||||
|
|||||||
+8
-8
@@ -43,7 +43,7 @@ class HfpProtocol:
|
|||||||
|
|
||||||
def feed(self, data):
|
def feed(self, data):
|
||||||
# Convert the data to a string if needed
|
# Convert the data to a string if needed
|
||||||
if type(data) == bytes:
|
if isinstance(data, bytes):
|
||||||
data = data.decode('utf-8')
|
data = data.decode('utf-8')
|
||||||
|
|
||||||
logger.debug(f'<<< Data received: {data}')
|
logger.debug(f'<<< Data received: {data}')
|
||||||
@@ -79,16 +79,16 @@ class HfpProtocol:
|
|||||||
async def initialize_service(self):
|
async def initialize_service(self):
|
||||||
# Perform Service Level Connection Initialization
|
# Perform Service Level Connection Initialization
|
||||||
self.send_command_line('AT+BRSF=2072') # Retrieve Supported Features
|
self.send_command_line('AT+BRSF=2072') # Retrieve Supported Features
|
||||||
line = await (self.next_line())
|
await (self.next_line())
|
||||||
line = await (self.next_line())
|
await (self.next_line())
|
||||||
|
|
||||||
self.send_command_line('AT+CIND=?')
|
self.send_command_line('AT+CIND=?')
|
||||||
line = await (self.next_line())
|
await (self.next_line())
|
||||||
line = await (self.next_line())
|
await (self.next_line())
|
||||||
|
|
||||||
self.send_command_line('AT+CIND?')
|
self.send_command_line('AT+CIND?')
|
||||||
line = await (self.next_line())
|
await (self.next_line())
|
||||||
line = await (self.next_line())
|
await (self.next_line())
|
||||||
|
|
||||||
self.send_command_line('AT+CMER=3,0,0,1')
|
self.send_command_line('AT+CMER=3,0,0,1')
|
||||||
line = await (self.next_line())
|
await (self.next_line())
|
||||||
|
|||||||
+105
-30
@@ -16,17 +16,61 @@
|
|||||||
# Imports
|
# Imports
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import collections
|
||||||
import logging
|
import logging
|
||||||
|
import struct
|
||||||
|
|
||||||
from colors import color
|
from colors import color
|
||||||
|
|
||||||
from .hci import *
|
from bumble.l2cap import L2CAP_PDU
|
||||||
from .l2cap import *
|
|
||||||
from .att import *
|
from .hci import (
|
||||||
from .gatt import *
|
HCI_ACL_DATA_PACKET,
|
||||||
from .smp import *
|
HCI_COMMAND_COMPLETE_EVENT,
|
||||||
from .core import ConnectionParameters
|
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_PIN_Code_Request_Negative_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 .utils import AbortableEventEmitter
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Logging
|
# Logging
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -71,6 +115,7 @@ class Host(AbortableEventEmitter):
|
|||||||
|
|
||||||
self.hci_sink = None
|
self.hci_sink = None
|
||||||
self.ready = False # True when we can accept incoming packets
|
self.ready = False # True when we can accept incoming packets
|
||||||
|
self.reset_done = False
|
||||||
self.connections = {} # Connections, by connection handle
|
self.connections = {} # Connections, by connection handle
|
||||||
self.pending_command = None
|
self.pending_command = None
|
||||||
self.pending_response = None
|
self.pending_response = None
|
||||||
@@ -139,10 +184,12 @@ class Host(AbortableEventEmitter):
|
|||||||
self.local_version is not None
|
self.local_version is not None
|
||||||
and self.local_version.hci_version <= HCI_VERSION_BLUETOOTH_CORE_4_0
|
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
|
# Some older controllers don't like event masks with bits they don't
|
||||||
|
# understand
|
||||||
le_event_mask = bytes.fromhex('1F00000000000000')
|
le_event_mask = bytes.fromhex('1F00000000000000')
|
||||||
else:
|
else:
|
||||||
le_event_mask = bytes.fromhex('FFFFF00000000000')
|
le_event_mask = bytes.fromhex('FFFFF00000000000')
|
||||||
|
|
||||||
await self.send_command(
|
await self.send_command(
|
||||||
HCI_LE_Set_Event_Mask_Command(le_event_mask=le_event_mask)
|
HCI_LE_Set_Event_Mask_Command(le_event_mask=le_event_mask)
|
||||||
)
|
)
|
||||||
@@ -159,7 +206,8 @@ class Host(AbortableEventEmitter):
|
|||||||
)
|
)
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'HCI ACL flow control: hc_acl_data_packet_length={self.hc_acl_data_packet_length},'
|
'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}'
|
f'hc_total_num_acl_data_packets={self.hc_total_num_acl_data_packets}'
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -175,8 +223,10 @@ class Host(AbortableEventEmitter):
|
|||||||
)
|
)
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'HCI LE ACL flow control: hc_le_acl_data_packet_length={self.hc_le_acl_data_packet_length},'
|
'HCI LE ACL flow control: '
|
||||||
f'hc_total_num_le_acl_data_packets={self.hc_total_num_le_acl_data_packets}'
|
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}'
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -244,9 +294,9 @@ class Host(AbortableEventEmitter):
|
|||||||
|
|
||||||
# Check the return parameters if required
|
# Check the return parameters if required
|
||||||
if check_result:
|
if check_result:
|
||||||
if type(response.return_parameters) is int:
|
if isinstance(response.return_parameters, int):
|
||||||
status = response.return_parameters
|
status = response.return_parameters
|
||||||
elif type(response.return_parameters) is bytes:
|
elif isinstance(response.return_parameters, bytes):
|
||||||
# return parameters first field is a one byte status code
|
# return parameters first field is a one byte status code
|
||||||
status = response.return_parameters[0]
|
status = response.return_parameters[0]
|
||||||
else:
|
else:
|
||||||
@@ -306,7 +356,8 @@ class Host(AbortableEventEmitter):
|
|||||||
|
|
||||||
if len(self.acl_packet_queue):
|
if len(self.acl_packet_queue):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'{self.acl_packets_in_flight} ACL packets in flight, {len(self.acl_packet_queue)} in queue'
|
f'{self.acl_packets_in_flight} ACL packets in flight, '
|
||||||
|
f'{len(self.acl_packet_queue)} in queue'
|
||||||
)
|
)
|
||||||
|
|
||||||
def check_acl_packet_queue(self):
|
def check_acl_packet_queue(self):
|
||||||
@@ -400,7 +451,9 @@ class Host(AbortableEventEmitter):
|
|||||||
# Check that it is what we were expecting
|
# Check that it is what we were expecting
|
||||||
if self.pending_command.op_code != event.command_opcode:
|
if self.pending_command.op_code != event.command_opcode:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f'!!! command result mismatch, expected 0x{self.pending_command.op_code:X} but got 0x{event.command_opcode:X}'
|
'!!! 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)
|
self.pending_response.set_result(event)
|
||||||
@@ -415,10 +468,12 @@ class Host(AbortableEventEmitter):
|
|||||||
|
|
||||||
def on_hci_command_complete_event(self, event):
|
def on_hci_command_complete_event(self, event):
|
||||||
if event.command_opcode == 0:
|
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')
|
logger.debug('no-command event')
|
||||||
else:
|
return None
|
||||||
return self.on_command_processed(event)
|
|
||||||
|
return self.on_command_processed(event)
|
||||||
|
|
||||||
def on_hci_command_status_event(self, event):
|
def on_hci_command_status_event(self, event):
|
||||||
return self.on_command_processed(event)
|
return self.on_command_processed(event)
|
||||||
@@ -431,7 +486,8 @@ class Host(AbortableEventEmitter):
|
|||||||
else:
|
else:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
color(
|
color(
|
||||||
f'!!! {total_packets} completed but only {self.acl_packets_in_flight} in flight'
|
'!!! {total_packets} completed but only '
|
||||||
|
f'{self.acl_packets_in_flight} in flight'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.acl_packets_in_flight = 0
|
self.acl_packets_in_flight = 0
|
||||||
@@ -451,7 +507,8 @@ class Host(AbortableEventEmitter):
|
|||||||
if event.status == HCI_SUCCESS:
|
if event.status == HCI_SUCCESS:
|
||||||
# Create/update the connection
|
# Create/update the connection
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'### CONNECTION: [0x{event.connection_handle:04X}] {event.peer_address} as {HCI_Constant.role_name(event.role)}'
|
f'### CONNECTION: [0x{event.connection_handle:04X}] '
|
||||||
|
f'{event.peer_address} as {HCI_Constant.role_name(event.role)}'
|
||||||
)
|
)
|
||||||
|
|
||||||
connection = self.connections.get(event.connection_handle)
|
connection = self.connections.get(event.connection_handle)
|
||||||
@@ -496,7 +553,8 @@ class Host(AbortableEventEmitter):
|
|||||||
if event.status == HCI_SUCCESS:
|
if event.status == HCI_SUCCESS:
|
||||||
# Create/update the connection
|
# Create/update the connection
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'### BR/EDR CONNECTION: [0x{event.connection_handle:04X}] {event.bd_addr}'
|
f'### BR/EDR CONNECTION: [0x{event.connection_handle:04X}] '
|
||||||
|
f'{event.bd_addr}'
|
||||||
)
|
)
|
||||||
|
|
||||||
connection = self.connections.get(event.connection_handle)
|
connection = self.connections.get(event.connection_handle)
|
||||||
@@ -536,7 +594,10 @@ class Host(AbortableEventEmitter):
|
|||||||
|
|
||||||
if event.status == HCI_SUCCESS:
|
if event.status == HCI_SUCCESS:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'### DISCONNECTION: [0x{event.connection_handle:04X}] {connection.peer_address} as {HCI_Constant.role_name(connection.role)}, reason={event.reason}'
|
f'### DISCONNECTION: [0x{event.connection_handle:04X}] '
|
||||||
|
f'{connection.peer_address} as '
|
||||||
|
f'{HCI_Constant.role_name(connection.role)}, '
|
||||||
|
f'reason={event.reason}'
|
||||||
)
|
)
|
||||||
del self.connections[event.connection_handle]
|
del self.connections[event.connection_handle]
|
||||||
|
|
||||||
@@ -616,9 +677,15 @@ class Host(AbortableEventEmitter):
|
|||||||
logger.debug('no long term key provider')
|
logger.debug('no long term key provider')
|
||||||
long_term_key = None
|
long_term_key = None
|
||||||
else:
|
else:
|
||||||
long_term_key = await self.abort_on('flush', self.long_term_key_provider(
|
long_term_key = await self.abort_on(
|
||||||
connection.handle, event.random_number, event.encryption_diversifier
|
'flush',
|
||||||
))
|
# pylint: disable-next=not-callable
|
||||||
|
self.long_term_key_provider(
|
||||||
|
connection.handle,
|
||||||
|
event.random_number,
|
||||||
|
event.encryption_diversifier,
|
||||||
|
),
|
||||||
|
)
|
||||||
if long_term_key:
|
if long_term_key:
|
||||||
response = HCI_LE_Long_Term_Key_Request_Reply_Command(
|
response = HCI_LE_Long_Term_Key_Request_Reply_Command(
|
||||||
connection_handle=event.connection_handle,
|
connection_handle=event.connection_handle,
|
||||||
@@ -642,12 +709,14 @@ class Host(AbortableEventEmitter):
|
|||||||
def on_hci_role_change_event(self, event):
|
def on_hci_role_change_event(self, event):
|
||||||
if event.status == HCI_SUCCESS:
|
if event.status == HCI_SUCCESS:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'role change for {event.bd_addr}: {HCI_Constant.role_name(event.new_role)}'
|
f'role change for {event.bd_addr}: '
|
||||||
|
f'{HCI_Constant.role_name(event.new_role)}'
|
||||||
)
|
)
|
||||||
# TODO: lookup the connection and update the role
|
# TODO: lookup the connection and update the role
|
||||||
else:
|
else:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'role change for {event.bd_addr} failed: {HCI_Constant.error_name(event.status)}'
|
f'role change for {event.bd_addr} failed: '
|
||||||
|
f'{HCI_Constant.error_name(event.status)}'
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_hci_le_data_length_change_event(self, event):
|
def on_hci_le_data_length_change_event(self, event):
|
||||||
@@ -706,13 +775,15 @@ class Host(AbortableEventEmitter):
|
|||||||
|
|
||||||
def on_hci_link_key_notification_event(self, event):
|
def on_hci_link_key_notification_event(self, event):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'link key for {event.bd_addr}: {event.link_key.hex()}, type={HCI_Constant.link_key_type_name(event.key_type)}'
|
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)
|
self.emit('link_key', event.bd_addr, event.link_key, event.key_type)
|
||||||
|
|
||||||
def on_hci_simple_pairing_complete_event(self, event):
|
def on_hci_simple_pairing_complete_event(self, event):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'simple pairing complete for {event.bd_addr}: status={HCI_Constant.status_name(event.status)}'
|
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):
|
def on_hci_pin_code_request_event(self, event):
|
||||||
@@ -728,7 +799,11 @@ class Host(AbortableEventEmitter):
|
|||||||
logger.debug('no link key provider')
|
logger.debug('no link key provider')
|
||||||
link_key = None
|
link_key = None
|
||||||
else:
|
else:
|
||||||
link_key = await self.abort_on('flush', 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:
|
if link_key:
|
||||||
response = HCI_Link_Key_Request_Reply_Command(
|
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
|
||||||
@@ -763,7 +838,7 @@ class Host(AbortableEventEmitter):
|
|||||||
'authentication_user_passkey_notification', event.bd_addr, event.passkey
|
'authentication_user_passkey_notification', event.bd_addr, event.passkey
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_hci_inquiry_complete_event(self, event):
|
def on_hci_inquiry_complete_event(self, _event):
|
||||||
self.emit('inquiry_complete')
|
self.emit('inquiry_complete')
|
||||||
|
|
||||||
def on_hci_inquiry_result_with_rssi_event(self, event):
|
def on_hci_inquiry_result_with_rssi_event(self, event):
|
||||||
|
|||||||
+13
-8
@@ -76,8 +76,10 @@ class PairingKeys:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def key_from_dict(keys_dict, key_name):
|
def key_from_dict(keys_dict, key_name):
|
||||||
key_dict = keys_dict.get(key_name)
|
key_dict = keys_dict.get(key_name)
|
||||||
if key_dict is not None:
|
if key_dict is None:
|
||||||
return PairingKeys.Key.from_dict(key_dict)
|
return None
|
||||||
|
|
||||||
|
return PairingKeys.Key.from_dict(key_dict)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_dict(keys_dict):
|
def from_dict(keys_dict):
|
||||||
@@ -121,9 +123,9 @@ class PairingKeys:
|
|||||||
|
|
||||||
def print(self, prefix=''):
|
def print(self, prefix=''):
|
||||||
keys_dict = self.to_dict()
|
keys_dict = self.to_dict()
|
||||||
for (property, value) in keys_dict.items():
|
for (container_property, value) in keys_dict.items():
|
||||||
if type(value) is dict:
|
if isinstance(value, dict):
|
||||||
print(f'{prefix}{color(property, "cyan")}:')
|
print(f'{prefix}{color(container_property, "cyan")}:')
|
||||||
for (key_property, key_value) in value.items():
|
for (key_property, key_value) in value.items():
|
||||||
print(f'{prefix} {color(key_property, "green")}: {key_value}')
|
print(f'{prefix} {color(key_property, "green")}: {key_value}')
|
||||||
else:
|
else:
|
||||||
@@ -138,7 +140,7 @@ class KeyStore:
|
|||||||
async def update(self, name, keys):
|
async def update(self, name, keys):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def get(self, name):
|
async def get(self, _name):
|
||||||
return PairingKeys()
|
return PairingKeys()
|
||||||
|
|
||||||
async def get_all(self):
|
async def get_all(self):
|
||||||
@@ -193,6 +195,9 @@ class JsonKeyStore(KeyStore):
|
|||||||
|
|
||||||
if filename is None:
|
if filename is None:
|
||||||
# Use a default for the current user
|
# Use a default for the current user
|
||||||
|
|
||||||
|
# Import here because this may not exist on all platforms
|
||||||
|
# pylint: disable=import-outside-toplevel
|
||||||
import appdirs
|
import appdirs
|
||||||
|
|
||||||
self.directory_name = os.path.join(
|
self.directory_name = os.path.join(
|
||||||
@@ -219,7 +224,7 @@ class JsonKeyStore(KeyStore):
|
|||||||
|
|
||||||
async def load(self):
|
async def load(self):
|
||||||
try:
|
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)
|
return json.load(json_file)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
return {}
|
return {}
|
||||||
@@ -231,7 +236,7 @@ class JsonKeyStore(KeyStore):
|
|||||||
|
|
||||||
# Save to a temporary file
|
# Save to a temporary file
|
||||||
temp_filename = self.filename + '.tmp'
|
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)
|
json.dump(db, output, sort_keys=True, indent=4)
|
||||||
|
|
||||||
# Atomically replace the previous file
|
# Atomically replace the previous file
|
||||||
|
|||||||
+113
-47
@@ -41,6 +41,7 @@ logger = logging.getLogger(__name__)
|
|||||||
# Constants
|
# Constants
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# fmt: off
|
# fmt: off
|
||||||
|
# pylint: disable=line-too-long
|
||||||
|
|
||||||
L2CAP_SIGNALING_CID = 0x01
|
L2CAP_SIGNALING_CID = 0x01
|
||||||
L2CAP_LE_SIGNALING_CID = 0x05
|
L2CAP_LE_SIGNALING_CID = 0x05
|
||||||
@@ -137,11 +138,15 @@ L2CAP_MAXIMUM_TRANSMISSION_UNIT_CONFIGURATION_OPTION_TYPE = 0x01
|
|||||||
L2CAP_MTU_CONFIGURATION_PARAMETER_TYPE = 0x01
|
L2CAP_MTU_CONFIGURATION_PARAMETER_TYPE = 0x01
|
||||||
|
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
# pylint: enable=line-too-long
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Classes
|
# Classes
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
# pylint: disable=invalid-name
|
||||||
|
|
||||||
|
|
||||||
class L2CAP_PDU:
|
class L2CAP_PDU:
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec @ Vol 3, Part A - 3 DATA PACKET FORMAT
|
See Bluetooth spec @ Vol 3, Part A - 3 DATA PACKET FORMAT
|
||||||
@@ -181,6 +186,7 @@ class L2CAP_Control_Frame:
|
|||||||
|
|
||||||
classes = {}
|
classes = {}
|
||||||
code = 0
|
code = 0
|
||||||
|
name = None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_bytes(pdu):
|
def from_bytes(pdu):
|
||||||
@@ -215,11 +221,11 @@ class L2CAP_Control_Frame:
|
|||||||
def decode_configuration_options(data):
|
def decode_configuration_options(data):
|
||||||
options = []
|
options = []
|
||||||
while len(data) >= 2:
|
while len(data) >= 2:
|
||||||
type = data[0]
|
value_type = data[0]
|
||||||
length = data[1]
|
length = data[1]
|
||||||
value = data[2 : 2 + length]
|
value = data[2 : 2 + length]
|
||||||
data = data[2 + length :]
|
data = data[2 + length :]
|
||||||
options.append((type, value))
|
options.append((value_type, value))
|
||||||
|
|
||||||
return options
|
return options
|
||||||
|
|
||||||
@@ -236,7 +242,8 @@ class L2CAP_Control_Frame:
|
|||||||
cls.code = key_with_value(L2CAP_CONTROL_FRAME_NAMES, cls.name)
|
cls.code = key_with_value(L2CAP_CONTROL_FRAME_NAMES, cls.name)
|
||||||
if cls.code is None:
|
if cls.code is None:
|
||||||
raise KeyError(
|
raise KeyError(
|
||||||
f'Control Frame name {cls.name} not found in L2CAP_CONTROL_FRAME_NAMES'
|
f'Control Frame name {cls.name} '
|
||||||
|
'not found in L2CAP_CONTROL_FRAME_NAMES'
|
||||||
)
|
)
|
||||||
cls.fields = fields
|
cls.fields = fields
|
||||||
|
|
||||||
@@ -281,6 +288,7 @@ class L2CAP_Control_Frame:
|
|||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@L2CAP_Control_Frame.subclass(
|
@L2CAP_Control_Frame.subclass(
|
||||||
|
# pylint: disable=unnecessary-lambda
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
'reason',
|
'reason',
|
||||||
@@ -311,6 +319,7 @@ class L2CAP_Command_Reject(L2CAP_Control_Frame):
|
|||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@L2CAP_Control_Frame.subclass(
|
@L2CAP_Control_Frame.subclass(
|
||||||
|
# pylint: disable=unnecessary-lambda
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
'psm',
|
'psm',
|
||||||
@@ -356,6 +365,7 @@ class L2CAP_Connection_Request(L2CAP_Control_Frame):
|
|||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@L2CAP_Control_Frame.subclass(
|
@L2CAP_Control_Frame.subclass(
|
||||||
|
# pylint: disable=unnecessary-lambda
|
||||||
[
|
[
|
||||||
('destination_cid', 2),
|
('destination_cid', 2),
|
||||||
('source_cid', 2),
|
('source_cid', 2),
|
||||||
@@ -380,6 +390,7 @@ class L2CAP_Connection_Response(L2CAP_Control_Frame):
|
|||||||
CONNECTION_REFUSED_SOURCE_CID_ALREADY_ALLOCATED = 0x0007
|
CONNECTION_REFUSED_SOURCE_CID_ALREADY_ALLOCATED = 0x0007
|
||||||
CONNECTION_REFUSED_UNACCEPTABLE_PARAMETERS = 0x000B
|
CONNECTION_REFUSED_UNACCEPTABLE_PARAMETERS = 0x000B
|
||||||
|
|
||||||
|
# pylint: disable=line-too-long
|
||||||
RESULT_NAMES = {
|
RESULT_NAMES = {
|
||||||
CONNECTION_SUCCESSFUL: 'CONNECTION_SUCCESSFUL',
|
CONNECTION_SUCCESSFUL: 'CONNECTION_SUCCESSFUL',
|
||||||
CONNECTION_PENDING: 'CONNECTION_PENDING',
|
CONNECTION_PENDING: 'CONNECTION_PENDING',
|
||||||
@@ -406,6 +417,7 @@ class L2CAP_Configure_Request(L2CAP_Control_Frame):
|
|||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@L2CAP_Control_Frame.subclass(
|
@L2CAP_Control_Frame.subclass(
|
||||||
|
# pylint: disable=unnecessary-lambda
|
||||||
[
|
[
|
||||||
('source_cid', 2),
|
('source_cid', 2),
|
||||||
('flags', 2),
|
('flags', 2),
|
||||||
@@ -481,6 +493,7 @@ class L2CAP_Echo_Response(L2CAP_Control_Frame):
|
|||||||
'info_type',
|
'info_type',
|
||||||
{
|
{
|
||||||
'size': 2,
|
'size': 2,
|
||||||
|
# pylint: disable-next=unnecessary-lambda
|
||||||
'mapper': lambda x: L2CAP_Information_Request.info_type_name(x),
|
'mapper': lambda x: L2CAP_Information_Request.info_type_name(x),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -524,6 +537,7 @@ class L2CAP_Information_Request(L2CAP_Control_Frame):
|
|||||||
('info_type', {'size': 2, 'mapper': L2CAP_Information_Request.info_type_name}),
|
('info_type', {'size': 2, 'mapper': L2CAP_Information_Request.info_type_name}),
|
||||||
(
|
(
|
||||||
'result',
|
'result',
|
||||||
|
# pylint: disable-next=unnecessary-lambda
|
||||||
{'size': 2, 'mapper': lambda x: L2CAP_Information_Response.result_name(x)},
|
{'size': 2, 'mapper': lambda x: L2CAP_Information_Response.result_name(x)},
|
||||||
),
|
),
|
||||||
('data', '*'),
|
('data', '*'),
|
||||||
@@ -568,12 +582,14 @@ class L2CAP_Connection_Parameter_Update_Response(L2CAP_Control_Frame):
|
|||||||
)
|
)
|
||||||
class L2CAP_LE_Credit_Based_Connection_Request(L2CAP_Control_Frame):
|
class L2CAP_LE_Credit_Based_Connection_Request(L2CAP_Control_Frame):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec @ Vol 3, Part A - 4.22 LE CREDIT BASED CONNECTION REQUEST (CODE 0x14)
|
See Bluetooth spec @ Vol 3, Part A - 4.22 LE CREDIT BASED CONNECTION REQUEST
|
||||||
|
(CODE 0x14)
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@L2CAP_Control_Frame.subclass(
|
@L2CAP_Control_Frame.subclass(
|
||||||
|
# pylint: disable=unnecessary-lambda,line-too-long
|
||||||
[
|
[
|
||||||
('destination_cid', 2),
|
('destination_cid', 2),
|
||||||
('mtu', 2),
|
('mtu', 2),
|
||||||
@@ -592,7 +608,8 @@ class L2CAP_LE_Credit_Based_Connection_Request(L2CAP_Control_Frame):
|
|||||||
)
|
)
|
||||||
class L2CAP_LE_Credit_Based_Connection_Response(L2CAP_Control_Frame):
|
class L2CAP_LE_Credit_Based_Connection_Response(L2CAP_Control_Frame):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec @ Vol 3, Part A - 4.23 LE CREDIT BASED CONNECTION RESPONSE (CODE 0x15)
|
See Bluetooth spec @ Vol 3, Part A - 4.23 LE CREDIT BASED CONNECTION RESPONSE
|
||||||
|
(CODE 0x15)
|
||||||
'''
|
'''
|
||||||
|
|
||||||
CONNECTION_SUCCESSFUL = 0x0000
|
CONNECTION_SUCCESSFUL = 0x0000
|
||||||
@@ -606,6 +623,7 @@ class L2CAP_LE_Credit_Based_Connection_Response(L2CAP_Control_Frame):
|
|||||||
CONNECTION_REFUSED_SOURCE_CID_ALREADY_ALLOCATED = 0x000A
|
CONNECTION_REFUSED_SOURCE_CID_ALREADY_ALLOCATED = 0x000A
|
||||||
CONNECTION_REFUSED_UNACCEPTABLE_PARAMETERS = 0x000B
|
CONNECTION_REFUSED_UNACCEPTABLE_PARAMETERS = 0x000B
|
||||||
|
|
||||||
|
# pylint: disable=line-too-long
|
||||||
RESULT_NAMES = {
|
RESULT_NAMES = {
|
||||||
CONNECTION_SUCCESSFUL: 'CONNECTION_SUCCESSFUL',
|
CONNECTION_SUCCESSFUL: 'CONNECTION_SUCCESSFUL',
|
||||||
CONNECTION_REFUSED_LE_PSM_NOT_SUPPORTED: 'CONNECTION_REFUSED_LE_PSM_NOT_SUPPORTED',
|
CONNECTION_REFUSED_LE_PSM_NOT_SUPPORTED: 'CONNECTION_REFUSED_LE_PSM_NOT_SUPPORTED',
|
||||||
@@ -693,6 +711,7 @@ class Channel(EventEmitter):
|
|||||||
self.destination_cid = 0
|
self.destination_cid = 0
|
||||||
self.response = None
|
self.response = None
|
||||||
self.connection_result = None
|
self.connection_result = None
|
||||||
|
self.disconnection_result = None
|
||||||
self.sink = None
|
self.sink = None
|
||||||
|
|
||||||
def change_state(self, new_state):
|
def change_state(self, new_state):
|
||||||
@@ -723,6 +742,7 @@ class Channel(EventEmitter):
|
|||||||
self.response.set_result(pdu)
|
self.response.set_result(pdu)
|
||||||
self.response = None
|
self.response = None
|
||||||
elif self.sink:
|
elif self.sink:
|
||||||
|
# pylint: disable=not-callable
|
||||||
self.sink(pdu)
|
self.sink(pdu)
|
||||||
else:
|
else:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
@@ -746,7 +766,8 @@ class Channel(EventEmitter):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create a future to wait for the state machine to get to a success or error state
|
# Create a future to wait for the state machine to get to a success or error
|
||||||
|
# state
|
||||||
self.connection_result = asyncio.get_running_loop().create_future()
|
self.connection_result = asyncio.get_running_loop().create_future()
|
||||||
|
|
||||||
# Wait for the connection to succeed or fail
|
# Wait for the connection to succeed or fail
|
||||||
@@ -768,7 +789,8 @@ class Channel(EventEmitter):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create a future to wait for the state machine to get to a success or error state
|
# Create a future to wait for the state machine to get to a success or error
|
||||||
|
# state
|
||||||
self.disconnection_result = asyncio.get_running_loop().create_future()
|
self.disconnection_result = asyncio.get_running_loop().create_future()
|
||||||
return await self.disconnection_result
|
return await self.disconnection_result
|
||||||
|
|
||||||
@@ -830,10 +852,10 @@ class Channel(EventEmitter):
|
|||||||
self.connection_result = None
|
self.connection_result = None
|
||||||
|
|
||||||
def on_configure_request(self, request):
|
def on_configure_request(self, request):
|
||||||
if (
|
if self.state not in (
|
||||||
self.state != Channel.WAIT_CONFIG
|
Channel.WAIT_CONFIG,
|
||||||
and self.state != Channel.WAIT_CONFIG_REQ
|
Channel.WAIT_CONFIG_REQ,
|
||||||
and self.state != Channel.WAIT_CONFIG_REQ_RSP
|
Channel.WAIT_CONFIG_REQ_RSP,
|
||||||
):
|
):
|
||||||
logger.warning(color('invalid state', 'red'))
|
logger.warning(color('invalid state', 'red'))
|
||||||
return
|
return
|
||||||
@@ -871,10 +893,7 @@ class Channel(EventEmitter):
|
|||||||
if response.result == L2CAP_Configure_Response.SUCCESS:
|
if response.result == L2CAP_Configure_Response.SUCCESS:
|
||||||
if self.state == Channel.WAIT_CONFIG_REQ_RSP:
|
if self.state == Channel.WAIT_CONFIG_REQ_RSP:
|
||||||
self.change_state(Channel.WAIT_CONFIG_REQ)
|
self.change_state(Channel.WAIT_CONFIG_REQ)
|
||||||
elif (
|
elif self.state in (Channel.WAIT_CONFIG_RSP, Channel.WAIT_CONTROL_IND):
|
||||||
self.state == Channel.WAIT_CONFIG_RSP
|
|
||||||
or self.state == Channel.WAIT_CONTROL_IND
|
|
||||||
):
|
|
||||||
self.change_state(Channel.OPEN)
|
self.change_state(Channel.OPEN)
|
||||||
if self.connection_result:
|
if self.connection_result:
|
||||||
self.connection_result.set_result(None)
|
self.connection_result.set_result(None)
|
||||||
@@ -897,14 +916,15 @@ class Channel(EventEmitter):
|
|||||||
else:
|
else:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
color(
|
color(
|
||||||
f'!!! configuration rejected: {L2CAP_Configure_Response.result_name(response.result)}',
|
'!!! configuration rejected: '
|
||||||
|
f'{L2CAP_Configure_Response.result_name(response.result)}',
|
||||||
'red',
|
'red',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
# TODO: decide how to fail gracefully
|
# TODO: decide how to fail gracefully
|
||||||
|
|
||||||
def on_disconnection_request(self, request):
|
def on_disconnection_request(self, request):
|
||||||
if self.state == Channel.OPEN or self.state == Channel.WAIT_DISCONNECT:
|
if self.state in (Channel.OPEN, Channel.WAIT_DISCONNECT):
|
||||||
self.send_control_frame(
|
self.send_control_frame(
|
||||||
L2CAP_Disconnection_Response(
|
L2CAP_Disconnection_Response(
|
||||||
identifier=request.identifier,
|
identifier=request.identifier,
|
||||||
@@ -938,7 +958,12 @@ class Channel(EventEmitter):
|
|||||||
self.manager.on_channel_closed(self)
|
self.manager.on_channel_closed(self)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'Channel({self.source_cid}->{self.destination_cid}, PSM={self.psm}, MTU={self.mtu}, state={Channel.STATE_NAMES[self.state]})'
|
return (
|
||||||
|
f'Channel({self.source_cid}->{self.destination_cid}, '
|
||||||
|
f'PSM={self.psm}, '
|
||||||
|
f'MTU={self.mtu}, '
|
||||||
|
f'state={Channel.STATE_NAMES[self.state]})'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -976,7 +1001,7 @@ class LeConnectionOrientedChannel(EventEmitter):
|
|||||||
destination_cid,
|
destination_cid,
|
||||||
mtu,
|
mtu,
|
||||||
mps,
|
mps,
|
||||||
credits,
|
credits, # pylint: disable=redefined-builtin
|
||||||
peer_mtu,
|
peer_mtu,
|
||||||
peer_mps,
|
peer_mps,
|
||||||
peer_credits,
|
peer_credits,
|
||||||
@@ -1001,6 +1026,7 @@ class LeConnectionOrientedChannel(EventEmitter):
|
|||||||
self.out_queue = deque()
|
self.out_queue = deque()
|
||||||
self.out_sdu = None
|
self.out_sdu = None
|
||||||
self.sink = None
|
self.sink = None
|
||||||
|
self.connected = False
|
||||||
self.connection_result = None
|
self.connection_result = None
|
||||||
self.disconnection_result = None
|
self.disconnection_result = None
|
||||||
self.drained = asyncio.Event()
|
self.drained = asyncio.Event()
|
||||||
@@ -1072,7 +1098,8 @@ class LeConnectionOrientedChannel(EventEmitter):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create a future to wait for the state machine to get to a success or error state
|
# Create a future to wait for the state machine to get to a success or error
|
||||||
|
# state
|
||||||
self.disconnection_result = asyncio.get_running_loop().create_future()
|
self.disconnection_result = asyncio.get_running_loop().create_future()
|
||||||
return await self.disconnection_result
|
return await self.disconnection_result
|
||||||
|
|
||||||
@@ -1110,7 +1137,8 @@ class LeConnectionOrientedChannel(EventEmitter):
|
|||||||
|
|
||||||
# Check if the SDU is complete
|
# Check if the SDU is complete
|
||||||
if self.in_sdu_length == 0:
|
if self.in_sdu_length == 0:
|
||||||
# We don't know the size yet, check if we have received the header to compute it
|
# We don't know the size yet, check if we have received the header to
|
||||||
|
# compute it
|
||||||
if len(self.in_sdu) >= 2:
|
if len(self.in_sdu) >= 2:
|
||||||
self.in_sdu_length = struct.unpack_from('<H', self.in_sdu, 0)[0]
|
self.in_sdu_length = struct.unpack_from('<H', self.in_sdu, 0)[0]
|
||||||
if self.in_sdu_length == 0:
|
if self.in_sdu_length == 0:
|
||||||
@@ -1125,7 +1153,8 @@ class LeConnectionOrientedChannel(EventEmitter):
|
|||||||
if len(self.in_sdu) != 2 + self.in_sdu_length:
|
if len(self.in_sdu) != 2 + self.in_sdu_length:
|
||||||
# Overflow
|
# Overflow
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f'SDU overflow: sdu_length={self.in_sdu_length}, received {len(self.in_sdu) - 2}'
|
f'SDU overflow: sdu_length={self.in_sdu_length}, '
|
||||||
|
f'received {len(self.in_sdu) - 2}'
|
||||||
)
|
)
|
||||||
# TODO: we should disconnect
|
# TODO: we should disconnect
|
||||||
self.in_sdu = None
|
self.in_sdu = None
|
||||||
@@ -1134,7 +1163,7 @@ class LeConnectionOrientedChannel(EventEmitter):
|
|||||||
|
|
||||||
# Send the SDU to the sink
|
# Send the SDU to the sink
|
||||||
logger.debug(f'SDU complete: 2+{len(self.in_sdu) - 2} bytes')
|
logger.debug(f'SDU complete: 2+{len(self.in_sdu) - 2} bytes')
|
||||||
self.sink(self.in_sdu[2:])
|
self.sink(self.in_sdu[2:]) # pylint: disable=not-callable
|
||||||
|
|
||||||
# Prepare for a new SDU
|
# Prepare for a new SDU
|
||||||
self.in_sdu = None
|
self.in_sdu = None
|
||||||
@@ -1174,7 +1203,7 @@ class LeConnectionOrientedChannel(EventEmitter):
|
|||||||
# Cleanup
|
# Cleanup
|
||||||
self.connection_result = None
|
self.connection_result = None
|
||||||
|
|
||||||
def on_credits(self, credits):
|
def on_credits(self, credits): # pylint: disable=redefined-builtin
|
||||||
self.credits += credits
|
self.credits += credits
|
||||||
logger.debug(f'received {credits} credits, total = {self.credits}')
|
logger.debug(f'received {credits} credits, total = {self.credits}')
|
||||||
|
|
||||||
@@ -1228,7 +1257,8 @@ class LeConnectionOrientedChannel(EventEmitter):
|
|||||||
# Keep what's still left to send
|
# Keep what's still left to send
|
||||||
self.out_sdu = self.out_sdu[len(packet) :]
|
self.out_sdu = self.out_sdu[len(packet) :]
|
||||||
continue
|
continue
|
||||||
elif self.out_queue:
|
|
||||||
|
if self.out_queue:
|
||||||
# Create the next SDU (2 bytes header plus up to MTU bytes payload)
|
# Create the next SDU (2 bytes header plus up to MTU bytes payload)
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'assembling SDU from {len(self.out_queue)} packets in output queue'
|
f'assembling SDU from {len(self.out_queue)} packets in output queue'
|
||||||
@@ -1282,13 +1312,20 @@ class LeConnectionOrientedChannel(EventEmitter):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'CoC({self.source_cid}->{self.destination_cid}, State={self.state_name(self.state)}, PSM={self.le_psm}, MTU={self.mtu}/{self.peer_mtu}, MPS={self.mps}/{self.peer_mps}, credits={self.credits}/{self.peer_credits})'
|
return (
|
||||||
|
f'CoC({self.source_cid}->{self.destination_cid}, '
|
||||||
|
f'State={self.state_name(self.state)}, '
|
||||||
|
f'PSM={self.le_psm}, '
|
||||||
|
f'MTU={self.mtu}/{self.peer_mtu}, '
|
||||||
|
f'MPS={self.mps}/{self.peer_mps}, '
|
||||||
|
f'credits={self.credits}/{self.peer_credits})'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class ChannelManager:
|
class ChannelManager:
|
||||||
def __init__(
|
def __init__(
|
||||||
self, extended_features=[], connectionless_mtu=L2CAP_DEFAULT_CONNECTIONLESS_MTU
|
self, extended_features=(), connectionless_mtu=L2CAP_DEFAULT_CONNECTIONLESS_MTU
|
||||||
):
|
):
|
||||||
self._host = None
|
self._host = None
|
||||||
self.identifiers = {} # Incrementing identifier values by connection
|
self.identifiers = {} # Incrementing identifier values by connection
|
||||||
@@ -1322,10 +1359,14 @@ class ChannelManager:
|
|||||||
if connection_channels := self.channels.get(connection_handle):
|
if connection_channels := self.channels.get(connection_handle):
|
||||||
return connection_channels.get(cid)
|
return connection_channels.get(cid)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def find_le_coc_channel(self, connection_handle, cid):
|
def find_le_coc_channel(self, connection_handle, cid):
|
||||||
if connection_channels := self.le_coc_channels.get(connection_handle):
|
if connection_channels := self.le_coc_channels.get(connection_handle):
|
||||||
return connection_channels.get(cid)
|
return connection_channels.get(cid)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def find_free_br_edr_cid(channels):
|
def find_free_br_edr_cid(channels):
|
||||||
# Pick the smallest valid CID that's not already in the list
|
# Pick the smallest valid CID that's not already in the list
|
||||||
@@ -1337,6 +1378,8 @@ class ChannelManager:
|
|||||||
if cid not in channels:
|
if cid not in channels:
|
||||||
return cid
|
return cid
|
||||||
|
|
||||||
|
raise RuntimeError('no free CID available')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def find_free_le_cid(channels):
|
def find_free_le_cid(channels):
|
||||||
# Pick the smallest valid CID that's not already in the list
|
# Pick the smallest valid CID that's not already in the list
|
||||||
@@ -1348,6 +1391,8 @@ class ChannelManager:
|
|||||||
if cid not in channels:
|
if cid not in channels:
|
||||||
return cid
|
return cid
|
||||||
|
|
||||||
|
raise RuntimeError('no free CID')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def check_le_coc_parameters(max_credits, mtu, mps):
|
def check_le_coc_parameters(max_credits, mtu, mps):
|
||||||
if (
|
if (
|
||||||
@@ -1442,7 +1487,7 @@ class ChannelManager:
|
|||||||
|
|
||||||
return psm
|
return psm
|
||||||
|
|
||||||
def on_disconnection(self, connection_handle, reason):
|
def on_disconnection(self, connection_handle, _reason):
|
||||||
logger.debug(f'disconnection from {connection_handle}, cleaning up channels')
|
logger.debug(f'disconnection from {connection_handle}, cleaning up channels')
|
||||||
if connection_handle in self.channels:
|
if connection_handle in self.channels:
|
||||||
del self.channels[connection_handle]
|
del self.channels[connection_handle]
|
||||||
@@ -1452,14 +1497,16 @@ class ChannelManager:
|
|||||||
del self.identifiers[connection_handle]
|
del self.identifiers[connection_handle]
|
||||||
|
|
||||||
def send_pdu(self, connection, cid, pdu):
|
def send_pdu(self, connection, cid, pdu):
|
||||||
pdu_str = pdu.hex() if type(pdu) is bytes else str(pdu)
|
pdu_str = pdu.hex() if isinstance(pdu, bytes) else str(pdu)
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'{color(">>> Sending L2CAP PDU", "blue")} on connection [0x{connection.handle:04X}] (CID={cid}) {connection.peer_address}: {pdu_str}'
|
f'{color(">>> Sending L2CAP PDU", "blue")} '
|
||||||
|
f'on connection [0x{connection.handle:04X}] (CID={cid}) '
|
||||||
|
f'{connection.peer_address}: {pdu_str}'
|
||||||
)
|
)
|
||||||
self.host.send_l2cap_pdu(connection.handle, cid, bytes(pdu))
|
self.host.send_l2cap_pdu(connection.handle, cid, bytes(pdu))
|
||||||
|
|
||||||
def on_pdu(self, connection, cid, pdu):
|
def on_pdu(self, connection, cid, pdu):
|
||||||
if cid == L2CAP_SIGNALING_CID or cid == L2CAP_LE_SIGNALING_CID:
|
if cid in (L2CAP_SIGNALING_CID, L2CAP_LE_SIGNALING_CID):
|
||||||
# Parse the L2CAP payload into a Control Frame object
|
# Parse the L2CAP payload into a Control Frame object
|
||||||
control_frame = L2CAP_Control_Frame.from_bytes(pdu)
|
control_frame = L2CAP_Control_Frame.from_bytes(pdu)
|
||||||
|
|
||||||
@@ -1479,13 +1526,17 @@ class ChannelManager:
|
|||||||
|
|
||||||
def send_control_frame(self, connection, cid, control_frame):
|
def send_control_frame(self, connection, cid, control_frame):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'{color(">>> Sending L2CAP Signaling Control Frame", "blue")} on connection [0x{connection.handle:04X}] (CID={cid}) {connection.peer_address}:\n{control_frame}'
|
f'{color(">>> Sending L2CAP Signaling Control Frame", "blue")} '
|
||||||
|
f'on connection [0x{connection.handle:04X}] (CID={cid}) '
|
||||||
|
f'{connection.peer_address}:\n{control_frame}'
|
||||||
)
|
)
|
||||||
self.host.send_l2cap_pdu(connection.handle, cid, bytes(control_frame))
|
self.host.send_l2cap_pdu(connection.handle, cid, bytes(control_frame))
|
||||||
|
|
||||||
def on_control_frame(self, connection, cid, control_frame):
|
def on_control_frame(self, connection, cid, control_frame):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'{color("<<< Received L2CAP Signaling Control Frame", "green")} on connection [0x{connection.handle:04X}] (CID={cid}) {connection.peer_address}:\n{control_frame}'
|
f'{color("<<< Received L2CAP Signaling Control Frame", "green")} '
|
||||||
|
f'on connection [0x{connection.handle:04X}] (CID={cid}) '
|
||||||
|
f'{connection.peer_address}:\n{control_frame}'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Find the handler method
|
# Find the handler method
|
||||||
@@ -1518,7 +1569,7 @@ class ChannelManager:
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_l2cap_command_reject(self, connection, cid, packet):
|
def on_l2cap_command_reject(self, _connection, _cid, packet):
|
||||||
logger.warning(f'{color("!!! Command rejected:", "red")} {packet.reason}')
|
logger.warning(f'{color("!!! Command rejected:", "red")} {packet.reason}')
|
||||||
|
|
||||||
def on_l2cap_connection_request(self, connection, cid, request):
|
def on_l2cap_connection_request(self, connection, cid, request):
|
||||||
@@ -1536,6 +1587,7 @@ class ChannelManager:
|
|||||||
identifier=request.identifier,
|
identifier=request.identifier,
|
||||||
destination_cid=request.source_cid,
|
destination_cid=request.source_cid,
|
||||||
source_cid=0,
|
source_cid=0,
|
||||||
|
# pylint: disable=line-too-long
|
||||||
result=L2CAP_Connection_Response.CONNECTION_REFUSED_NO_RESOURCES_AVAILABLE,
|
result=L2CAP_Connection_Response.CONNECTION_REFUSED_NO_RESOURCES_AVAILABLE,
|
||||||
status=0x0000,
|
status=0x0000,
|
||||||
),
|
),
|
||||||
@@ -1556,7 +1608,8 @@ class ChannelManager:
|
|||||||
channel.on_connection_request(request)
|
channel.on_connection_request(request)
|
||||||
else:
|
else:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f'No server for connection 0x{connection.handle:04X} on PSM {request.psm}'
|
f'No server for connection 0x{connection.handle:04X} '
|
||||||
|
f'on PSM {request.psm}'
|
||||||
)
|
)
|
||||||
self.send_control_frame(
|
self.send_control_frame(
|
||||||
connection,
|
connection,
|
||||||
@@ -1565,6 +1618,7 @@ class ChannelManager:
|
|||||||
identifier=request.identifier,
|
identifier=request.identifier,
|
||||||
destination_cid=request.source_cid,
|
destination_cid=request.source_cid,
|
||||||
source_cid=0,
|
source_cid=0,
|
||||||
|
# pylint: disable=line-too-long
|
||||||
result=L2CAP_Connection_Response.CONNECTION_REFUSED_LE_PSM_NOT_SUPPORTED,
|
result=L2CAP_Connection_Response.CONNECTION_REFUSED_LE_PSM_NOT_SUPPORTED,
|
||||||
status=0x0000,
|
status=0x0000,
|
||||||
),
|
),
|
||||||
@@ -1576,7 +1630,8 @@ class ChannelManager:
|
|||||||
) is None:
|
) is None:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
color(
|
color(
|
||||||
f'channel {response.source_cid} not found for 0x{connection.handle:04X}:{cid}',
|
f'channel {response.source_cid} not found for '
|
||||||
|
f'0x{connection.handle:04X}:{cid}',
|
||||||
'red',
|
'red',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -1590,7 +1645,8 @@ class ChannelManager:
|
|||||||
) is None:
|
) is None:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
color(
|
color(
|
||||||
f'channel {request.destination_cid} not found for 0x{connection.handle:04X}:{cid}',
|
f'channel {request.destination_cid} not found for '
|
||||||
|
f'0x{connection.handle:04X}:{cid}',
|
||||||
'red',
|
'red',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -1604,7 +1660,8 @@ class ChannelManager:
|
|||||||
) is None:
|
) is None:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
color(
|
color(
|
||||||
f'channel {response.source_cid} not found for 0x{connection.handle:04X}:{cid}',
|
f'channel {response.source_cid} not found for '
|
||||||
|
f'0x{connection.handle:04X}:{cid}',
|
||||||
'red',
|
'red',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -1618,7 +1675,8 @@ class ChannelManager:
|
|||||||
) is None:
|
) is None:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
color(
|
color(
|
||||||
f'channel {request.destination_cid} not found for 0x{connection.handle:04X}:{cid}',
|
f'channel {request.destination_cid} not found for '
|
||||||
|
f'0x{connection.handle:04X}:{cid}',
|
||||||
'red',
|
'red',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -1632,7 +1690,8 @@ class ChannelManager:
|
|||||||
) is None:
|
) is None:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
color(
|
color(
|
||||||
f'channel {response.source_cid} not found for 0x{connection.handle:04X}:{cid}',
|
f'channel {response.source_cid} not found for '
|
||||||
|
f'0x{connection.handle:04X}:{cid}',
|
||||||
'red',
|
'red',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -1648,7 +1707,7 @@ class ChannelManager:
|
|||||||
L2CAP_Echo_Response(identifier=request.identifier, data=request.data),
|
L2CAP_Echo_Response(identifier=request.identifier, data=request.data),
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_l2cap_echo_response(self, connection, cid, response):
|
def on_l2cap_echo_response(self, _connection, _cid, response):
|
||||||
logger.debug(f'<<< Echo response: data={response.data.hex()}')
|
logger.debug(f'<<< Echo response: data={response.data.hex()}')
|
||||||
# TODO notify listeners
|
# TODO notify listeners
|
||||||
|
|
||||||
@@ -1663,7 +1722,7 @@ class ChannelManager:
|
|||||||
result = L2CAP_Information_Response.SUCCESS
|
result = L2CAP_Information_Response.SUCCESS
|
||||||
data = sum(1 << cid for cid in self.fixed_channels).to_bytes(8, 'little')
|
data = sum(1 << cid for cid in self.fixed_channels).to_bytes(8, 'little')
|
||||||
else:
|
else:
|
||||||
result = L2CAP_Information_Request.NO_SUPPORTED
|
result = L2CAP_Information_Response.NOT_SUPPORTED
|
||||||
|
|
||||||
self.send_control_frame(
|
self.send_control_frame(
|
||||||
connection,
|
connection,
|
||||||
@@ -1730,6 +1789,7 @@ class ChannelManager:
|
|||||||
mtu=mtu,
|
mtu=mtu,
|
||||||
mps=mps,
|
mps=mps,
|
||||||
initial_credits=0,
|
initial_credits=0,
|
||||||
|
# pylint: disable=line-too-long
|
||||||
result=L2CAP_LE_Credit_Based_Connection_Response.CONNECTION_REFUSED_SOURCE_CID_ALREADY_ALLOCATED,
|
result=L2CAP_LE_Credit_Based_Connection_Response.CONNECTION_REFUSED_SOURCE_CID_ALREADY_ALLOCATED,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -1748,6 +1808,7 @@ class ChannelManager:
|
|||||||
mtu=mtu,
|
mtu=mtu,
|
||||||
mps=mps,
|
mps=mps,
|
||||||
initial_credits=0,
|
initial_credits=0,
|
||||||
|
# pylint: disable=line-too-long
|
||||||
result=L2CAP_LE_Credit_Based_Connection_Response.CONNECTION_REFUSED_NO_RESOURCES_AVAILABLE,
|
result=L2CAP_LE_Credit_Based_Connection_Response.CONNECTION_REFUSED_NO_RESOURCES_AVAILABLE,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -1755,7 +1816,8 @@ class ChannelManager:
|
|||||||
|
|
||||||
# Create a new channel
|
# Create a new channel
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'creating LE CoC server channel with cid={source_cid} for psm {request.le_psm}'
|
f'creating LE CoC server channel with cid={source_cid} for psm '
|
||||||
|
f'{request.le_psm}'
|
||||||
)
|
)
|
||||||
channel = LeConnectionOrientedChannel(
|
channel = LeConnectionOrientedChannel(
|
||||||
self,
|
self,
|
||||||
@@ -1784,6 +1846,7 @@ class ChannelManager:
|
|||||||
mtu=mtu,
|
mtu=mtu,
|
||||||
mps=mps,
|
mps=mps,
|
||||||
initial_credits=max_credits,
|
initial_credits=max_credits,
|
||||||
|
# pylint: disable=line-too-long
|
||||||
result=L2CAP_LE_Credit_Based_Connection_Response.CONNECTION_SUCCESSFUL,
|
result=L2CAP_LE_Credit_Based_Connection_Response.CONNECTION_SUCCESSFUL,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -1792,7 +1855,8 @@ class ChannelManager:
|
|||||||
server(channel)
|
server(channel)
|
||||||
else:
|
else:
|
||||||
logger.info(
|
logger.info(
|
||||||
f'No LE server for connection 0x{connection.handle:04X} on PSM {request.le_psm}'
|
f'No LE server for connection 0x{connection.handle:04X} '
|
||||||
|
f'on PSM {request.le_psm}'
|
||||||
)
|
)
|
||||||
self.send_control_frame(
|
self.send_control_frame(
|
||||||
connection,
|
connection,
|
||||||
@@ -1803,11 +1867,12 @@ class ChannelManager:
|
|||||||
mtu=L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MTU,
|
mtu=L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MTU,
|
||||||
mps=L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MPS,
|
mps=L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MPS,
|
||||||
initial_credits=0,
|
initial_credits=0,
|
||||||
|
# pylint: disable=line-too-long
|
||||||
result=L2CAP_LE_Credit_Based_Connection_Response.CONNECTION_REFUSED_LE_PSM_NOT_SUPPORTED,
|
result=L2CAP_LE_Credit_Based_Connection_Response.CONNECTION_REFUSED_LE_PSM_NOT_SUPPORTED,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_l2cap_le_credit_based_connection_response(self, connection, cid, response):
|
def on_l2cap_le_credit_based_connection_response(self, connection, _cid, response):
|
||||||
# Find the pending request by identifier
|
# Find the pending request by identifier
|
||||||
request = self.le_coc_requests.get(response.identifier)
|
request = self.le_coc_requests.get(response.identifier)
|
||||||
if request is None:
|
if request is None:
|
||||||
@@ -1820,7 +1885,8 @@ class ChannelManager:
|
|||||||
if channel is None:
|
if channel is None:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
color(
|
color(
|
||||||
f'received connection response for an unknown channel (cid={request.source_cid})',
|
'received connection response for an unknown channel '
|
||||||
|
f'(cid={request.source_cid})',
|
||||||
'red',
|
'red',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -1829,7 +1895,7 @@ class ChannelManager:
|
|||||||
# Process the response
|
# Process the response
|
||||||
channel.on_connection_response(response)
|
channel.on_connection_response(response)
|
||||||
|
|
||||||
def on_l2cap_le_flow_control_credit(self, connection, cid, credit):
|
def on_l2cap_le_flow_control_credit(self, connection, _cid, credit):
|
||||||
channel = self.find_le_coc_channel(connection.handle, credit.cid)
|
channel = self.find_le_coc_channel(connection.handle, credit.cid)
|
||||||
if channel is None:
|
if channel is None:
|
||||||
logger.warning(f'received credits for an unknown channel (cid={credit.cid}')
|
logger.warning(f'received credits for an unknown channel (cid={credit.cid}')
|
||||||
|
|||||||
+26
-21
@@ -17,9 +17,10 @@
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
import logging
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
import websockets
|
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from colors import color
|
from colors import color
|
||||||
|
import websockets
|
||||||
|
|
||||||
from bumble.hci import (
|
from bumble.hci import (
|
||||||
Address,
|
Address,
|
||||||
@@ -47,7 +48,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:
|
class LocalLink:
|
||||||
'''
|
'''
|
||||||
@@ -119,7 +121,8 @@ class LocalLink:
|
|||||||
|
|
||||||
def connect(self, central_address, le_create_connection_command):
|
def connect(self, central_address, le_create_connection_command):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'$$$ CONNECTION {central_address} -> {le_create_connection_command.peer_address}'
|
f'$$$ CONNECTION {central_address} -> '
|
||||||
|
f'{le_create_connection_command.peer_address}'
|
||||||
)
|
)
|
||||||
self.pending_connection = (central_address, le_create_connection_command)
|
self.pending_connection = (central_address, le_create_connection_command)
|
||||||
asyncio.get_running_loop().call_soon(self.on_connection_complete)
|
asyncio.get_running_loop().call_soon(self.on_connection_complete)
|
||||||
@@ -144,11 +147,13 @@ class LocalLink:
|
|||||||
|
|
||||||
def disconnect(self, central_address, peripheral_address, disconnect_command):
|
def disconnect(self, central_address, peripheral_address, disconnect_command):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'$$$ DISCONNECTION {central_address} -> {peripheral_address}: reason = {disconnect_command.reason}'
|
f'$$$ DISCONNECTION {central_address} -> '
|
||||||
|
f'{peripheral_address}: reason = {disconnect_command.reason}'
|
||||||
)
|
)
|
||||||
args = [central_address, peripheral_address, disconnect_command]
|
args = [central_address, peripheral_address, disconnect_command]
|
||||||
asyncio.get_running_loop().call_soon(self.on_disconnection_complete, *args)
|
asyncio.get_running_loop().call_soon(self.on_disconnection_complete, *args)
|
||||||
|
|
||||||
|
# pylint: disable=too-many-arguments
|
||||||
def on_connection_encrypted(
|
def on_connection_encrypted(
|
||||||
self, central_address, peripheral_address, rand, ediv, ltk
|
self, central_address, peripheral_address, rand, ediv, ltk
|
||||||
):
|
):
|
||||||
@@ -217,6 +222,7 @@ class RemoteLink:
|
|||||||
async def run_connection(self):
|
async def run_connection(self):
|
||||||
# Connect to the relay
|
# Connect to the relay
|
||||||
logger.debug(f'connecting to {self.uri}')
|
logger.debug(f'connecting to {self.uri}')
|
||||||
|
# pylint: disable-next=no-member
|
||||||
websocket = await websockets.connect(self.uri)
|
websocket = await websockets.connect(self.uri)
|
||||||
self.websocket.set_result(websocket)
|
self.websocket.set_result(websocket)
|
||||||
logger.debug(f'connected to {self.uri}')
|
logger.debug(f'connected to {self.uri}')
|
||||||
@@ -287,11 +293,11 @@ class RemoteLink:
|
|||||||
self.controller.on_link_central_connected(Address(sender))
|
self.controller.on_link_central_connected(Address(sender))
|
||||||
|
|
||||||
# Accept the connection by responding to it
|
# 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, _):
|
async def on_connected_message_received(self, sender, _):
|
||||||
if not self.pending_connection:
|
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
|
return
|
||||||
|
|
||||||
# Remember the connection
|
# Remember the connection
|
||||||
@@ -313,7 +319,7 @@ class RemoteLink:
|
|||||||
if sender in self.peripheral_connections:
|
if sender in self.peripheral_connections:
|
||||||
self.peripheral_connections.remove(sender)
|
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
|
# TODO parse params to get real args
|
||||||
self.controller.on_link_encrypted(Address(sender), bytes(8), 0, bytes(16))
|
self.controller.on_link_encrypted(Address(sender), bytes(8), 0, bytes(16))
|
||||||
|
|
||||||
@@ -335,7 +341,7 @@ class RemoteLink:
|
|||||||
|
|
||||||
# TODO: parse the result
|
# 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
|
# Ensure we have a connection
|
||||||
websocket = await self.websocket
|
websocket = await self.websocket
|
||||||
|
|
||||||
@@ -352,23 +358,23 @@ class RemoteLink:
|
|||||||
self.execute(self.notify_address_changed)
|
self.execute(self.notify_address_changed)
|
||||||
|
|
||||||
async def send_advertising_data_to_relay(self, data):
|
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))
|
self.execute(partial(self.send_advertising_data_to_relay, data))
|
||||||
|
|
||||||
async def send_acl_data_to_relay(self, peer_address, 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, data):
|
||||||
self.execute(partial(self.send_acl_data_to_relay, peer_address, data))
|
self.execute(partial(self.send_acl_data_to_relay, peer_address, data))
|
||||||
|
|
||||||
async def send_connection_request_to_relay(self, peer_address):
|
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:
|
if self.pending_connection:
|
||||||
logger.warn('connection already pending')
|
logger.warning('connection already pending')
|
||||||
return
|
return
|
||||||
self.pending_connection = le_create_connection_command
|
self.pending_connection = le_create_connection_command
|
||||||
self.execute(
|
self.execute(
|
||||||
@@ -385,11 +391,12 @@ class RemoteLink:
|
|||||||
|
|
||||||
def disconnect(self, central_address, peripheral_address, disconnect_command):
|
def disconnect(self, central_address, peripheral_address, disconnect_command):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'disconnect {central_address} -> {peripheral_address}: reason = {disconnect_command.reason}'
|
f'disconnect {central_address} -> '
|
||||||
|
f'{peripheral_address}: reason = {disconnect_command.reason}'
|
||||||
)
|
)
|
||||||
self.execute(
|
self.execute(
|
||||||
partial(
|
partial(
|
||||||
self.send_targetted_message,
|
self.send_targeted_message,
|
||||||
peripheral_address,
|
peripheral_address,
|
||||||
f'disconnect:reason={disconnect_command.reason}',
|
f'disconnect:reason={disconnect_command.reason}',
|
||||||
)
|
)
|
||||||
@@ -398,15 +405,13 @@ class RemoteLink:
|
|||||||
self.on_disconnection_complete, disconnect_command
|
self.on_disconnection_complete, disconnect_command
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_connection_encrypted(
|
def on_connection_encrypted(self, _, peripheral_address, rand, ediv, ltk):
|
||||||
self, central_address, peripheral_address, rand, ediv, ltk
|
|
||||||
):
|
|
||||||
asyncio.get_running_loop().call_soon(
|
asyncio.get_running_loop().call_soon(
|
||||||
self.controller.on_link_encrypted, peripheral_address, rand, ediv, ltk
|
self.controller.on_link_encrypted, peripheral_address, rand, ediv, ltk
|
||||||
)
|
)
|
||||||
self.execute(
|
self.execute(
|
||||||
partial(
|
partial(
|
||||||
self.send_targetted_message,
|
self.send_targeted_message,
|
||||||
peripheral_address,
|
peripheral_address,
|
||||||
f'encrypted:ltk={ltk.hex()}',
|
f'encrypted:ltk={ltk.hex()}',
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
import struct
|
import struct
|
||||||
import logging
|
import logging
|
||||||
|
from typing import List
|
||||||
from ..core import AdvertisingData
|
from ..core import AdvertisingData
|
||||||
from ..gatt import (
|
from ..gatt import (
|
||||||
GATT_ASHA_SERVICE,
|
GATT_ASHA_SERVICE,
|
||||||
@@ -29,7 +30,6 @@ from ..gatt import (
|
|||||||
TemplateService,
|
TemplateService,
|
||||||
Characteristic,
|
Characteristic,
|
||||||
CharacteristicValue,
|
CharacteristicValue,
|
||||||
PackedCharacteristicAdapter,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -50,23 +50,26 @@ class AshaService(TemplateService):
|
|||||||
SUPPORTED_CODEC_ID = [0x02, 0x01] # Codec IDs [G.722 at 16 kHz]
|
SUPPORTED_CODEC_ID = [0x02, 0x01] # Codec IDs [G.722 at 16 kHz]
|
||||||
RENDER_DELAY = [00, 00]
|
RENDER_DELAY = [00, 00]
|
||||||
|
|
||||||
def __init__(self, capability: int, hisyncid: [int]):
|
def __init__(self, capability: int, hisyncid: List[int]):
|
||||||
self.hisyncid = hisyncid
|
self.hisyncid = hisyncid
|
||||||
self.capability = capability # Device Capabilities [Left, Monaural]
|
self.capability = capability # Device Capabilities [Left, Monaural]
|
||||||
|
|
||||||
# Handler for volume control
|
# Handler for volume control
|
||||||
def on_volume_write(connection, value):
|
def on_volume_write(_connection, value):
|
||||||
logger.info(f'--- VOLUME Write:{value[0]}')
|
logger.info(f'--- VOLUME Write:{value[0]}')
|
||||||
|
|
||||||
# Handler for audio control commands
|
# Handler for audio control commands
|
||||||
def on_audio_control_point_write(connection, value):
|
def on_audio_control_point_write(_connection, value):
|
||||||
logger.info(f'--- AUDIO CONTROL POINT Write:{value.hex()}')
|
logger.info(f'--- AUDIO CONTROL POINT Write:{value.hex()}')
|
||||||
opcode = value[0]
|
opcode = value[0]
|
||||||
if opcode == AshaService.OPCODE_START:
|
if opcode == AshaService.OPCODE_START:
|
||||||
# Start
|
# Start
|
||||||
audio_type = ('Unknown', 'Ringtone', 'Phone Call', 'Media')[value[2]]
|
audio_type = ('Unknown', 'Ringtone', 'Phone Call', 'Media')[value[2]]
|
||||||
logger.info(
|
logger.info(
|
||||||
f'### START: codec={value[1]}, audio_type={audio_type}, volume={value[3]}, otherstate={value[4]}'
|
f'### START: codec={value[1]}, '
|
||||||
|
f'audio_type={audio_type}, '
|
||||||
|
f'volume={value[3]}, '
|
||||||
|
f'otherstate={value[4]}'
|
||||||
)
|
)
|
||||||
elif opcode == AshaService.OPCODE_STOP:
|
elif opcode == AshaService.OPCODE_STOP:
|
||||||
logger.info('### STOP')
|
logger.info('### STOP')
|
||||||
@@ -74,7 +77,8 @@ class AshaService(TemplateService):
|
|||||||
logger.info(f'### STATUS: connected={value[1]}')
|
logger.info(f'### STATUS: connected={value[1]}')
|
||||||
|
|
||||||
# TODO Respond with a status
|
# TODO Respond with a status
|
||||||
# asyncio.create_task(device.notify_subscribers(audio_status_characteristic, force=True))
|
# asyncio.create_task(device.notify_subscribers(audio_status_characteristic,
|
||||||
|
# force=True))
|
||||||
|
|
||||||
self.read_only_properties_characteristic = Characteristic(
|
self.read_only_properties_characteristic = Characteristic(
|
||||||
GATT_ASHA_READ_ONLY_PROPERTIES_CHARACTERISTIC,
|
GATT_ASHA_READ_ONLY_PROPERTIES_CHARACTERISTIC,
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class BatteryService(TemplateService):
|
|||||||
Characteristic.READABLE,
|
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])
|
super().__init__([self.battery_level_characteristic])
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ class BatteryServiceProxy(ProfileServiceProxy):
|
|||||||
GATT_BATTERY_LEVEL_CHARACTERISTIC
|
GATT_BATTERY_LEVEL_CHARACTERISTIC
|
||||||
):
|
):
|
||||||
self.battery_level = PackedCharacteristicAdapter(
|
self.battery_level = PackedCharacteristicAdapter(
|
||||||
characteristics[0], format=BatteryService.BATTERY_LEVEL_FORMAT
|
characteristics[0], pack_format=BatteryService.BATTERY_LEVEL_FORMAT
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.battery_level = None
|
self.battery_level = None
|
||||||
|
|||||||
@@ -156,6 +156,7 @@ class HeartRateService(TemplateService):
|
|||||||
0,
|
0,
|
||||||
CharacteristicValue(read=read_heart_rate_measurement),
|
CharacteristicValue(read=read_heart_rate_measurement),
|
||||||
),
|
),
|
||||||
|
# pylint: disable=unnecessary-lambda
|
||||||
encode=lambda value: bytes(value),
|
encode=lambda value: bytes(value),
|
||||||
)
|
)
|
||||||
characteristics = [self.heart_rate_measurement_characteristic]
|
characteristics = [self.heart_rate_measurement_characteristic]
|
||||||
@@ -185,7 +186,7 @@ class HeartRateService(TemplateService):
|
|||||||
Characteristic.WRITEABLE,
|
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)
|
characteristics.append(self.heart_rate_control_point_characteristic)
|
||||||
|
|
||||||
@@ -224,7 +225,7 @@ class HeartRateServiceProxy(ProfileServiceProxy):
|
|||||||
):
|
):
|
||||||
self.heart_rate_control_point = PackedCharacteristicAdapter(
|
self.heart_rate_control_point = PackedCharacteristicAdapter(
|
||||||
characteristics[0],
|
characteristics[0],
|
||||||
format=HeartRateService.HEART_RATE_CONTROL_POINT_FORMAT,
|
pack_format=HeartRateService.HEART_RATE_CONTROL_POINT_FORMAT,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.heart_rate_control_point = None
|
self.heart_rate_control_point = None
|
||||||
|
|||||||
+102
-65
@@ -21,7 +21,8 @@ import asyncio
|
|||||||
from colors import color
|
from colors import color
|
||||||
from pyee import EventEmitter
|
from pyee import EventEmitter
|
||||||
|
|
||||||
from .core import BT_BR_EDR_TRANSPORT, InvalidStateError, ProtocolError, ConnectionError
|
from . import core
|
||||||
|
from .core import BT_BR_EDR_TRANSPORT, InvalidStateError, ProtocolError
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Logging
|
# Logging
|
||||||
@@ -104,17 +105,17 @@ RFCOMM_DYNAMIC_CHANNEL_NUMBER_END = 30
|
|||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
def fcs(buffer):
|
def compute_fcs(buffer):
|
||||||
fcs = 0xFF
|
result = 0xFF
|
||||||
for byte in buffer:
|
for byte in buffer:
|
||||||
fcs = CRC_TABLE[fcs ^ byte]
|
result = CRC_TABLE[result ^ byte]
|
||||||
return 0xFF - fcs
|
return 0xFF - result
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class RFCOMM_Frame:
|
class RFCOMM_Frame:
|
||||||
def __init__(self, type, c_r, dlci, p_f, information=b'', with_credits=False):
|
def __init__(self, frame_type, c_r, dlci, p_f, information=b'', with_credits=False):
|
||||||
self.type = type
|
self.type = frame_type
|
||||||
self.c_r = c_r
|
self.c_r = c_r
|
||||||
self.dlci = dlci
|
self.dlci = dlci
|
||||||
self.p_f = p_f
|
self.p_f = p_f
|
||||||
@@ -129,18 +130,18 @@ class RFCOMM_Frame:
|
|||||||
# 1-byte length indicator
|
# 1-byte length indicator
|
||||||
self.length = bytes([(length << 1) | 1])
|
self.length = bytes([(length << 1) | 1])
|
||||||
self.address = (dlci << 2) | (c_r << 1) | 1
|
self.address = (dlci << 2) | (c_r << 1) | 1
|
||||||
self.control = type | (p_f << 4)
|
self.control = frame_type | (p_f << 4)
|
||||||
if type == RFCOMM_UIH_FRAME:
|
if frame_type == RFCOMM_UIH_FRAME:
|
||||||
self.fcs = fcs(bytes([self.address, self.control]))
|
self.fcs = compute_fcs(bytes([self.address, self.control]))
|
||||||
else:
|
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):
|
def type_name(self):
|
||||||
return RFCOMM_FRAME_TYPE_NAMES[self.type]
|
return RFCOMM_FRAME_TYPE_NAMES[self.type]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_mcc(data):
|
def parse_mcc(data):
|
||||||
type = data[0] >> 2
|
mcc_type = data[0] >> 2
|
||||||
c_r = (data[0] >> 1) & 1
|
c_r = (data[0] >> 1) & 1
|
||||||
length = data[1]
|
length = data[1]
|
||||||
if data[1] & 1:
|
if data[1] & 1:
|
||||||
@@ -150,12 +151,12 @@ class RFCOMM_Frame:
|
|||||||
length = (data[3] << 7) & (length >> 1)
|
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
|
@staticmethod
|
||||||
def make_mcc(type, c_r, data):
|
def make_mcc(mcc_type, c_r, data):
|
||||||
return (
|
return (
|
||||||
bytes([(type << 2 | c_r << 1 | 1) & 0xFF, (len(data) & 0x7F) << 1 | 1])
|
bytes([(mcc_type << 2 | c_r << 1 | 1) & 0xFF, (len(data) & 0x7F) << 1 | 1])
|
||||||
+ data
|
+ data
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -186,7 +187,7 @@ class RFCOMM_Frame:
|
|||||||
# Extract fields
|
# Extract fields
|
||||||
dlci = (data[0] >> 2) & 0x3F
|
dlci = (data[0] >> 2) & 0x3F
|
||||||
c_r = (data[0] >> 1) & 0x01
|
c_r = (data[0] >> 1) & 0x01
|
||||||
type = data[1] & 0xEF
|
frame_type = data[1] & 0xEF
|
||||||
p_f = (data[1] >> 4) & 0x01
|
p_f = (data[1] >> 4) & 0x01
|
||||||
length = data[2]
|
length = data[2]
|
||||||
if length & 0x01:
|
if length & 0x01:
|
||||||
@@ -198,9 +199,9 @@ class RFCOMM_Frame:
|
|||||||
fcs = data[-1]
|
fcs = data[-1]
|
||||||
|
|
||||||
# Construct the frame and check the CRC
|
# 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:
|
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')
|
raise ValueError('fcs mismatch')
|
||||||
|
|
||||||
return frame
|
return frame
|
||||||
@@ -214,7 +215,14 @@ class RFCOMM_Frame:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self):
|
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})'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -264,7 +272,15 @@ class RFCOMM_MCC_PN:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self):
|
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})'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -302,7 +318,14 @@ class RFCOMM_MCC_MSC:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self):
|
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})'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -336,6 +359,7 @@ class DLC(EventEmitter):
|
|||||||
self.role = multiplexer.role
|
self.role = multiplexer.role
|
||||||
self.c_r = 1 if self.role == Multiplexer.INITIATOR else 0
|
self.c_r = 1 if self.role == Multiplexer.INITIATOR else 0
|
||||||
self.sink = None
|
self.sink = None
|
||||||
|
self.connection_result = None
|
||||||
|
|
||||||
# Compute the MTU
|
# Compute the MTU
|
||||||
max_overhead = 4 + 1 # header with 2-byte length + fcs
|
max_overhead = 4 + 1 # header with 2-byte length + fcs
|
||||||
@@ -360,30 +384,38 @@ class DLC(EventEmitter):
|
|||||||
handler = getattr(self, f'on_{frame.type_name()}_frame'.lower())
|
handler = getattr(self, f'on_{frame.type_name()}_frame'.lower())
|
||||||
handler(frame)
|
handler(frame)
|
||||||
|
|
||||||
def on_sabm_frame(self, frame):
|
def on_sabm_frame(self, _frame):
|
||||||
if self.state != DLC.CONNECTING:
|
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
|
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
|
# 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(type=RFCOMM_MCC_MSC_TYPE, c_r=1, data=bytes(msc))
|
mcc = RFCOMM_Frame.make_mcc(
|
||||||
|
mcc_type=RFCOMM_MCC_MSC_TYPE, c_r=1, data=bytes(msc)
|
||||||
|
)
|
||||||
logger.debug(f'>>> MCC MSC Command: {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.change_state(DLC.CONNECTED)
|
||||||
self.emit('open')
|
self.emit('open')
|
||||||
|
|
||||||
def on_ua_frame(self, frame):
|
def on_ua_frame(self, _frame):
|
||||||
if self.state != DLC.CONNECTING:
|
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
|
return
|
||||||
|
|
||||||
# Exchange the modem status with the peer
|
# 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(type=RFCOMM_MCC_MSC_TYPE, c_r=1, data=bytes(msc))
|
mcc = RFCOMM_Frame.make_mcc(
|
||||||
|
mcc_type=RFCOMM_MCC_MSC_TYPE, c_r=1, data=bytes(msc)
|
||||||
|
)
|
||||||
logger.debug(f'>>> MCC MSC Command: {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))
|
||||||
|
|
||||||
@@ -394,7 +426,7 @@ class DLC(EventEmitter):
|
|||||||
# TODO: handle all states
|
# TODO: handle all states
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def on_disc_frame(self, frame):
|
def on_disc_frame(self, _frame):
|
||||||
# TODO: handle all states
|
# 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))
|
||||||
|
|
||||||
@@ -402,25 +434,28 @@ class DLC(EventEmitter):
|
|||||||
data = frame.information
|
data = frame.information
|
||||||
if frame.p_f == 1:
|
if frame.p_f == 1:
|
||||||
# With credits
|
# With credits
|
||||||
credits = frame.information[0]
|
received_credits = frame.information[0]
|
||||||
self.tx_credits += credits
|
self.tx_credits += received_credits
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'<<< Credits [{self.dlci}]: received {credits}, total={self.tx_credits}'
|
f'<<< Credits [{self.dlci}]: '
|
||||||
|
f'received {credits}, total={self.tx_credits}'
|
||||||
)
|
)
|
||||||
data = data[1:]
|
data = data[1:]
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'{color("<<< Data", "yellow")} [{self.dlci}] {len(data)} bytes, rx_credits={self.rx_credits}: {data.hex()}'
|
f'{color("<<< Data", "yellow")} '
|
||||||
|
f'[{self.dlci}] {len(data)} bytes, '
|
||||||
|
f'rx_credits={self.rx_credits}: {data.hex()}'
|
||||||
)
|
)
|
||||||
if len(data) and self.sink:
|
if len(data) and self.sink:
|
||||||
self.sink(data)
|
self.sink(data) # pylint: disable=not-callable
|
||||||
|
|
||||||
# Update the credits
|
# Update the credits
|
||||||
if self.rx_credits > 0:
|
if self.rx_credits > 0:
|
||||||
self.rx_credits -= 1
|
self.rx_credits -= 1
|
||||||
else:
|
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)
|
# Check if there's anything to send (including credits)
|
||||||
self.process_tx()
|
self.process_tx()
|
||||||
@@ -434,7 +469,7 @@ class DLC(EventEmitter):
|
|||||||
logger.debug(f'<<< MCC MSC Command: {msc}')
|
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 = RFCOMM_Frame.make_mcc(
|
||||||
type=RFCOMM_MCC_MSC_TYPE, c_r=0, data=bytes(msc)
|
mcc_type=RFCOMM_MCC_MSC_TYPE, c_r=0, data=bytes(msc)
|
||||||
)
|
)
|
||||||
logger.debug(f'>>> MCC MSC Response: {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))
|
||||||
@@ -443,7 +478,7 @@ class DLC(EventEmitter):
|
|||||||
logger.debug(f'<<< MCC MSC Response: {msc}')
|
logger.debug(f'<<< MCC MSC Response: {msc}')
|
||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
if not self.state == DLC.INIT:
|
if self.state != DLC.INIT:
|
||||||
raise InvalidStateError('invalid state')
|
raise InvalidStateError('invalid state')
|
||||||
|
|
||||||
self.change_state(DLC.CONNECTING)
|
self.change_state(DLC.CONNECTING)
|
||||||
@@ -451,7 +486,7 @@ class DLC(EventEmitter):
|
|||||||
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):
|
def accept(self):
|
||||||
if not self.state == DLC.INIT:
|
if self.state != DLC.INIT:
|
||||||
raise InvalidStateError('invalid state')
|
raise InvalidStateError('invalid state')
|
||||||
|
|
||||||
pn = RFCOMM_MCC_PN(
|
pn = RFCOMM_MCC_PN(
|
||||||
@@ -463,7 +498,7 @@ class DLC(EventEmitter):
|
|||||||
max_retransmissions=0,
|
max_retransmissions=0,
|
||||||
window_size=RFCOMM_DEFAULT_INITIAL_RX_CREDITS,
|
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}')
|
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)
|
self.change_state(DLC.CONNECTING)
|
||||||
@@ -471,8 +506,8 @@ class DLC(EventEmitter):
|
|||||||
def rx_credits_needed(self):
|
def rx_credits_needed(self):
|
||||||
if self.rx_credits <= self.rx_threshold:
|
if self.rx_credits <= self.rx_threshold:
|
||||||
return RFCOMM_DEFAULT_INITIAL_RX_CREDITS - self.rx_credits
|
return RFCOMM_DEFAULT_INITIAL_RX_CREDITS - self.rx_credits
|
||||||
else:
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def process_tx(self):
|
def process_tx(self):
|
||||||
# Send anything we can (or an empty frame if we need to send rx credits)
|
# Send anything we can (or an empty frame if we need to send rx credits)
|
||||||
@@ -496,7 +531,9 @@ class DLC(EventEmitter):
|
|||||||
|
|
||||||
# Send the frame
|
# Send the frame
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'>>> sending {len(chunk)} bytes with {rx_credits_needed} credits, rx_credits={self.rx_credits}, tx_credits={self.tx_credits}'
|
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(
|
self.send_frame(
|
||||||
RFCOMM_Frame.uih(
|
RFCOMM_Frame.uih(
|
||||||
@@ -512,8 +549,8 @@ class DLC(EventEmitter):
|
|||||||
# Stream protocol
|
# Stream protocol
|
||||||
def write(self, data):
|
def write(self, data):
|
||||||
# We can only send bytes
|
# We can only send bytes
|
||||||
if type(data) != bytes:
|
if not isinstance(data, bytes):
|
||||||
if type(data) == str:
|
if isinstance(data, str):
|
||||||
# Automatically convert strings to bytes using UTF-8
|
# Automatically convert strings to bytes using UTF-8
|
||||||
data = data.encode('utf-8')
|
data = data.encode('utf-8')
|
||||||
else:
|
else:
|
||||||
@@ -592,14 +629,14 @@ class Multiplexer(EventEmitter):
|
|||||||
self.on_frame(frame)
|
self.on_frame(frame)
|
||||||
else:
|
else:
|
||||||
if frame.type == RFCOMM_DM_FRAME:
|
if frame.type == RFCOMM_DM_FRAME:
|
||||||
# DM responses are for a DLCI, but since we only create the dlc when we receive
|
# DM responses are for a DLCI, but since we only create the dlc when we
|
||||||
# a PN response (because we need the parameters), we handle DM frames at the Multiplexer
|
# receive a PN response (because we need the parameters), we handle DM
|
||||||
# level
|
# frames at the Multiplexer level
|
||||||
self.on_dm_frame(frame)
|
self.on_dm_frame(frame)
|
||||||
else:
|
else:
|
||||||
dlc = self.dlcs.get(frame.dlci)
|
dlc = self.dlcs.get(frame.dlci)
|
||||||
if dlc is None:
|
if dlc is None:
|
||||||
logger.warn(f'no dlc for DLCI {frame.dlci}')
|
logger.warning(f'no dlc for DLCI {frame.dlci}')
|
||||||
return
|
return
|
||||||
dlc.on_frame(frame)
|
dlc.on_frame(frame)
|
||||||
|
|
||||||
@@ -607,14 +644,14 @@ class Multiplexer(EventEmitter):
|
|||||||
handler = getattr(self, f'on_{frame.type_name()}_frame'.lower())
|
handler = getattr(self, f'on_{frame.type_name()}_frame'.lower())
|
||||||
handler(frame)
|
handler(frame)
|
||||||
|
|
||||||
def on_sabm_frame(self, frame):
|
def on_sabm_frame(self, _frame):
|
||||||
if self.state != Multiplexer.INIT:
|
if self.state != Multiplexer.INIT:
|
||||||
logger.debug('not in INIT state, ignoring SABM')
|
logger.debug('not in INIT state, ignoring SABM')
|
||||||
return
|
return
|
||||||
self.change_state(Multiplexer.CONNECTED)
|
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:
|
if self.state == Multiplexer.CONNECTING:
|
||||||
self.change_state(Multiplexer.CONNECTED)
|
self.change_state(Multiplexer.CONNECTED)
|
||||||
if self.connection_result:
|
if self.connection_result:
|
||||||
@@ -626,34 +663,34 @@ class Multiplexer(EventEmitter):
|
|||||||
self.disconnection_result.set_result(None)
|
self.disconnection_result.set_result(None)
|
||||||
self.disconnection_result = None
|
self.disconnection_result = None
|
||||||
|
|
||||||
def on_dm_frame(self, frame):
|
def on_dm_frame(self, _frame):
|
||||||
if self.state == Multiplexer.OPENING:
|
if self.state == Multiplexer.OPENING:
|
||||||
self.change_state(Multiplexer.CONNECTED)
|
self.change_state(Multiplexer.CONNECTED)
|
||||||
if self.open_result:
|
if self.open_result:
|
||||||
self.open_result.set_exception(
|
self.open_result.set_exception(
|
||||||
ConnectionError(
|
core.ConnectionError(
|
||||||
ConnectionError.CONNECTION_REFUSED,
|
core.ConnectionError.CONNECTION_REFUSED,
|
||||||
BT_BR_EDR_TRANSPORT,
|
BT_BR_EDR_TRANSPORT,
|
||||||
self.l2cap_channel.connection.peer_address,
|
self.l2cap_channel.connection.peer_address,
|
||||||
'rfcomm',
|
'rfcomm',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
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.change_state(Multiplexer.DISCONNECTED)
|
||||||
self.send_frame(
|
self.send_frame(
|
||||||
RFCOMM_Frame.ua(c_r=0 if self.role == Multiplexer.INITIATOR else 1, dlci=0)
|
RFCOMM_Frame.ua(c_r=0 if self.role == Multiplexer.INITIATOR else 1, dlci=0)
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_uih_frame(self, frame):
|
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)
|
pn = RFCOMM_MCC_PN.from_bytes(value)
|
||||||
self.on_mcc_pn(c_r, pn)
|
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)
|
mcs = RFCOMM_MCC_MSC.from_bytes(value)
|
||||||
self.on_mcc_msc(c_r, mcs)
|
self.on_mcc_msc(c_r, mcs)
|
||||||
|
|
||||||
@@ -669,7 +706,7 @@ class Multiplexer(EventEmitter):
|
|||||||
if pn.dlci & 1:
|
if pn.dlci & 1:
|
||||||
# Not expected, this is an initiator-side number
|
# Not expected, this is an initiator-side number
|
||||||
# TODO: error out
|
# TODO: error out
|
||||||
logger.warn(f'invalid DLCI: {pn.dlci}')
|
logger.warning(f'invalid DLCI: {pn.dlci}')
|
||||||
else:
|
else:
|
||||||
if self.acceptor:
|
if self.acceptor:
|
||||||
channel_number = pn.dlci >> 1
|
channel_number = pn.dlci >> 1
|
||||||
@@ -688,7 +725,7 @@ class Multiplexer(EventEmitter):
|
|||||||
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:
|
else:
|
||||||
# No acceptor?? shouldn't happen
|
# No acceptor?? shouldn't happen
|
||||||
logger.warn(color('!!! no acceptor registered', 'red'))
|
logger.warning(color('!!! no acceptor registered', 'red'))
|
||||||
else:
|
else:
|
||||||
# Response
|
# Response
|
||||||
logger.debug(f'>>> PN Response: {pn}')
|
logger.debug(f'>>> PN Response: {pn}')
|
||||||
@@ -697,12 +734,12 @@ class Multiplexer(EventEmitter):
|
|||||||
self.dlcs[pn.dlci] = dlc
|
self.dlcs[pn.dlci] = dlc
|
||||||
dlc.connect()
|
dlc.connect()
|
||||||
else:
|
else:
|
||||||
logger.warn('ignoring PN response')
|
logger.warning('ignoring PN response')
|
||||||
|
|
||||||
def on_mcc_msc(self, c_r, msc):
|
def on_mcc_msc(self, c_r, msc):
|
||||||
dlc = self.dlcs.get(msc.dlci)
|
dlc = self.dlcs.get(msc.dlci)
|
||||||
if dlc is None:
|
if dlc is None:
|
||||||
logger.warn(f'no dlc for DLCI {msc.dlci}')
|
logger.warning(f'no dlc for DLCI {msc.dlci}')
|
||||||
return
|
return
|
||||||
dlc.on_mcc_msc(c_r, msc)
|
dlc.on_mcc_msc(c_r, msc)
|
||||||
|
|
||||||
@@ -732,8 +769,8 @@ class Multiplexer(EventEmitter):
|
|||||||
if self.state != Multiplexer.CONNECTED:
|
if self.state != Multiplexer.CONNECTED:
|
||||||
if self.state == Multiplexer.OPENING:
|
if self.state == Multiplexer.OPENING:
|
||||||
raise InvalidStateError('open already in progress')
|
raise InvalidStateError('open already in progress')
|
||||||
else:
|
|
||||||
raise InvalidStateError('not connected')
|
raise InvalidStateError('not connected')
|
||||||
|
|
||||||
pn = RFCOMM_MCC_PN(
|
pn = RFCOMM_MCC_PN(
|
||||||
dlci=channel << 1,
|
dlci=channel << 1,
|
||||||
@@ -744,7 +781,7 @@ class Multiplexer(EventEmitter):
|
|||||||
max_retransmissions=0,
|
max_retransmissions=0,
|
||||||
window_size=RFCOMM_DEFAULT_INITIAL_RX_CREDITS,
|
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}')
|
logger.debug(f'>>> Sending MCC: {pn}')
|
||||||
self.open_result = asyncio.get_running_loop().create_future()
|
self.open_result = asyncio.get_running_loop().create_future()
|
||||||
self.change_state(Multiplexer.OPENING)
|
self.change_state(Multiplexer.OPENING)
|
||||||
@@ -784,7 +821,7 @@ class Client:
|
|||||||
self.connection, RFCOMM_PSM
|
self.connection, RFCOMM_PSM
|
||||||
)
|
)
|
||||||
except ProtocolError as error:
|
except ProtocolError as error:
|
||||||
logger.warn(f'L2CAP connection failed: {error}')
|
logger.warning(f'L2CAP connection failed: {error}')
|
||||||
raise
|
raise
|
||||||
|
|
||||||
# Create a mutliplexer to manage DLCs with the server
|
# Create a mutliplexer to manage DLCs with the server
|
||||||
|
|||||||
+82
-59
@@ -34,6 +34,7 @@ logger = logging.getLogger(__name__)
|
|||||||
# Constants
|
# Constants
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# fmt: off
|
# fmt: off
|
||||||
|
# pylint: disable=line-too-long
|
||||||
|
|
||||||
SDP_CONTINUATION_WATCHDOG = 64 # Maximum number of continuations we're willing to do
|
SDP_CONTINUATION_WATCHDOG = 64 # Maximum number of continuations we're willing to do
|
||||||
|
|
||||||
@@ -115,6 +116,8 @@ SDP_PUBLIC_BROWSE_ROOT = core.UUID.from_16_bits(0x1002, 'PublicBrowseRoot')
|
|||||||
SDP_ALL_ATTRIBUTES_RANGE = (0x0000FFFF, 4) # Express this as tuple so we can convey the desired encoding size
|
SDP_ALL_ATTRIBUTES_RANGE = (0x0000FFFF, 4) # Express this as tuple so we can convey the desired encoding size
|
||||||
|
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
# pylint: enable=line-too-long
|
||||||
|
# pylint: disable=invalid-name
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -167,12 +170,13 @@ class DataElement:
|
|||||||
URL: lambda x: DataElement(DataElement.URL, x.decode('utf8')),
|
URL: lambda x: DataElement(DataElement.URL, x.decode('utf8')),
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, type, value, value_size=None):
|
def __init__(self, element_type, value, value_size=None):
|
||||||
self.type = type
|
self.type = element_type
|
||||||
self.value = value
|
self.value = value
|
||||||
self.value_size = value_size
|
self.value_size = value_size
|
||||||
self.bytes = None # Used a cache when parsing from bytes so we can emit a byte-for-byte replica
|
# Used as 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:
|
self.bytes = None
|
||||||
|
if element_type in (DataElement.UNSIGNED_INTEGER, DataElement.SIGNED_INTEGER):
|
||||||
if value_size is None:
|
if value_size is None:
|
||||||
raise ValueError('integer types must have a value size specified')
|
raise ValueError('integer types must have a value size specified')
|
||||||
|
|
||||||
@@ -240,27 +244,33 @@ class DataElement:
|
|||||||
def unsigned_integer_from_bytes(data):
|
def unsigned_integer_from_bytes(data):
|
||||||
if len(data) == 1:
|
if len(data) == 1:
|
||||||
return data[0]
|
return data[0]
|
||||||
elif len(data) == 2:
|
|
||||||
|
if len(data) == 2:
|
||||||
return struct.unpack('>H', data)[0]
|
return struct.unpack('>H', data)[0]
|
||||||
elif len(data) == 4:
|
|
||||||
|
if len(data) == 4:
|
||||||
return struct.unpack('>I', data)[0]
|
return struct.unpack('>I', data)[0]
|
||||||
elif len(data) == 8:
|
|
||||||
|
if len(data) == 8:
|
||||||
return struct.unpack('>Q', data)[0]
|
return struct.unpack('>Q', data)[0]
|
||||||
else:
|
|
||||||
raise ValueError(f'invalid integer length {len(data)}')
|
raise ValueError(f'invalid integer length {len(data)}')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def signed_integer_from_bytes(data):
|
def signed_integer_from_bytes(data):
|
||||||
if len(data) == 1:
|
if len(data) == 1:
|
||||||
return struct.unpack('b', data)[0]
|
return struct.unpack('b', data)[0]
|
||||||
elif len(data) == 2:
|
|
||||||
|
if len(data) == 2:
|
||||||
return struct.unpack('>h', data)[0]
|
return struct.unpack('>h', data)[0]
|
||||||
elif len(data) == 4:
|
|
||||||
|
if len(data) == 4:
|
||||||
return struct.unpack('>i', data)[0]
|
return struct.unpack('>i', data)[0]
|
||||||
elif len(data) == 8:
|
|
||||||
|
if len(data) == 8:
|
||||||
return struct.unpack('>q', data)[0]
|
return struct.unpack('>q', data)[0]
|
||||||
else:
|
|
||||||
raise ValueError(f'invalid integer length {len(data)}')
|
raise ValueError(f'invalid integer length {len(data)}')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def list_from_bytes(data):
|
def list_from_bytes(data):
|
||||||
@@ -278,11 +288,11 @@ class DataElement:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_bytes(data):
|
def from_bytes(data):
|
||||||
type = data[0] >> 3
|
element_type = data[0] >> 3
|
||||||
size_index = data[0] & 7
|
size_index = data[0] & 7
|
||||||
value_offset = 0
|
value_offset = 0
|
||||||
if size_index == 0:
|
if size_index == 0:
|
||||||
if type == DataElement.NIL:
|
if element_type == DataElement.NIL:
|
||||||
value_size = 0
|
value_size = 0
|
||||||
else:
|
else:
|
||||||
value_size = 1
|
value_size = 1
|
||||||
@@ -305,17 +315,17 @@ class DataElement:
|
|||||||
value_offset = 4
|
value_offset = 4
|
||||||
|
|
||||||
value_data = data[1 + value_offset : 1 + value_offset + value_size]
|
value_data = data[1 + value_offset : 1 + value_offset + value_size]
|
||||||
constructor = DataElement.type_constructors.get(type)
|
constructor = DataElement.type_constructors.get(element_type)
|
||||||
if constructor:
|
if constructor:
|
||||||
if (
|
if element_type in (
|
||||||
type == DataElement.UNSIGNED_INTEGER
|
DataElement.UNSIGNED_INTEGER,
|
||||||
or type == DataElement.SIGNED_INTEGER
|
DataElement.SIGNED_INTEGER,
|
||||||
):
|
):
|
||||||
result = constructor(value_data, value_size)
|
result = constructor(value_data, value_size)
|
||||||
else:
|
else:
|
||||||
result = constructor(value_data)
|
result = constructor(value_data)
|
||||||
else:
|
else:
|
||||||
result = DataElement(type, value_data)
|
result = DataElement(element_type, value_data)
|
||||||
result.bytes = data[
|
result.bytes = data[
|
||||||
: 1 + value_offset + value_size
|
: 1 + value_offset + value_size
|
||||||
] # Keep a copy so we can re-serialize to an exact replica
|
] # Keep a copy so we can re-serialize to an exact replica
|
||||||
@@ -334,7 +344,8 @@ class DataElement:
|
|||||||
elif self.type == DataElement.UNSIGNED_INTEGER:
|
elif self.type == DataElement.UNSIGNED_INTEGER:
|
||||||
if self.value < 0:
|
if self.value < 0:
|
||||||
raise ValueError('UNSIGNED_INTEGER cannot be negative')
|
raise ValueError('UNSIGNED_INTEGER cannot be negative')
|
||||||
elif self.value_size == 1:
|
|
||||||
|
if self.value_size == 1:
|
||||||
data = struct.pack('B', self.value)
|
data = struct.pack('B', self.value)
|
||||||
elif self.value_size == 2:
|
elif self.value_size == 2:
|
||||||
data = struct.pack('>H', self.value)
|
data = struct.pack('>H', self.value)
|
||||||
@@ -357,11 +368,11 @@ class DataElement:
|
|||||||
raise ValueError('invalid value_size')
|
raise ValueError('invalid value_size')
|
||||||
elif self.type == DataElement.UUID:
|
elif self.type == DataElement.UUID:
|
||||||
data = bytes(reversed(bytes(self.value)))
|
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')
|
data = self.value.encode('utf8')
|
||||||
elif self.type == DataElement.BOOLEAN:
|
elif self.type == DataElement.BOOLEAN:
|
||||||
data = bytes([1 if self.value else 0])
|
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])
|
data = b''.join([bytes(element) for element in self.value])
|
||||||
else:
|
else:
|
||||||
data = self.value
|
data = self.value
|
||||||
@@ -372,10 +383,10 @@ class DataElement:
|
|||||||
if size != 0:
|
if size != 0:
|
||||||
raise ValueError('NIL must be empty')
|
raise ValueError('NIL must be empty')
|
||||||
size_index = 0
|
size_index = 0
|
||||||
elif (
|
elif self.type in (
|
||||||
self.type == DataElement.UNSIGNED_INTEGER
|
DataElement.UNSIGNED_INTEGER,
|
||||||
or self.type == DataElement.SIGNED_INTEGER
|
DataElement.SIGNED_INTEGER,
|
||||||
or self.type == DataElement.UUID
|
DataElement.UUID,
|
||||||
):
|
):
|
||||||
if size <= 1:
|
if size <= 1:
|
||||||
size_index = 0
|
size_index = 0
|
||||||
@@ -389,11 +400,11 @@ class DataElement:
|
|||||||
size_index = 4
|
size_index = 4
|
||||||
else:
|
else:
|
||||||
raise ValueError('invalid data size')
|
raise ValueError('invalid data size')
|
||||||
elif (
|
elif self.type in (
|
||||||
self.type == DataElement.TEXT_STRING
|
DataElement.TEXT_STRING,
|
||||||
or self.type == DataElement.SEQUENCE
|
DataElement.SEQUENCE,
|
||||||
or self.type == DataElement.ALTERNATIVE
|
DataElement.ALTERNATIVE,
|
||||||
or self.type == DataElement.URL
|
DataElement.URL,
|
||||||
):
|
):
|
||||||
if size <= 0xFF:
|
if size <= 0xFF:
|
||||||
size_index = 5
|
size_index = 5
|
||||||
@@ -419,14 +430,19 @@ class DataElement:
|
|||||||
type_name = name_or_number(self.TYPE_NAMES, self.type)
|
type_name = name_or_number(self.TYPE_NAMES, self.type)
|
||||||
if self.type == DataElement.NIL:
|
if self.type == DataElement.NIL:
|
||||||
value_string = ''
|
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 ''
|
container_separator = '\n' if pretty else ''
|
||||||
element_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}]'
|
elements = [
|
||||||
elif (
|
element.to_string(pretty, indentation + 1 if pretty else 0)
|
||||||
self.type == DataElement.UNSIGNED_INTEGER
|
for element in self.value
|
||||||
or self.type == DataElement.SIGNED_INTEGER
|
]
|
||||||
):
|
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}'
|
value_string = f'{self.value}#{self.value_size}'
|
||||||
elif isinstance(self.value, DataElement):
|
elif isinstance(self.value, DataElement):
|
||||||
value_string = self.value.to_string(pretty, indentation)
|
value_string = self.value.to_string(pretty, indentation)
|
||||||
@@ -440,8 +456,8 @@ class DataElement:
|
|||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class ServiceAttribute:
|
class ServiceAttribute:
|
||||||
def __init__(self, id, value):
|
def __init__(self, attribute_id, value):
|
||||||
self.id = id
|
self.id = attribute_id
|
||||||
self.value = value
|
self.value = value
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -450,7 +466,7 @@ class ServiceAttribute:
|
|||||||
for i in range(0, len(elements) // 2):
|
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:
|
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
|
continue
|
||||||
attribute_list.append(ServiceAttribute(attribute_id.value, attribute_value))
|
attribute_list.append(ServiceAttribute(attribute_id.value, attribute_value))
|
||||||
|
|
||||||
@@ -468,27 +484,31 @@ class ServiceAttribute:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def id_name(id):
|
def id_name(id_code):
|
||||||
return name_or_number(SDP_ATTRIBUTE_ID_NAMES, id)
|
return name_or_number(SDP_ATTRIBUTE_ID_NAMES, id_code)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_uuid_in_value(uuid, value):
|
def is_uuid_in_value(uuid, value):
|
||||||
# Find if a uuid matches a value, either directly or recursing into sequences
|
# Find if a uuid matches a value, either directly or recursing into sequences
|
||||||
if value.type == DataElement.UUID:
|
if value.type == DataElement.UUID:
|
||||||
return value.value == uuid
|
return value.value == uuid
|
||||||
elif value.type == DataElement.SEQUENCE:
|
|
||||||
|
if value.type == DataElement.SEQUENCE:
|
||||||
for element in value.value:
|
for element in value.value:
|
||||||
if ServiceAttribute.is_uuid_in_value(uuid, element):
|
if ServiceAttribute.is_uuid_in_value(uuid, element):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def to_string(self, color=False):
|
return False
|
||||||
if color:
|
|
||||||
return f'Attribute(id={colors.color(self.id_name(self.id),"magenta")},value={self.value})'
|
def to_string(self, with_colors=False):
|
||||||
else:
|
if with_colors:
|
||||||
return f'Attribute(id={self.id_name(self.id)},value={self.value})'
|
return (
|
||||||
|
f'Attribute(id={colors.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):
|
def __str__(self):
|
||||||
return self.to_string()
|
return self.to_string()
|
||||||
@@ -501,10 +521,12 @@ class SDP_PDU:
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
sdp_pdu_classes = {}
|
sdp_pdu_classes = {}
|
||||||
|
name = None
|
||||||
|
pdu_id = 0
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_bytes(pdu):
|
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)
|
cls = SDP_PDU.sdp_pdu_classes.get(pdu_id)
|
||||||
if cls is None:
|
if cls is None:
|
||||||
@@ -755,7 +777,7 @@ class Client:
|
|||||||
DataElement.unsigned_integer(
|
DataElement.unsigned_integer(
|
||||||
attribute_id[0], value_size=attribute_id[1]
|
attribute_id[0], value_size=attribute_id[1]
|
||||||
)
|
)
|
||||||
if type(attribute_id) is tuple
|
if isinstance(attribute_id, tuple)
|
||||||
else DataElement.unsigned_integer_16(attribute_id)
|
else DataElement.unsigned_integer_16(attribute_id)
|
||||||
for attribute_id in attribute_ids
|
for attribute_id in attribute_ids
|
||||||
]
|
]
|
||||||
@@ -787,7 +809,7 @@ class Client:
|
|||||||
# Parse the result into attribute lists
|
# Parse the result into attribute lists
|
||||||
attribute_lists_sequences = DataElement.from_bytes(accumulator)
|
attribute_lists_sequences = DataElement.from_bytes(accumulator)
|
||||||
if attribute_lists_sequences.type != DataElement.SEQUENCE:
|
if attribute_lists_sequences.type != DataElement.SEQUENCE:
|
||||||
logger.warn('unexpected data type')
|
logger.warning('unexpected data type')
|
||||||
return []
|
return []
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -805,7 +827,7 @@ class Client:
|
|||||||
DataElement.unsigned_integer(
|
DataElement.unsigned_integer(
|
||||||
attribute_id[0], value_size=attribute_id[1]
|
attribute_id[0], value_size=attribute_id[1]
|
||||||
)
|
)
|
||||||
if type(attribute_id) is tuple
|
if isinstance(attribute_id, tuple)
|
||||||
else DataElement.unsigned_integer_16(attribute_id)
|
else DataElement.unsigned_integer_16(attribute_id)
|
||||||
for attribute_id in attribute_ids
|
for attribute_id in attribute_ids
|
||||||
]
|
]
|
||||||
@@ -837,7 +859,7 @@ class Client:
|
|||||||
# Parse the result into a list of attributes
|
# Parse the result into a list of attributes
|
||||||
attribute_list_sequence = DataElement.from_bytes(accumulator)
|
attribute_list_sequence = DataElement.from_bytes(accumulator)
|
||||||
if attribute_list_sequence.type != DataElement.SEQUENCE:
|
if attribute_list_sequence.type != DataElement.SEQUENCE:
|
||||||
logger.warn('unexpected data type')
|
logger.warning('unexpected data type')
|
||||||
return []
|
return []
|
||||||
|
|
||||||
return ServiceAttribute.list_from_data_elements(attribute_list_sequence.value)
|
return ServiceAttribute.list_from_data_elements(attribute_list_sequence.value)
|
||||||
@@ -850,6 +872,7 @@ class Server:
|
|||||||
def __init__(self, device):
|
def __init__(self, device):
|
||||||
self.device = device
|
self.device = device
|
||||||
self.service_records = {} # Service records maps, by record handle
|
self.service_records = {} # Service records maps, by record handle
|
||||||
|
self.channel = None
|
||||||
self.current_response = None
|
self.current_response = None
|
||||||
|
|
||||||
def register(self, l2cap_channel_manager):
|
def register(self, l2cap_channel_manager):
|
||||||
@@ -884,7 +907,7 @@ class Server:
|
|||||||
try:
|
try:
|
||||||
sdp_pdu = SDP_PDU.from_bytes(pdu)
|
sdp_pdu = SDP_PDU.from_bytes(pdu)
|
||||||
except Exception as error:
|
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(
|
self.send_response(
|
||||||
SDP_ErrorResponse(
|
SDP_ErrorResponse(
|
||||||
transaction_id=0, error_code=SDP_INVALID_REQUEST_SYNTAX_ERROR
|
transaction_id=0, error_code=SDP_INVALID_REQUEST_SYNTAX_ERROR
|
||||||
@@ -945,7 +968,7 @@ class Server:
|
|||||||
if attribute.id >= id_range_start and attribute.id <= id_range_end
|
if attribute.id >= id_range_start and attribute.id <= id_range_end
|
||||||
]
|
]
|
||||||
|
|
||||||
# Return the maching attributes, sorted by attribute id
|
# Return the matching attributes, sorted by attribute id
|
||||||
attributes.sort(key=lambda x: x.id)
|
attributes.sort(key=lambda x: x.id)
|
||||||
attribute_list = DataElement.sequence([])
|
attribute_list = DataElement.sequence([])
|
||||||
for attribute in attributes:
|
for attribute in attributes:
|
||||||
|
|||||||
+91
-69
@@ -28,8 +28,14 @@ import secrets
|
|||||||
from pyee import EventEmitter
|
from pyee import EventEmitter
|
||||||
from colors import color
|
from colors import color
|
||||||
|
|
||||||
from .core import *
|
from .hci import Address, HCI_LE_Enable_Encryption_Command, HCI_Object, key_with_value
|
||||||
from .hci import *
|
from .core import (
|
||||||
|
BT_BR_EDR_TRANSPORT,
|
||||||
|
BT_CENTRAL_ROLE,
|
||||||
|
BT_LE_TRANSPORT,
|
||||||
|
ProtocolError,
|
||||||
|
name_or_number,
|
||||||
|
)
|
||||||
from .keys import PairingKeys
|
from .keys import PairingKeys
|
||||||
from . import crypto
|
from . import crypto
|
||||||
|
|
||||||
@@ -44,6 +50,7 @@ logger = logging.getLogger(__name__)
|
|||||||
# Constants
|
# Constants
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# fmt: off
|
# fmt: off
|
||||||
|
# pylint: disable=line-too-long
|
||||||
|
|
||||||
SMP_CID = 0x06
|
SMP_CID = 0x06
|
||||||
SMP_BR_CID = 0x07
|
SMP_BR_CID = 0x07
|
||||||
@@ -158,6 +165,8 @@ SMP_CTKD_H7_LEBR_SALT = bytes.fromhex('00000000000000000000000000000000746D7031'
|
|||||||
SMP_CTKD_H7_BRLE_SALT = bytes.fromhex('00000000000000000000000000000000746D7032')
|
SMP_CTKD_H7_BRLE_SALT = bytes.fromhex('00000000000000000000000000000000746D7032')
|
||||||
|
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
# pylint: enable=line-too-long
|
||||||
|
# pylint: disable=invalid-name
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -177,6 +186,7 @@ class SMP_Command:
|
|||||||
|
|
||||||
smp_classes = {}
|
smp_classes = {}
|
||||||
code = 0
|
code = 0
|
||||||
|
name = ''
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_bytes(pdu):
|
def from_bytes(pdu):
|
||||||
@@ -206,7 +216,10 @@ class SMP_Command:
|
|||||||
keypress = (value >> 4) & 1
|
keypress = (value >> 4) & 1
|
||||||
ct2 = (value >> 5) & 1
|
ct2 = (value >> 5) & 1
|
||||||
|
|
||||||
return f'bonding_flags={bonding_flags}, MITM={mitm}, sc={sc}, keypress={keypress}, ct2={ct2}'
|
return (
|
||||||
|
f'bonding_flags={bonding_flags}, '
|
||||||
|
f'MITM={mitm}, sc={sc}, keypress={keypress}, ct2={ct2}'
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def io_capability_name(io_capability):
|
def io_capability_name(io_capability):
|
||||||
@@ -458,11 +471,11 @@ class AddressResolver:
|
|||||||
|
|
||||||
def resolve(self, address):
|
def resolve(self, address):
|
||||||
address_bytes = bytes(address)
|
address_bytes = bytes(address)
|
||||||
hash = address_bytes[0:3]
|
hash_part = address_bytes[0:3]
|
||||||
prand = address_bytes[3:6]
|
prand = address_bytes[3:6]
|
||||||
for (irk, resolved_address) in self.resolving_keys:
|
for (irk, resolved_address) in self.resolving_keys:
|
||||||
local_hash = crypto.ah(irk, prand)
|
local_hash = crypto.ah(irk, prand)
|
||||||
if local_hash == hash:
|
if local_hash == hash_part:
|
||||||
# Match!
|
# Match!
|
||||||
if resolved_address.address_type == Address.PUBLIC_DEVICE_ADDRESS:
|
if resolved_address.address_type == Address.PUBLIC_DEVICE_ADDRESS:
|
||||||
resolved_address_type = Address.PUBLIC_IDENTITY_ADDRESS
|
resolved_address_type = Address.PUBLIC_IDENTITY_ADDRESS
|
||||||
@@ -472,6 +485,8 @@ class AddressResolver:
|
|||||||
address=str(resolved_address), address_type=resolved_address_type
|
address=str(resolved_address), address_type=resolved_address_type
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class PairingDelegate:
|
class PairingDelegate:
|
||||||
@@ -500,13 +515,13 @@ class PairingDelegate:
|
|||||||
async def confirm(self):
|
async def confirm(self):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def compare_numbers(self, number, digits=6):
|
async def compare_numbers(self, _number, _digits=6):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def get_number(self):
|
async def get_number(self):
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
async def display_number(self, number, digits=6):
|
async def display_number(self, _number, _digits=6):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def key_distribution_response(
|
async def key_distribution_response(
|
||||||
@@ -528,7 +543,11 @@ class PairingConfig:
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
io_capability_str = SMP_Command.io_capability_name(self.delegate.io_capability)
|
io_capability_str = SMP_Command.io_capability_name(self.delegate.io_capability)
|
||||||
return f'PairingConfig(sc={self.sc}, mitm={self.mitm}, bonding={self.bonding}, delegate[{io_capability_str}])'
|
return (
|
||||||
|
f'PairingConfig(sc={self.sc}, '
|
||||||
|
f'mitm={self.mitm}, bonding={self.bonding}, '
|
||||||
|
f'delegate[{io_capability_str}])'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -548,14 +567,16 @@ class Session:
|
|||||||
|
|
||||||
# I/O Capability to pairing method decision matrix
|
# I/O Capability to pairing method decision matrix
|
||||||
#
|
#
|
||||||
# See Bluetooth spec @ Vol 3, part H - Table 2.8: Mapping of IO Capabilities to Key Generation Method
|
# See Bluetooth spec @ Vol 3, part H - Table 2.8: Mapping of IO Capabilities to Key
|
||||||
|
# Generation Method
|
||||||
#
|
#
|
||||||
# Map: initiator -> responder -> <method>
|
# Map: initiator -> responder -> <method>
|
||||||
# where <method> may be a simple entry or a 2-element tuple, with the first element for legacy
|
# where <method> may be a simple entry or a 2-element tuple, with the first element
|
||||||
# pairing and the second for secure connections, when the two are different.
|
# for legacy pairing and the second for secure connections, when the two are
|
||||||
# Each entry is either a method name, or, for PASSKEY, a tuple:
|
# different. Each entry is either a method name, or, for PASSKEY, a tuple:
|
||||||
# (method, initiator_displays, responder_displays)
|
# (method, initiator_displays, responder_displays)
|
||||||
# to specify if the initiator and responder should display (True) or input a code (False).
|
# to specify if the initiator and responder should display (True) or input a code
|
||||||
|
# (False).
|
||||||
PAIRING_METHODS = {
|
PAIRING_METHODS = {
|
||||||
SMP_DISPLAY_ONLY_IO_CAPABILITY: {
|
SMP_DISPLAY_ONLY_IO_CAPABILITY: {
|
||||||
SMP_DISPLAY_ONLY_IO_CAPABILITY: JUST_WORKS,
|
SMP_DISPLAY_ONLY_IO_CAPABILITY: JUST_WORKS,
|
||||||
@@ -606,6 +627,10 @@ class Session:
|
|||||||
def __init__(self, manager, connection, pairing_config):
|
def __init__(self, manager, connection, pairing_config):
|
||||||
self.manager = manager
|
self.manager = manager
|
||||||
self.connection = connection
|
self.connection = connection
|
||||||
|
self.preq = None
|
||||||
|
self.pres = None
|
||||||
|
self.ea = None
|
||||||
|
self.eb = None
|
||||||
self.tk = bytes(16)
|
self.tk = bytes(16)
|
||||||
self.r = bytes(16)
|
self.r = bytes(16)
|
||||||
self.stk = None
|
self.stk = None
|
||||||
@@ -626,6 +651,7 @@ class Session:
|
|||||||
self.peer_signature_key = None
|
self.peer_signature_key = None
|
||||||
self.peer_expected_distributions = []
|
self.peer_expected_distributions = []
|
||||||
self.dh_key = None
|
self.dh_key = None
|
||||||
|
self.confirm_value = None
|
||||||
self.passkey = 0
|
self.passkey = 0
|
||||||
self.passkey_step = 0
|
self.passkey_step = 0
|
||||||
self.passkey_display = False
|
self.passkey_display = False
|
||||||
@@ -726,6 +752,8 @@ class Session:
|
|||||||
else:
|
else:
|
||||||
return self.ltk
|
return self.ltk
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def decide_pairing_method(
|
def decide_pairing_method(
|
||||||
self, auth_req, initiator_io_capability, responder_io_capability
|
self, auth_req, initiator_io_capability, responder_io_capability
|
||||||
):
|
):
|
||||||
@@ -734,10 +762,10 @@ class Session:
|
|||||||
return
|
return
|
||||||
|
|
||||||
details = self.PAIRING_METHODS[initiator_io_capability][responder_io_capability]
|
details = self.PAIRING_METHODS[initiator_io_capability][responder_io_capability]
|
||||||
if type(details) is tuple and len(details) == 2:
|
if isinstance(details, tuple) and len(details) == 2:
|
||||||
# One entry for legacy pairing and one for secure connections
|
# One entry for legacy pairing and one for secure connections
|
||||||
details = details[1 if self.sc else 0]
|
details = details[1 if self.sc else 0]
|
||||||
if type(details) is int:
|
if isinstance(details, int):
|
||||||
# Just a method ID
|
# Just a method ID
|
||||||
self.pairing_method = details
|
self.pairing_method = details
|
||||||
else:
|
else:
|
||||||
@@ -762,7 +790,7 @@ class Session:
|
|||||||
next_steps()
|
next_steps()
|
||||||
return
|
return
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
logger.warn(f'exception while confirm: {error}')
|
logger.warning(f'exception while confirm: {error}')
|
||||||
|
|
||||||
self.send_pairing_failed(SMP_CONFIRM_VALUE_FAILED_ERROR)
|
self.send_pairing_failed(SMP_CONFIRM_VALUE_FAILED_ERROR)
|
||||||
|
|
||||||
@@ -779,7 +807,7 @@ class Session:
|
|||||||
next_steps()
|
next_steps()
|
||||||
return
|
return
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
logger.warn(f'exception while prompting: {error}')
|
logger.warning(f'exception while prompting: {error}')
|
||||||
|
|
||||||
self.send_pairing_failed(SMP_CONFIRM_VALUE_FAILED_ERROR)
|
self.send_pairing_failed(SMP_CONFIRM_VALUE_FAILED_ERROR)
|
||||||
|
|
||||||
@@ -793,7 +821,7 @@ class Session:
|
|||||||
logger.debug(f'user input: {passkey}')
|
logger.debug(f'user input: {passkey}')
|
||||||
next_steps(passkey)
|
next_steps(passkey)
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
logger.warn(f'exception while prompting: {error}')
|
logger.warning(f'exception while prompting: {error}')
|
||||||
self.send_pairing_failed(SMP_PASSKEY_ENTRY_FAILED_ERROR)
|
self.send_pairing_failed(SMP_PASSKEY_ENTRY_FAILED_ERROR)
|
||||||
|
|
||||||
self.connection.abort_on('disconnection', prompt())
|
self.connection.abort_on('disconnection', prompt())
|
||||||
@@ -808,8 +836,9 @@ class Session:
|
|||||||
self.tk = self.passkey.to_bytes(16, byteorder='little')
|
self.tk = self.passkey.to_bytes(16, byteorder='little')
|
||||||
logger.debug(f'TK from passkey = {self.tk.hex()}')
|
logger.debug(f'TK from passkey = {self.tk.hex()}')
|
||||||
|
|
||||||
self.connection.abort_on('disconnection',
|
self.connection.abort_on(
|
||||||
self.pairing_config.delegate.display_number(self.passkey, digits=6)
|
'disconnection',
|
||||||
|
self.pairing_config.delegate.display_number(self.passkey, digits=6),
|
||||||
)
|
)
|
||||||
|
|
||||||
def input_passkey(self, next_steps=None):
|
def input_passkey(self, next_steps=None):
|
||||||
@@ -872,10 +901,7 @@ class Session:
|
|||||||
logger.debug(f'generated random: {self.r.hex()}')
|
logger.debug(f'generated random: {self.r.hex()}')
|
||||||
|
|
||||||
if self.sc:
|
if self.sc:
|
||||||
if (
|
if self.pairing_method in (self.JUST_WORKS, self.NUMERIC_COMPARISON):
|
||||||
self.pairing_method == self.JUST_WORKS
|
|
||||||
or self.pairing_method == self.NUMERIC_COMPARISON
|
|
||||||
):
|
|
||||||
z = 0
|
z = 0
|
||||||
elif self.pairing_method == self.PASSKEY:
|
elif self.pairing_method == self.PASSKEY:
|
||||||
z = 0x80 + ((self.passkey >> self.passkey_step) & 1)
|
z = 0x80 + ((self.passkey >> self.passkey_step) & 1)
|
||||||
@@ -926,7 +952,7 @@ class Session:
|
|||||||
connection_handle=self.connection.handle,
|
connection_handle=self.connection.handle,
|
||||||
random_number=bytes(8),
|
random_number=bytes(8),
|
||||||
encrypted_diversifier=0,
|
encrypted_diversifier=0,
|
||||||
long_term_key=key
|
long_term_key=key,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -948,7 +974,9 @@ class Session:
|
|||||||
self.connection.transport == BT_BR_EDR_TRANSPORT
|
self.connection.transport == BT_BR_EDR_TRANSPORT
|
||||||
and self.initiator_key_distribution & SMP_ENC_KEY_DISTRIBUTION_FLAG
|
and self.initiator_key_distribution & SMP_ENC_KEY_DISTRIBUTION_FLAG
|
||||||
):
|
):
|
||||||
self.ctkd_task = self.connection.abort_on('disconnection', self.derive_ltk())
|
self.ctkd_task = self.connection.abort_on(
|
||||||
|
'disconnection', self.derive_ltk()
|
||||||
|
)
|
||||||
elif not self.sc:
|
elif not self.sc:
|
||||||
# Distribute the LTK, EDIV and RAND
|
# Distribute the LTK, EDIV and RAND
|
||||||
if self.initiator_key_distribution & SMP_ENC_KEY_DISTRIBUTION_FLAG:
|
if self.initiator_key_distribution & SMP_ENC_KEY_DISTRIBUTION_FLAG:
|
||||||
@@ -995,7 +1023,9 @@ class Session:
|
|||||||
self.connection.transport == BT_BR_EDR_TRANSPORT
|
self.connection.transport == BT_BR_EDR_TRANSPORT
|
||||||
and self.responder_key_distribution & SMP_ENC_KEY_DISTRIBUTION_FLAG
|
and self.responder_key_distribution & SMP_ENC_KEY_DISTRIBUTION_FLAG
|
||||||
):
|
):
|
||||||
self.ctkd_task = self.connection.abort_on('disconnection', self.derive_ltk())
|
self.ctkd_task = self.connection.abort_on(
|
||||||
|
'disconnection', self.derive_ltk()
|
||||||
|
)
|
||||||
# Distribute the LTK, EDIV and RAND
|
# Distribute the LTK, EDIV and RAND
|
||||||
elif not self.sc:
|
elif not self.sc:
|
||||||
if self.responder_key_distribution & SMP_ENC_KEY_DISTRIBUTION_FLAG:
|
if self.responder_key_distribution & SMP_ENC_KEY_DISTRIBUTION_FLAG:
|
||||||
@@ -1055,13 +1085,14 @@ class Session:
|
|||||||
if key_distribution_flags & SMP_SIGN_KEY_DISTRIBUTION_FLAG != 0:
|
if key_distribution_flags & SMP_SIGN_KEY_DISTRIBUTION_FLAG != 0:
|
||||||
self.peer_expected_distributions.append(SMP_Signing_Information_Command)
|
self.peer_expected_distributions.append(SMP_Signing_Information_Command)
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'expecting distributions: {[c.__name__ for c in self.peer_expected_distributions]}'
|
'expecting distributions: '
|
||||||
|
f'{[c.__name__ for c in self.peer_expected_distributions]}'
|
||||||
)
|
)
|
||||||
|
|
||||||
def check_key_distribution(self, command_class):
|
def check_key_distribution(self, command_class):
|
||||||
# First, check that the connection is encrypted
|
# First, check that the connection is encrypted
|
||||||
if not self.connection.is_encrypted:
|
if not self.connection.is_encrypted:
|
||||||
logger.warn(
|
logger.warning(
|
||||||
color('received key distribution on a non-encrypted connection', 'red')
|
color('received key distribution on a non-encrypted connection', 'red')
|
||||||
)
|
)
|
||||||
self.send_pairing_failed(SMP_UNSPECIFIED_REASON_ERROR)
|
self.send_pairing_failed(SMP_UNSPECIFIED_REASON_ERROR)
|
||||||
@@ -1071,14 +1102,16 @@ class Session:
|
|||||||
if command_class in self.peer_expected_distributions:
|
if command_class in self.peer_expected_distributions:
|
||||||
self.peer_expected_distributions.remove(command_class)
|
self.peer_expected_distributions.remove(command_class)
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'remaining distributions: {[c.__name__ for c in self.peer_expected_distributions]}'
|
'remaining distributions: '
|
||||||
|
f'{[c.__name__ for c in self.peer_expected_distributions]}'
|
||||||
)
|
)
|
||||||
if not self.peer_expected_distributions:
|
if not self.peer_expected_distributions:
|
||||||
self.on_peer_key_distribution_complete()
|
self.on_peer_key_distribution_complete()
|
||||||
else:
|
else:
|
||||||
logger.warn(
|
logger.warning(
|
||||||
color(
|
color(
|
||||||
f'!!! unexpected key distribution command: {command_class.__name__}',
|
'!!! unexpected key distribution command: '
|
||||||
|
f'{command_class.__name__}',
|
||||||
'red',
|
'red',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -1094,7 +1127,7 @@ class Session:
|
|||||||
# Wait for the pairing process to finish
|
# Wait for the pairing process to finish
|
||||||
await self.connection.abort_on('disconnection', self.pairing_result)
|
await self.connection.abort_on('disconnection', self.pairing_result)
|
||||||
|
|
||||||
def on_disconnection(self, reason):
|
def on_disconnection(self, _):
|
||||||
self.connection.remove_listener('disconnection', self.on_disconnection)
|
self.connection.remove_listener('disconnection', self.on_disconnection)
|
||||||
self.connection.remove_listener(
|
self.connection.remove_listener(
|
||||||
'connection_encryption_change', self.on_connection_encryption_change
|
'connection_encryption_change', self.on_connection_encryption_change
|
||||||
@@ -1131,8 +1164,8 @@ class Session:
|
|||||||
|
|
||||||
if self.completed:
|
if self.completed:
|
||||||
return
|
return
|
||||||
else:
|
|
||||||
self.completed = True
|
self.completed = True
|
||||||
|
|
||||||
if self.pairing_result is not None and not self.pairing_result.done():
|
if self.pairing_result is not None and not self.pairing_result.done():
|
||||||
self.pairing_result.set_result(None)
|
self.pairing_result.set_result(None)
|
||||||
@@ -1192,8 +1225,8 @@ class Session:
|
|||||||
|
|
||||||
if self.completed:
|
if self.completed:
|
||||||
return
|
return
|
||||||
else:
|
|
||||||
self.completed = True
|
self.completed = True
|
||||||
|
|
||||||
error = ProtocolError(reason, 'smp', error_name(reason))
|
error = ProtocolError(reason, 'smp', error_name(reason))
|
||||||
if self.pairing_result is not None and not self.pairing_result.done():
|
if self.pairing_result is not None and not self.pairing_result.done():
|
||||||
@@ -1217,7 +1250,9 @@ class Session:
|
|||||||
logger.error(color('SMP command not handled???', 'red'))
|
logger.error(color('SMP command not handled???', 'red'))
|
||||||
|
|
||||||
def on_smp_pairing_request_command(self, command):
|
def on_smp_pairing_request_command(self, command):
|
||||||
self.connection.abort_on('disconnection', self.on_smp_pairing_request_command_async(command))
|
self.connection.abort_on(
|
||||||
|
'disconnection', self.on_smp_pairing_request_command_async(command)
|
||||||
|
)
|
||||||
|
|
||||||
async def on_smp_pairing_request_command_async(self, command):
|
async def on_smp_pairing_request_command_async(self, command):
|
||||||
# Check if the request should proceed
|
# Check if the request should proceed
|
||||||
@@ -1237,7 +1272,7 @@ class Session:
|
|||||||
|
|
||||||
# Check for OOB
|
# Check for OOB
|
||||||
if command.oob_data_flag != 0:
|
if command.oob_data_flag != 0:
|
||||||
self.terminate(SMP_OOB_NOT_AVAILABLE_ERROR)
|
self.send_pairing_failed(SMP_OOB_NOT_AVAILABLE_ERROR)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Decide which pairing method to use
|
# Decide which pairing method to use
|
||||||
@@ -1281,7 +1316,7 @@ class Session:
|
|||||||
|
|
||||||
def on_smp_pairing_response_command(self, command):
|
def on_smp_pairing_response_command(self, command):
|
||||||
if self.is_responder:
|
if self.is_responder:
|
||||||
logger.warn(color('received pairing response as a responder', 'red'))
|
logger.warning(color('received pairing response as a responder', 'red'))
|
||||||
return
|
return
|
||||||
|
|
||||||
# Save the response
|
# Save the response
|
||||||
@@ -1330,7 +1365,7 @@ class Session:
|
|||||||
else:
|
else:
|
||||||
self.send_pairing_confirm_command()
|
self.send_pairing_confirm_command()
|
||||||
|
|
||||||
def on_smp_pairing_confirm_command_legacy(self, command):
|
def on_smp_pairing_confirm_command_legacy(self, _):
|
||||||
if self.is_initiator:
|
if self.is_initiator:
|
||||||
self.send_pairing_random_command()
|
self.send_pairing_random_command()
|
||||||
else:
|
else:
|
||||||
@@ -1340,11 +1375,8 @@ class Session:
|
|||||||
else:
|
else:
|
||||||
self.send_pairing_confirm_command()
|
self.send_pairing_confirm_command()
|
||||||
|
|
||||||
def on_smp_pairing_confirm_command_secure_connections(self, command):
|
def on_smp_pairing_confirm_command_secure_connections(self, _):
|
||||||
if (
|
if self.pairing_method in (self.JUST_WORKS, self.NUMERIC_COMPARISON):
|
||||||
self.pairing_method == self.JUST_WORKS
|
|
||||||
or self.pairing_method == self.NUMERIC_COMPARISON
|
|
||||||
):
|
|
||||||
if self.is_initiator:
|
if self.is_initiator:
|
||||||
self.r = crypto.r()
|
self.r = crypto.r()
|
||||||
self.send_pairing_random_command()
|
self.send_pairing_random_command()
|
||||||
@@ -1397,11 +1429,9 @@ class Session:
|
|||||||
self.send_pairing_random_command()
|
self.send_pairing_random_command()
|
||||||
|
|
||||||
def on_smp_pairing_random_command_secure_connections(self, command):
|
def on_smp_pairing_random_command_secure_connections(self, command):
|
||||||
|
# pylint: disable=too-many-return-statements
|
||||||
if self.is_initiator:
|
if self.is_initiator:
|
||||||
if (
|
if self.pairing_method in (self.JUST_WORKS, self.NUMERIC_COMPARISON):
|
||||||
self.pairing_method == self.JUST_WORKS
|
|
||||||
or self.pairing_method == self.NUMERIC_COMPARISON
|
|
||||||
):
|
|
||||||
# Check that the random value matches what was committed to earlier
|
# Check that the random value matches what was committed to earlier
|
||||||
confirm_verifier = crypto.f4(
|
confirm_verifier = crypto.f4(
|
||||||
self.pkb, self.pka, command.random_value, bytes([0])
|
self.pkb, self.pka, command.random_value, bytes([0])
|
||||||
@@ -1432,10 +1462,7 @@ class Session:
|
|||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
if (
|
if self.pairing_method in (self.JUST_WORKS, self.NUMERIC_COMPARISON):
|
||||||
self.pairing_method == self.JUST_WORKS
|
|
||||||
or self.pairing_method == self.NUMERIC_COMPARISON
|
|
||||||
):
|
|
||||||
self.send_pairing_random_command()
|
self.send_pairing_random_command()
|
||||||
elif self.pairing_method == self.PASSKEY:
|
elif self.pairing_method == self.PASSKEY:
|
||||||
# Check that the random value matches what was committed to earlier
|
# Check that the random value matches what was committed to earlier
|
||||||
@@ -1467,10 +1494,7 @@ class Session:
|
|||||||
(mac_key, self.ltk) = crypto.f5(self.dh_key, self.na, self.nb, a, b)
|
(mac_key, self.ltk) = crypto.f5(self.dh_key, self.na, self.nb, a, b)
|
||||||
|
|
||||||
# Compute the DH Key checks
|
# Compute the DH Key checks
|
||||||
if (
|
if self.pairing_method in (self.JUST_WORKS, self.NUMERIC_COMPARISON):
|
||||||
self.pairing_method == self.JUST_WORKS
|
|
||||||
or self.pairing_method == self.NUMERIC_COMPARISON
|
|
||||||
):
|
|
||||||
ra = bytes(16)
|
ra = bytes(16)
|
||||||
rb = ra
|
rb = ra
|
||||||
elif self.pairing_method == self.PASSKEY:
|
elif self.pairing_method == self.PASSKEY:
|
||||||
@@ -1495,10 +1519,7 @@ class Session:
|
|||||||
self.wait_before_continuing.set_result(None)
|
self.wait_before_continuing.set_result(None)
|
||||||
|
|
||||||
# Prompt the user for confirmation if needed
|
# Prompt the user for confirmation if needed
|
||||||
if (
|
if self.pairing_method in (self.JUST_WORKS, self.NUMERIC_COMPARISON):
|
||||||
self.pairing_method == self.JUST_WORKS
|
|
||||||
or self.pairing_method == self.NUMERIC_COMPARISON
|
|
||||||
):
|
|
||||||
# Compute the 6-digit code
|
# Compute the 6-digit code
|
||||||
code = crypto.g2(self.pka, self.pkb, self.na, self.nb) % 1000000
|
code = crypto.g2(self.pka, self.pkb, self.na, self.nb) % 1000000
|
||||||
|
|
||||||
@@ -1547,10 +1568,7 @@ class Session:
|
|||||||
else:
|
else:
|
||||||
self.send_public_key_command()
|
self.send_public_key_command()
|
||||||
|
|
||||||
if (
|
if self.pairing_method in (self.JUST_WORKS, self.NUMERIC_COMPARISON):
|
||||||
self.pairing_method == self.JUST_WORKS
|
|
||||||
or self.pairing_method == self.NUMERIC_COMPARISON
|
|
||||||
):
|
|
||||||
# We can now send the confirmation value
|
# We can now send the confirmation value
|
||||||
self.send_pairing_confirm_command()
|
self.send_pairing_confirm_command()
|
||||||
|
|
||||||
@@ -1616,7 +1634,8 @@ class Manager(EventEmitter):
|
|||||||
|
|
||||||
def send_command(self, connection, command):
|
def send_command(self, connection, command):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'>>> Sending SMP Command on connection [0x{connection.handle:04X}] {connection.peer_address}: {command}'
|
f'>>> Sending SMP Command on connection [0x{connection.handle:04X}] '
|
||||||
|
f'{connection.peer_address}: {command}'
|
||||||
)
|
)
|
||||||
cid = SMP_BR_CID if connection.transport == BT_BR_EDR_TRANSPORT else SMP_CID
|
cid = SMP_BR_CID if connection.transport == BT_BR_EDR_TRANSPORT else SMP_CID
|
||||||
connection.send_l2cap_pdu(cid, command.to_bytes())
|
connection.send_l2cap_pdu(cid, command.to_bytes())
|
||||||
@@ -1638,7 +1657,8 @@ class Manager(EventEmitter):
|
|||||||
# Parse the L2CAP payload into an SMP Command object
|
# Parse the L2CAP payload into an SMP Command object
|
||||||
command = SMP_Command.from_bytes(pdu)
|
command = SMP_Command.from_bytes(pdu)
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'<<< Received SMP Command on connection [0x{connection.handle:04X}] {connection.peer_address}: {command}'
|
f'<<< Received SMP Command on connection [0x{connection.handle:04X}] '
|
||||||
|
f'{connection.peer_address}: {command}'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Delegate the handling of the command to the session
|
# Delegate the handling of the command to the session
|
||||||
@@ -1684,7 +1704,7 @@ class Manager(EventEmitter):
|
|||||||
try:
|
try:
|
||||||
await self.device.keystore.update(str(identity_address), keys)
|
await self.device.keystore.update(str(identity_address), keys)
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
logger.warn(f'!!! error while storing keys: {error}')
|
logger.warning(f'!!! error while storing keys: {error}')
|
||||||
|
|
||||||
self.device.abort_on('flush', store_keys())
|
self.device.abort_on('flush', store_keys())
|
||||||
|
|
||||||
@@ -1702,3 +1722,5 @@ class Manager(EventEmitter):
|
|||||||
def get_long_term_key(self, connection, rand, ediv):
|
def get_long_term_key(self, connection, rand, ediv):
|
||||||
if session := self.sessions.get(connection.handle):
|
if session := self.sessions.get(connection.handle):
|
||||||
return session.get_long_term_key(rand, ediv)
|
return session.get_long_term_key(rand, ediv)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|||||||
@@ -35,61 +35,76 @@ async def open_transport(name):
|
|||||||
Where <parameters> depend on the type (and may be empty for some types).
|
Where <parameters> depend on the type (and may be empty for some types).
|
||||||
The supported types are: serial,udp,tcp,pty,usb
|
The supported types are: serial,udp,tcp,pty,usb
|
||||||
'''
|
'''
|
||||||
|
# pylint: disable=import-outside-toplevel
|
||||||
|
# pylint: disable=too-many-return-statements
|
||||||
|
|
||||||
scheme, *spec = name.split(':', 1)
|
scheme, *spec = name.split(':', 1)
|
||||||
if scheme == 'serial' and spec:
|
if scheme == 'serial' and spec:
|
||||||
from .serial import open_serial_transport
|
from .serial import open_serial_transport
|
||||||
|
|
||||||
return await open_serial_transport(spec[0])
|
return await open_serial_transport(spec[0])
|
||||||
elif scheme == 'udp' and spec:
|
|
||||||
|
if scheme == 'udp' and spec:
|
||||||
from .udp import open_udp_transport
|
from .udp import open_udp_transport
|
||||||
|
|
||||||
return await open_udp_transport(spec[0])
|
return await open_udp_transport(spec[0])
|
||||||
elif scheme == 'tcp-client' and spec:
|
|
||||||
|
if scheme == 'tcp-client' and spec:
|
||||||
from .tcp_client import open_tcp_client_transport
|
from .tcp_client import open_tcp_client_transport
|
||||||
|
|
||||||
return await open_tcp_client_transport(spec[0])
|
return await open_tcp_client_transport(spec[0])
|
||||||
elif scheme == 'tcp-server' and spec:
|
|
||||||
|
if scheme == 'tcp-server' and spec:
|
||||||
from .tcp_server import open_tcp_server_transport
|
from .tcp_server import open_tcp_server_transport
|
||||||
|
|
||||||
return await open_tcp_server_transport(spec[0])
|
return await open_tcp_server_transport(spec[0])
|
||||||
elif scheme == 'ws-client' and spec:
|
|
||||||
|
if scheme == 'ws-client' and spec:
|
||||||
from .ws_client import open_ws_client_transport
|
from .ws_client import open_ws_client_transport
|
||||||
|
|
||||||
return await open_ws_client_transport(spec[0])
|
return await open_ws_client_transport(spec[0])
|
||||||
elif scheme == 'ws-server' and spec:
|
|
||||||
|
if scheme == 'ws-server' and spec:
|
||||||
from .ws_server import open_ws_server_transport
|
from .ws_server import open_ws_server_transport
|
||||||
|
|
||||||
return await open_ws_server_transport(spec[0])
|
return await open_ws_server_transport(spec[0])
|
||||||
elif scheme == 'pty':
|
|
||||||
|
if scheme == 'pty':
|
||||||
from .pty import open_pty_transport
|
from .pty import open_pty_transport
|
||||||
|
|
||||||
return await open_pty_transport(spec[0] if spec else None)
|
return await open_pty_transport(spec[0] if spec else None)
|
||||||
elif scheme == 'file':
|
|
||||||
|
if scheme == 'file':
|
||||||
from .file import open_file_transport
|
from .file import open_file_transport
|
||||||
|
|
||||||
return await open_file_transport(spec[0] if spec else None)
|
return await open_file_transport(spec[0] if spec else None)
|
||||||
elif scheme == 'vhci':
|
|
||||||
|
if scheme == 'vhci':
|
||||||
from .vhci import open_vhci_transport
|
from .vhci import open_vhci_transport
|
||||||
|
|
||||||
return await open_vhci_transport(spec[0] if spec else None)
|
return await open_vhci_transport(spec[0] if spec else None)
|
||||||
elif scheme == 'hci-socket':
|
|
||||||
|
if scheme == 'hci-socket':
|
||||||
from .hci_socket import open_hci_socket_transport
|
from .hci_socket import open_hci_socket_transport
|
||||||
|
|
||||||
return await open_hci_socket_transport(spec[0] if spec else None)
|
return await open_hci_socket_transport(spec[0] if spec else None)
|
||||||
elif scheme == 'usb':
|
|
||||||
|
if scheme == 'usb':
|
||||||
from .usb import open_usb_transport
|
from .usb import open_usb_transport
|
||||||
|
|
||||||
return await open_usb_transport(spec[0] if spec else None)
|
return await open_usb_transport(spec[0] if spec else None)
|
||||||
elif scheme == 'pyusb':
|
|
||||||
|
if scheme == 'pyusb':
|
||||||
from .pyusb import open_pyusb_transport
|
from .pyusb import open_pyusb_transport
|
||||||
|
|
||||||
return await open_pyusb_transport(spec[0] if spec else None)
|
return await open_pyusb_transport(spec[0] if spec else None)
|
||||||
elif scheme == 'android-emulator':
|
|
||||||
|
if scheme == 'android-emulator':
|
||||||
from .android_emulator import open_android_emulator_transport
|
from .android_emulator import open_android_emulator_transport
|
||||||
|
|
||||||
return await open_android_emulator_transport(spec[0] if spec else None)
|
return await open_android_emulator_transport(spec[0] if spec else None)
|
||||||
else:
|
|
||||||
raise ValueError('unknown transport scheme')
|
raise ValueError('unknown transport scheme')
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -104,5 +119,5 @@ async def open_transport_or_link(name):
|
|||||||
link.close()
|
link.close()
|
||||||
|
|
||||||
return LinkTransport(controller, AsyncPipeSink(controller))
|
return LinkTransport(controller, AsyncPipeSink(controller))
|
||||||
else:
|
|
||||||
return await open_transport(name)
|
return await open_transport(name)
|
||||||
|
|||||||
@@ -65,9 +65,12 @@ class PacketPump:
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class PacketParser:
|
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
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
# pylint: disable=attribute-defined-outside-init
|
||||||
|
|
||||||
NEED_TYPE = 0
|
NEED_TYPE = 0
|
||||||
NEED_LENGTH = 1
|
NEED_LENGTH = 1
|
||||||
NEED_BODY = 2
|
NEED_BODY = 2
|
||||||
@@ -278,7 +281,7 @@ class PumpedPacketSource(ParserSource):
|
|||||||
logger.debug('source pump task done')
|
logger.debug('source pump task done')
|
||||||
break
|
break
|
||||||
except Exception as error:
|
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)
|
self.terminated.set_result(error)
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -309,7 +312,7 @@ class PumpedPacketSink:
|
|||||||
logger.debug('sink pump task done')
|
logger.debug('sink pump task done')
|
||||||
break
|
break
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
logger.warn(f'exception while sending packet: {error}')
|
logger.warning(f'exception while sending packet: {error}')
|
||||||
break
|
break
|
||||||
|
|
||||||
self.pump_task = asyncio.create_task(pump_packets())
|
self.pump_task = asyncio.create_task(pump_packets())
|
||||||
|
|||||||
@@ -30,8 +30,9 @@ logger = logging.getLogger(__name__)
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
async def open_file_transport(spec):
|
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).
|
Open a File transport (typically not for a real file, but for a PTY or other unix
|
||||||
The parameter string is the path of the file to open
|
virtual files).
|
||||||
|
The parameter string is the path of the file to open.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
# Open the file
|
# Open the file
|
||||||
@@ -39,12 +40,12 @@ async def open_file_transport(spec):
|
|||||||
|
|
||||||
# Setup reading
|
# Setup reading
|
||||||
read_transport, packet_source = await asyncio.get_running_loop().connect_read_pipe(
|
read_transport, packet_source = await asyncio.get_running_loop().connect_read_pipe(
|
||||||
lambda: StreamPacketSource(), file
|
StreamPacketSource, file
|
||||||
)
|
)
|
||||||
|
|
||||||
# Setup writing
|
# Setup writing
|
||||||
write_transport, _ = await asyncio.get_running_loop().connect_write_pipe(
|
write_transport, _ = await asyncio.get_running_loop().connect_write_pipe(
|
||||||
lambda: asyncio.BaseProtocol(), file
|
asyncio.BaseProtocol, file
|
||||||
)
|
)
|
||||||
packet_sink = StreamPacketSink(write_transport)
|
packet_sink = StreamPacketSink(write_transport)
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ async def open_hci_socket_transport(spec):
|
|||||||
or a 0-based integer to indicate the adapter number.
|
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
|
# Create a raw HCI socket
|
||||||
try:
|
try:
|
||||||
@@ -49,10 +49,12 @@ async def open_hci_socket_transport(spec):
|
|||||||
socket.SOCK_RAW | socket.SOCK_NONBLOCK,
|
socket.SOCK_RAW | socket.SOCK_NONBLOCK,
|
||||||
socket.BTPROTO_HCI,
|
socket.BTPROTO_HCI,
|
||||||
)
|
)
|
||||||
except AttributeError:
|
except AttributeError as error:
|
||||||
# Not supported on this platform
|
# Not supported on this platform
|
||||||
logger.info("HCI sockets 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
|
# Compute the adapter index
|
||||||
if spec is None:
|
if spec is None:
|
||||||
@@ -66,13 +68,19 @@ async def open_hci_socket_transport(spec):
|
|||||||
try:
|
try:
|
||||||
ctypes.cdll.LoadLibrary('libc.so.6')
|
ctypes.cdll.LoadLibrary('libc.so.6')
|
||||||
libc = ctypes.CDLL('libc.so.6', use_errno=True)
|
libc = ctypes.CDLL('libc.so.6', use_errno=True)
|
||||||
except OSError:
|
except OSError as error:
|
||||||
logger.info("HCI sockets 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
|
||||||
libc.bind.argtypes = (ctypes.c_int, ctypes.POINTER(ctypes.c_char), ctypes.c_int)
|
libc.bind.argtypes = (ctypes.c_int, ctypes.POINTER(ctypes.c_char), ctypes.c_int)
|
||||||
libc.bind.restype = ctypes.c_int
|
libc.bind.restype = ctypes.c_int
|
||||||
bind_address = struct.pack(
|
bind_address = struct.pack(
|
||||||
'<HHH', socket.AF_BLUETOOTH, adapter_index, HCI_CHANNEL_USER
|
# pylint: disable=no-member
|
||||||
|
'<HHH',
|
||||||
|
socket.AF_BLUETOOTH,
|
||||||
|
adapter_index,
|
||||||
|
HCI_CHANNEL_USER,
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
libc.bind(
|
libc.bind(
|
||||||
@@ -85,9 +93,9 @@ async def open_hci_socket_transport(spec):
|
|||||||
raise IOError(ctypes.get_errno(), os.strerror(ctypes.get_errno()))
|
raise IOError(ctypes.get_errno(), os.strerror(ctypes.get_errno()))
|
||||||
|
|
||||||
class HciSocketSource(ParserSource):
|
class HciSocketSource(ParserSource):
|
||||||
def __init__(self, socket):
|
def __init__(self, hci_socket):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.socket = socket
|
self.socket = hci_socket
|
||||||
asyncio.get_running_loop().add_reader(
|
asyncio.get_running_loop().add_reader(
|
||||||
socket.fileno(), self.recv_until_would_block
|
socket.fileno(), self.recv_until_would_block
|
||||||
)
|
)
|
||||||
@@ -107,8 +115,8 @@ async def open_hci_socket_transport(spec):
|
|||||||
asyncio.get_running_loop().remove_reader(self.socket.fileno())
|
asyncio.get_running_loop().remove_reader(self.socket.fileno())
|
||||||
|
|
||||||
class HciSocketSink:
|
class HciSocketSink:
|
||||||
def __init__(self, socket):
|
def __init__(self, hci_socket):
|
||||||
self.socket = socket
|
self.socket = hci_socket
|
||||||
self.packets = collections.deque()
|
self.packets = collections.deque()
|
||||||
self.writer_added = False
|
self.writer_added = False
|
||||||
|
|
||||||
@@ -127,10 +135,13 @@ async def open_hci_socket_transport(spec):
|
|||||||
break
|
break
|
||||||
|
|
||||||
if self.packets:
|
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:
|
if not self.writer_added:
|
||||||
asyncio.get_running_loop().add_writer(
|
asyncio.get_running_loop().add_writer(
|
||||||
socket.fileno(), self.send_until_would_block
|
# pylint: disable=no-member
|
||||||
|
socket.fileno(),
|
||||||
|
self.send_until_would_block,
|
||||||
)
|
)
|
||||||
self.writer_added = True
|
self.writer_added = True
|
||||||
else:
|
else:
|
||||||
@@ -148,9 +159,9 @@ async def open_hci_socket_transport(spec):
|
|||||||
asyncio.get_running_loop().remove_writer(self.socket.fileno())
|
asyncio.get_running_loop().remove_writer(self.socket.fileno())
|
||||||
|
|
||||||
class HciSocketTransport(Transport):
|
class HciSocketTransport(Transport):
|
||||||
def __init__(self, socket, source, sink):
|
def __init__(self, hci_socket, source, sink):
|
||||||
super().__init__(source, sink)
|
super().__init__(source, sink)
|
||||||
self.socket = socket
|
self.socket = hci_socket
|
||||||
|
|
||||||
async def close(self):
|
async def close(self):
|
||||||
logger.debug('closing HCI socket transport')
|
logger.debug('closing HCI socket transport')
|
||||||
|
|||||||
@@ -47,11 +47,11 @@ async def open_pty_transport(spec):
|
|||||||
tty.setraw(replica)
|
tty.setraw(replica)
|
||||||
|
|
||||||
read_transport, packet_source = await asyncio.get_running_loop().connect_read_pipe(
|
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(
|
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)
|
packet_sink = StreamPacketSink(write_transport)
|
||||||
|
|
||||||
|
|||||||
@@ -17,11 +17,12 @@
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
import libusb_package
|
import libusb_package
|
||||||
import usb.core
|
import usb.core
|
||||||
import usb.util
|
import usb.util
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
from colors import color
|
from colors import color
|
||||||
|
|
||||||
from .common import Transport, ParserSource
|
from .common import Transport, ParserSource
|
||||||
@@ -49,6 +50,7 @@ async def open_pyusb_transport(spec):
|
|||||||
04b4:f901 --> the BT USB dongle with vendor=04b4 and product=f901
|
04b4:f901 --> the BT USB dongle with vendor=04b4 and product=f901
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
# pylint: disable=invalid-name
|
||||||
USB_RECIPIENT_DEVICE = 0x00
|
USB_RECIPIENT_DEVICE = 0x00
|
||||||
USB_REQUEST_TYPE_CLASS = 0x01 << 5
|
USB_REQUEST_TYPE_CLASS = 0x01 << 5
|
||||||
USB_ENDPOINT_EVENTS_IN = 0x81
|
USB_ENDPOINT_EVENTS_IN = 0x81
|
||||||
@@ -109,7 +111,7 @@ async def open_pyusb_transport(spec):
|
|||||||
def run(self):
|
def run(self):
|
||||||
while self.stop_event is None:
|
while self.stop_event is None:
|
||||||
time.sleep(1)
|
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):
|
class UsbPacketSource(asyncio.Protocol, ParserSource):
|
||||||
def __init__(self, device, sco_enabled):
|
def __init__(self, device, sco_enabled):
|
||||||
@@ -117,6 +119,7 @@ async def open_pyusb_transport(spec):
|
|||||||
self.device = device
|
self.device = device
|
||||||
self.loop = asyncio.get_running_loop()
|
self.loop = asyncio.get_running_loop()
|
||||||
self.queue = asyncio.Queue()
|
self.queue = asyncio.Queue()
|
||||||
|
self.dequeue_task = None
|
||||||
self.event_thread = threading.Thread(
|
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)
|
||||||
)
|
)
|
||||||
@@ -135,8 +138,8 @@ async def open_pyusb_transport(spec):
|
|||||||
)
|
)
|
||||||
self.sco_thread.stop_event = None
|
self.sco_thread.stop_event = None
|
||||||
|
|
||||||
def data_received(self, packet):
|
def data_received(self, data):
|
||||||
self.parser.feed_data(packet)
|
self.parser.feed_data(data)
|
||||||
|
|
||||||
def enqueue(self, packet):
|
def enqueue(self, packet):
|
||||||
self.queue.put_nowait(packet)
|
self.queue.put_nowait(packet)
|
||||||
@@ -180,16 +183,17 @@ async def open_pyusb_transport(spec):
|
|||||||
except usb.core.USBTimeoutError:
|
except usb.core.USBTimeoutError:
|
||||||
continue
|
continue
|
||||||
except usb.core.USBError:
|
except usb.core.USBError:
|
||||||
# Don't log this: because pyusb doesn't really support multiple threads
|
# Don't log this: because pyusb doesn't really support multiple
|
||||||
# reading at the same time, we can get occasional USBError(errno=5)
|
# threads reading at the same time, we can get occasional
|
||||||
# Input/Output errors reported, but they seem to be harmless.
|
# 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,
|
# Until support for async or multi-thread support is added to pyusb,
|
||||||
# we'll just live with this as is...
|
# we'll just live with this as is...
|
||||||
# logger.warning(f'USB read error: {error}')
|
# logger.warning(f'USB read error: {error}')
|
||||||
time.sleep(1) # Sleep one second to avoid busy looping
|
time.sleep(1) # Sleep one second to avoid busy looping
|
||||||
|
|
||||||
stop_event = current_thread.stop_event
|
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):
|
class UsbTransport(Transport):
|
||||||
def __init__(self, device, source, sink):
|
def __init__(self, device, source, sink):
|
||||||
@@ -243,6 +247,7 @@ async def open_pyusb_transport(spec):
|
|||||||
|
|
||||||
# Select an alternate setting for SCO, if available
|
# Select an alternate setting for SCO, if available
|
||||||
sco_enabled = False
|
sco_enabled = False
|
||||||
|
# pylint: disable=line-too-long
|
||||||
# NOTE: this is disabled for now, because SCO with alternate settings is broken,
|
# NOTE: this is disabled for now, because SCO with alternate settings is broken,
|
||||||
# see: https://github.com/libusb/libusb/issues/36
|
# see: https://github.com/libusb/libusb/issues/36
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ async def open_serial_transport(spec):
|
|||||||
device = spec
|
device = spec
|
||||||
serial_transport, packet_source = await serial_asyncio.create_serial_connection(
|
serial_transport, packet_source = await serial_asyncio.create_serial_connection(
|
||||||
asyncio.get_running_loop(),
|
asyncio.get_running_loop(),
|
||||||
lambda: StreamPacketSource(),
|
StreamPacketSource,
|
||||||
device,
|
device,
|
||||||
baudrate=speed,
|
baudrate=speed,
|
||||||
rtscts=rtscts,
|
rtscts=rtscts,
|
||||||
|
|||||||
@@ -37,13 +37,13 @@ async def open_tcp_client_transport(spec):
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
class TcpPacketSource(StreamPacketSource):
|
class TcpPacketSource(StreamPacketSource):
|
||||||
def connection_lost(self, error):
|
def connection_lost(self, exc):
|
||||||
logger.debug(f'connection lost: {error}')
|
logger.debug(f'connection lost: {exc}')
|
||||||
self.terminated.set_result(error)
|
self.terminated.set_result(exc)
|
||||||
|
|
||||||
remote_host, remote_port = spec.split(':')
|
remote_host, remote_port = spec.split(':')
|
||||||
tcp_transport, packet_source = await asyncio.get_running_loop().create_connection(
|
tcp_transport, packet_source = await asyncio.get_running_loop().create_connection(
|
||||||
lambda: TcpPacketSource(),
|
TcpPacketSource,
|
||||||
host=remote_host,
|
host=remote_host,
|
||||||
port=int(remote_port),
|
port=int(remote_port),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -49,8 +49,8 @@ async def open_tcp_server_transport(spec):
|
|||||||
|
|
||||||
# Called when a new connection is established
|
# Called when a new connection is established
|
||||||
def connection_made(self, transport):
|
def connection_made(self, transport):
|
||||||
peername = transport.get_extra_info('peername')
|
peer_name = transport.get_extra_info('peer_name')
|
||||||
logger.debug('connection from {}'.format(peername))
|
logger.debug(f'connection from {peer_name}')
|
||||||
self.packet_sink.transport = transport
|
self.packet_sink.transport = transport
|
||||||
|
|
||||||
# Called when the client is disconnected
|
# Called when the client is disconnected
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ async def open_udp_transport(spec):
|
|||||||
udp_transport,
|
udp_transport,
|
||||||
packet_source,
|
packet_source,
|
||||||
) = await asyncio.get_running_loop().create_datagram_endpoint(
|
) = await asyncio.get_running_loop().create_datagram_endpoint(
|
||||||
lambda: UdpPacketSource(),
|
UdpPacketSource,
|
||||||
local_addr=(local_host, int(local_port)),
|
local_addr=(local_host, int(local_port)),
|
||||||
remote_addr=(remote_host, int(remote_port)),
|
remote_addr=(remote_host, int(remote_port)),
|
||||||
)
|
)
|
||||||
|
|||||||
+59
-23
@@ -17,12 +17,13 @@
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import libusb_package
|
|
||||||
import usb1
|
|
||||||
import threading
|
import threading
|
||||||
import collections
|
import collections
|
||||||
import ctypes
|
import ctypes
|
||||||
import platform
|
import platform
|
||||||
|
|
||||||
|
import libusb_package
|
||||||
|
import usb1
|
||||||
from colors import color
|
from colors import color
|
||||||
|
|
||||||
from .common import Transport, ParserSource
|
from .common import Transport, ParserSource
|
||||||
@@ -39,9 +40,9 @@ logger = logging.getLogger(__name__)
|
|||||||
def load_libusb():
|
def load_libusb():
|
||||||
'''
|
'''
|
||||||
Attempt to load the libusb-1.0 C library from libusb_package in site-packages.
|
Attempt to load the libusb-1.0 C library from libusb_package in site-packages.
|
||||||
If library exists, we create a DLL object and initialize the usb1 backend.
|
If the library exists, we create a DLL object and initialize the usb1 backend.
|
||||||
This only needs to be done once, but bufore a usb1.USBContext is created.
|
This only needs to be done once, but before a usb1.USBContext is created.
|
||||||
If library does not exists, do nothing and usb1 will search default system paths
|
If the library does not exists, do nothing and usb1 will search default system paths
|
||||||
when usb1.USBContext is created.
|
when usb1.USBContext is created.
|
||||||
'''
|
'''
|
||||||
if libusb_path := libusb_package.get_library_path():
|
if libusb_path := libusb_package.get_library_path():
|
||||||
@@ -49,6 +50,7 @@ def load_libusb():
|
|||||||
libusb_dll = dll_loader(libusb_path, use_errno=True, use_last_error=True)
|
libusb_dll = dll_loader(libusb_path, use_errno=True, use_last_error=True)
|
||||||
usb1.loadLibrary(libusb_dll)
|
usb1.loadLibrary(libusb_dll)
|
||||||
|
|
||||||
|
|
||||||
async def open_usb_transport(spec):
|
async def open_usb_transport(spec):
|
||||||
'''
|
'''
|
||||||
Open a USB transport.
|
Open a USB transport.
|
||||||
@@ -60,21 +62,26 @@ async def open_usb_transport(spec):
|
|||||||
With <index> as the 0-based index to select amongst all the devices that appear
|
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
|
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
|
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
|
/<serial-number> suffix or #<index> suffix max be specified when more than one
|
||||||
the same vendor and product identifiers are present.
|
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:
|
In addition, if the moniker ends with the symbol "!", the device will be used in
|
||||||
the first USB interface of the device will be used, regardless of the interface class/subclass.
|
"forced" mode:
|
||||||
This may be useful for some devices that use a custom class/subclass but may nonetheless work as-is.
|
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:
|
Examples:
|
||||||
0 --> the first BT USB dongle
|
0 --> the first BT USB dongle
|
||||||
04b4:f901 --> the BT USB dongle with vendor=04b4 and product=f901
|
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#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:0B05:17CB! --> the BT USB dongle vendor=0B05 and product=17CB, in "forced" mode.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
# pylint: disable=invalid-name
|
||||||
USB_RECIPIENT_DEVICE = 0x00
|
USB_RECIPIENT_DEVICE = 0x00
|
||||||
USB_REQUEST_TYPE_CLASS = 0x01 << 5
|
USB_REQUEST_TYPE_CLASS = 0x01 << 5
|
||||||
USB_DEVICE_CLASS_DEVICE = 0x00
|
USB_DEVICE_CLASS_DEVICE = 0x00
|
||||||
@@ -125,6 +132,7 @@ async def open_usb_transport(spec):
|
|||||||
status = transfer.getStatus()
|
status = transfer.getStatus()
|
||||||
# logger.debug(f'<<< USB out transfer callback: status={status}')
|
# logger.debug(f'<<< USB out transfer callback: status={status}')
|
||||||
|
|
||||||
|
# pylint: disable=no-member
|
||||||
if status == usb1.TRANSFER_COMPLETED:
|
if status == usb1.TRANSFER_COMPLETED:
|
||||||
self.loop.call_soon_threadsafe(self.on_packet_sent_)
|
self.loop.call_soon_threadsafe(self.on_packet_sent_)
|
||||||
elif status == usb1.TRANSFER_CANCELLED:
|
elif status == usb1.TRANSFER_CANCELLED:
|
||||||
@@ -165,15 +173,20 @@ async def open_usb_transport(spec):
|
|||||||
else:
|
else:
|
||||||
logger.warning(color(f'unsupported packet type {packet_type}', 'red'))
|
logger.warning(color(f'unsupported packet type {packet_type}', 'red'))
|
||||||
|
|
||||||
async def close(self):
|
def close(self):
|
||||||
self.closed = True
|
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
|
# Empty the packet queue so that we don't send any more data
|
||||||
self.packets.clear()
|
self.packets.clear()
|
||||||
|
|
||||||
# If we have a transfer in flight, cancel it
|
# If we have a transfer in flight, cancel it
|
||||||
if self.transfer.isSubmitted():
|
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:
|
try:
|
||||||
self.transfer.cancel()
|
self.transfer.cancel()
|
||||||
|
|
||||||
@@ -192,12 +205,15 @@ async def open_usb_transport(spec):
|
|||||||
self.events_in = events_in
|
self.events_in = events_in
|
||||||
self.loop = asyncio.get_running_loop()
|
self.loop = asyncio.get_running_loop()
|
||||||
self.queue = asyncio.Queue()
|
self.queue = asyncio.Queue()
|
||||||
|
self.dequeue_task = None
|
||||||
self.closed = False
|
self.closed = False
|
||||||
self.event_loop_done = self.loop.create_future()
|
self.event_loop_done = self.loop.create_future()
|
||||||
self.cancel_done = {
|
self.cancel_done = {
|
||||||
hci.HCI_EVENT_PACKET: self.loop.create_future(),
|
hci.HCI_EVENT_PACKET: self.loop.create_future(),
|
||||||
hci.HCI_ACL_DATA_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
|
# Create a thread to process events
|
||||||
self.event_thread = threading.Thread(target=self.run)
|
self.event_thread = threading.Thread(target=self.run)
|
||||||
@@ -228,8 +244,13 @@ async def open_usb_transport(spec):
|
|||||||
def on_packet_received(self, transfer):
|
def on_packet_received(self, transfer):
|
||||||
packet_type = transfer.getUserData()
|
packet_type = transfer.getUserData()
|
||||||
status = transfer.getStatus()
|
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:
|
if status == usb1.TRANSFER_COMPLETED:
|
||||||
packet = (
|
packet = (
|
||||||
bytes([packet_type])
|
bytes([packet_type])
|
||||||
@@ -263,6 +284,7 @@ async def open_usb_transport(spec):
|
|||||||
self.events_in_transfer.isSubmitted()
|
self.events_in_transfer.isSubmitted()
|
||||||
or self.acl_in_transfer.isSubmitted()
|
or self.acl_in_transfer.isSubmitted()
|
||||||
):
|
):
|
||||||
|
# pylint: disable=no-member
|
||||||
try:
|
try:
|
||||||
self.context.handleEvents()
|
self.context.handleEvents()
|
||||||
except usb1.USBErrorInterrupted:
|
except usb1.USBErrorInterrupted:
|
||||||
@@ -271,19 +293,26 @@ async def open_usb_transport(spec):
|
|||||||
logger.debug('USB event loop done')
|
logger.debug('USB event loop done')
|
||||||
self.loop.call_soon_threadsafe(self.event_loop_done.set_result, None)
|
self.loop.call_soon_threadsafe(self.event_loop_done.set_result, None)
|
||||||
|
|
||||||
async def close(self):
|
def close(self):
|
||||||
self.closed = True
|
self.closed = True
|
||||||
|
|
||||||
|
async def terminate(self):
|
||||||
|
if not self.closed:
|
||||||
|
self.close()
|
||||||
|
|
||||||
self.dequeue_task.cancel()
|
self.dequeue_task.cancel()
|
||||||
|
|
||||||
# Cancel the transfers
|
# Cancel the transfers
|
||||||
for transfer in (self.events_in_transfer, self.acl_in_transfer):
|
for transfer in (self.events_in_transfer, self.acl_in_transfer):
|
||||||
if transfer.isSubmitted():
|
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()
|
packet_type = transfer.getUserData()
|
||||||
try:
|
try:
|
||||||
transfer.cancel()
|
transfer.cancel()
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'waiting for IN[{packet_type}] transfer cancellation to be done...'
|
f'waiting for IN[{packet_type}] transfer cancellation '
|
||||||
|
'to be done...'
|
||||||
)
|
)
|
||||||
await self.cancel_done[packet_type]
|
await self.cancel_done[packet_type]
|
||||||
logger.debug(f'IN[{packet_type}] transfer cancellation done')
|
logger.debug(f'IN[{packet_type}] transfer cancellation done')
|
||||||
@@ -314,8 +343,10 @@ async def open_usb_transport(spec):
|
|||||||
sink.start()
|
sink.start()
|
||||||
|
|
||||||
async def close(self):
|
async def close(self):
|
||||||
await self.source.close()
|
self.source.close()
|
||||||
await self.sink.close()
|
self.sink.close()
|
||||||
|
await self.source.terminate()
|
||||||
|
await self.sink.terminate()
|
||||||
self.device.releaseInterface(self.interface)
|
self.device.releaseInterface(self.interface)
|
||||||
self.device.close()
|
self.device.close()
|
||||||
self.context.close()
|
self.context.close()
|
||||||
@@ -400,6 +431,7 @@ async def open_usb_transport(spec):
|
|||||||
|
|
||||||
# Look for the first interface with the right class and endpoints
|
# Look for the first interface with the right class and endpoints
|
||||||
def find_endpoints(device):
|
def find_endpoints(device):
|
||||||
|
# pylint: disable-next=too-many-nested-blocks
|
||||||
for (configuration_index, configuration) in enumerate(device):
|
for (configuration_index, configuration) in enumerate(device):
|
||||||
interface = None
|
interface = None
|
||||||
for interface in configuration:
|
for interface in configuration:
|
||||||
@@ -448,10 +480,13 @@ async def open_usb_transport(spec):
|
|||||||
acl_out,
|
acl_out,
|
||||||
events_in,
|
events_in,
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'skipping configuration {configuration_index + 1} / interface {setting.getNumber()}'
|
f'skipping configuration {configuration_index + 1} / '
|
||||||
)
|
f'interface {setting.getNumber()}'
|
||||||
|
)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
endpoints = find_endpoints(found)
|
endpoints = find_endpoints(found)
|
||||||
if endpoints is None:
|
if endpoints is None:
|
||||||
@@ -469,6 +504,7 @@ async def open_usb_transport(spec):
|
|||||||
device = found.open()
|
device = found.open()
|
||||||
|
|
||||||
# Auto-detach the kernel driver if supported
|
# Auto-detach the kernel driver if supported
|
||||||
|
# pylint: disable=no-member
|
||||||
if usb1.hasCapability(usb1.CAP_SUPPORTS_DETACH_KERNEL_DRIVER):
|
if usb1.hasCapability(usb1.CAP_SUPPORTS_DETACH_KERNEL_DRIVER):
|
||||||
try:
|
try:
|
||||||
logger.debug('auto-detaching kernel driver')
|
logger.debug('auto-detaching kernel driver')
|
||||||
|
|||||||
@@ -44,11 +44,13 @@ async def open_ws_server_transport(spec):
|
|||||||
source = ParserSource()
|
source = ParserSource()
|
||||||
sink = PumpedPacketSink(self.send_packet)
|
sink = PumpedPacketSink(self.send_packet)
|
||||||
self.connection = asyncio.get_running_loop().create_future()
|
self.connection = asyncio.get_running_loop().create_future()
|
||||||
|
self.server = None
|
||||||
|
|
||||||
super().__init__(source, sink)
|
super().__init__(source, sink)
|
||||||
|
|
||||||
async def serve(self, local_host, local_port):
|
async def serve(self, local_host, local_port):
|
||||||
self.sink.start()
|
self.sink.start()
|
||||||
|
# pylint: disable-next=no-member
|
||||||
self.server = await websockets.serve(
|
self.server = await websockets.serve(
|
||||||
ws_handler=self.on_connection,
|
ws_handler=self.on_connection,
|
||||||
host=local_host if local_host != '_' else None,
|
host=local_host if local_host != '_' else None,
|
||||||
@@ -58,15 +60,17 @@ async def open_ws_server_transport(spec):
|
|||||||
|
|
||||||
async def on_connection(self, connection):
|
async def on_connection(self, connection):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'new connection on {connection.local_address} from {connection.remote_address}'
|
f'new connection on {connection.local_address} '
|
||||||
|
f'from {connection.remote_address}'
|
||||||
)
|
)
|
||||||
self.connection.set_result(connection)
|
self.connection.set_result(connection)
|
||||||
|
# pylint: disable=no-member
|
||||||
try:
|
try:
|
||||||
async for packet in connection:
|
async for packet in connection:
|
||||||
if type(packet) is bytes:
|
if isinstance(packet, bytes):
|
||||||
self.source.parser.feed_data(packet)
|
self.source.parser.feed_data(packet)
|
||||||
else:
|
else:
|
||||||
logger.warn('discarding packet: not a BINARY frame')
|
logger.warning('discarding packet: not a BINARY frame')
|
||||||
except websockets.WebSocketException as error:
|
except websockets.WebSocketException as error:
|
||||||
logger.debug(f'exception while receiving packet: {error}')
|
logger.debug(f'exception while receiving packet: {error}')
|
||||||
|
|
||||||
|
|||||||
+5
-3
@@ -47,6 +47,7 @@ def composite_listener(cls):
|
|||||||
registers/deregisters all methods named `on_<event_name>` as a listener for
|
registers/deregisters all methods named `on_<event_name>` as a listener for
|
||||||
the <event_name> event with an emitter.
|
the <event_name> event with an emitter.
|
||||||
"""
|
"""
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
|
||||||
def register(self, emitter):
|
def register(self, emitter):
|
||||||
for method_name in dir(cls):
|
for method_name in dir(cls):
|
||||||
@@ -65,7 +66,6 @@ def composite_listener(cls):
|
|||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class AbortableEventEmitter(EventEmitter):
|
class AbortableEventEmitter(EventEmitter):
|
||||||
|
|
||||||
def abort_on(self, event: str, awaitable: Awaitable):
|
def abort_on(self, event: str, awaitable: Awaitable):
|
||||||
"""
|
"""
|
||||||
Set a coroutine or future to abort when an event occur.
|
Set a coroutine or future to abort when an event occur.
|
||||||
@@ -77,7 +77,7 @@ class AbortableEventEmitter(EventEmitter):
|
|||||||
def on_event(*_):
|
def on_event(*_):
|
||||||
msg = f'abort: {event} event occurred.'
|
msg = f'abort: {event} event occurred.'
|
||||||
if isinstance(future, asyncio.Task):
|
if isinstance(future, asyncio.Task):
|
||||||
# python prior to 3.9 does not support passing a message on `Task.cancel`
|
# python < 3.9 does not support passing a message on `Task.cancel`
|
||||||
if sys.version_info < (3, 9, 0):
|
if sys.version_info < (3, 9, 0):
|
||||||
future.cancel()
|
future.cancel()
|
||||||
else:
|
else:
|
||||||
@@ -105,6 +105,7 @@ class CompositeEventEmitter(AbortableEventEmitter):
|
|||||||
|
|
||||||
@listener.setter
|
@listener.setter
|
||||||
def listener(self, listener):
|
def listener(self, listener):
|
||||||
|
# pylint: disable=protected-access
|
||||||
if self._listener:
|
if self._listener:
|
||||||
# Call the deregistration methods for each base class that has them
|
# Call the deregistration methods for each base class that has them
|
||||||
for cls in self._listener.__class__.mro():
|
for cls in self._listener.__class__.mro():
|
||||||
@@ -168,7 +169,8 @@ class AsyncRunner:
|
|||||||
await coroutine
|
await coroutine
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f'{color("!!! Exception in wrapper:", "red")} {traceback.format_exc()}'
|
f'{color("!!! Exception in wrapper:", "red")} '
|
||||||
|
f'{traceback.format_exc()}'
|
||||||
)
|
)
|
||||||
|
|
||||||
asyncio.create_task(run())
|
asyncio.create_task(run())
|
||||||
|
|||||||
+3
-3
@@ -2,7 +2,7 @@ Bumble Documentation
|
|||||||
====================
|
====================
|
||||||
|
|
||||||
The documentation consists of a collection of markdown text files, with the root of the file
|
The documentation consists of a collection of markdown text files, with the root of the file
|
||||||
hierarchy at `docs/mkdocs/src`, starting with `docs/mkdocs/src/index.md`.
|
hierarchy at `docs/mkdocs/src`, starting with `docs/mkdocs/src/index.md`.
|
||||||
You can read the documentation as text, with any text viewer or your favorite markdown viewer,
|
You can read the documentation as text, with any text viewer or your favorite markdown viewer,
|
||||||
or generate a static HTML "site" using `mkdocs`, which you can then open with any browser.
|
or generate a static HTML "site" using `mkdocs`, which you can then open with any browser.
|
||||||
|
|
||||||
@@ -14,9 +14,9 @@ The `mkdocs` directory contains all the data (actual documentation) and metadata
|
|||||||
`mkdocs/mkdocs.yml` contains the site configuration.
|
`mkdocs/mkdocs.yml` contains the site configuration.
|
||||||
`mkdocs/src/` is the directory where the actual documentation text, in markdown format, is located.
|
`mkdocs/src/` is the directory where the actual documentation text, in markdown format, is located.
|
||||||
|
|
||||||
To build, from the project's root directory:
|
To build, from the project's root directory:
|
||||||
```
|
```
|
||||||
$ mkdocs build -f docs/mkdocs/mkdocs.yml
|
$ mkdocs build -f docs/mkdocs/mkdocs.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
You can then open `docs/mkdocs/site/index.html` with any web browser.
|
You can then open `docs/mkdocs/site/index.html` with any web browser.
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
{"date":644900643.85054696,"appVersion":"4.1.5","drawing":{"modificationDate":644894800.328192,"activeArtboardIndex":0,"settings":{"outlineMode":false,"isolateActiveLayer":false,"snapToEdges":false,"snapToPoints":false,"guidesVisible":true,"snapToGrid":false,"units":"Pixels","dimensionsVisible":true,"dynamicGuides":false,"isCMYKColorPreviewEnabled":false,"undoHistoryDisabled":false,"snapToGuides":true,"drawOnlyUsingPencil":false,"whiteBackground":false,"rulersVisible":true,"isTimeLapseWatermarkDisabled":false},"artboardPaths":["Artboard0.json"],"documentVersion":"unknown"}}
|
{"date":644900643.85054696,"appVersion":"4.1.5","drawing":{"modificationDate":644894800.328192,"activeArtboardIndex":0,"settings":{"outlineMode":false,"isolateActiveLayer":false,"snapToEdges":false,"snapToPoints":false,"guidesVisible":true,"snapToGrid":false,"units":"Pixels","dimensionsVisible":true,"dynamicGuides":false,"isCMYKColorPreviewEnabled":false,"undoHistoryDisabled":false,"snapToGuides":true,"drawOnlyUsingPencil":false,"whiteBackground":false,"rulersVisible":true,"isTimeLapseWatermarkDisabled":false},"artboardPaths":["Artboard0.json"],"documentVersion":"unknown"}}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"documentJSONFilename":"Document.json","undoHistoryJSONFilename":"UndoHistory.json","fileFormatVersion":0,"thumbnailImageFilename":"Thumbnail.png"}
|
{"documentJSONFilename":"Document.json","undoHistoryJSONFilename":"UndoHistory.json","fileFormatVersion":0,"thumbnailImageFilename":"Thumbnail.png"}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
{"date":644900741.09290397,"appVersion":"4.1.5","drawing":{"modificationDate":644894800.328192,"activeArtboardIndex":0,"settings":{"outlineMode":false,"isolateActiveLayer":false,"snapToEdges":false,"snapToPoints":false,"guidesVisible":true,"snapToGrid":false,"units":"Pixels","dimensionsVisible":true,"dynamicGuides":false,"isCMYKColorPreviewEnabled":false,"undoHistoryDisabled":false,"snapToGuides":true,"drawOnlyUsingPencil":false,"whiteBackground":false,"rulersVisible":true,"isTimeLapseWatermarkDisabled":false},"artboardPaths":["Artboard0.json"],"documentVersion":"unknown"}}
|
{"date":644900741.09290397,"appVersion":"4.1.5","drawing":{"modificationDate":644894800.328192,"activeArtboardIndex":0,"settings":{"outlineMode":false,"isolateActiveLayer":false,"snapToEdges":false,"snapToPoints":false,"guidesVisible":true,"snapToGrid":false,"units":"Pixels","dimensionsVisible":true,"dynamicGuides":false,"isCMYKColorPreviewEnabled":false,"undoHistoryDisabled":false,"snapToGuides":true,"drawOnlyUsingPencil":false,"whiteBackground":false,"rulersVisible":true,"isTimeLapseWatermarkDisabled":false},"artboardPaths":["Artboard0.json"],"documentVersion":"unknown"}}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"documentJSONFilename":"Document.json","undoHistoryJSONFilename":"UndoHistory.json","fileFormatVersion":0,"thumbnailImageFilename":"Thumbnail.png"}
|
{"documentJSONFilename":"Document.json","undoHistoryJSONFilename":"UndoHistory.json","fileFormatVersion":0,"thumbnailImageFilename":"Thumbnail.png"}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -7,6 +7,8 @@ nav:
|
|||||||
- Getting Started: getting_started.md
|
- Getting Started: getting_started.md
|
||||||
- Development:
|
- Development:
|
||||||
- Python Environments: development/python_environments.md
|
- Python Environments: development/python_environments.md
|
||||||
|
- Contributing: development/contributing.md
|
||||||
|
- Code Style: development/code_style.md
|
||||||
- Use Cases:
|
- Use Cases:
|
||||||
- Overview: use_cases/index.md
|
- Overview: use_cases/index.md
|
||||||
- Use Case 1: use_cases/use_case_1.md
|
- Use Case 1: use_cases/use_case_1.md
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ mkdocs == 1.4.0
|
|||||||
mkdocs-material == 8.5.6
|
mkdocs-material == 8.5.6
|
||||||
mkdocs-material-extensions == 1.0.3
|
mkdocs-material-extensions == 1.0.3
|
||||||
pymdown-extensions == 9.6
|
pymdown-extensions == 9.6
|
||||||
mkdocstrings-python == 0.7.1
|
mkdocstrings-python == 0.7.1
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
API EXAMPLES
|
API EXAMPLES
|
||||||
============
|
============
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
API DEVELOPER GUIDE
|
API DEVELOPER GUIDE
|
||||||
===================
|
===================
|
||||||
|
|||||||
@@ -16,4 +16,3 @@ Bumble Python API
|
|||||||
|
|
||||||
### HCI_Disconnect_Command
|
### HCI_Disconnect_Command
|
||||||
::: bumble.hci.HCI_Disconnect_Command
|
::: bumble.hci.HCI_Disconnect_Command
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
GOLDEN GATE BRIDGE
|
GOLDEN GATE BRIDGE
|
||||||
==================
|
==================
|
||||||
|
|||||||
@@ -28,5 +28,3 @@ a host that send custom HCI commands that the controller may not understand.
|
|||||||
(through which the communication with other virtual controllers will be mediated).
|
(through which the communication with other virtual controllers will be mediated).
|
||||||
|
|
||||||
NOTE: this assumes you're running a Link Relay on port `10723`.
|
NOTE: this assumes you're running a Link Relay on port `10723`.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,4 +11,3 @@ These include:
|
|||||||
* [Golden Gate Bridge](gg_bridge.md) - a bridge between GATT and UDP to use with the Golden Gate "stack tool"
|
* [Golden Gate Bridge](gg_bridge.md) - a bridge between GATT and UDP to use with the Golden Gate "stack tool"
|
||||||
* [Show](show.md) - Parse a file with HCI packets and print the details of each packet in a human readable form
|
* [Show](show.md) - Parse a file with HCI packets and print the details of each packet in a human readable form
|
||||||
* [Link Relay](link_relay.md) - WebSocket relay for virtual RemoteLink instances to communicate with each other.
|
* [Link Relay](link_relay.md) - WebSocket relay for virtual RemoteLink instances to communicate with each other.
|
||||||
|
|
||||||
|
|||||||
@@ -31,4 +31,3 @@ The WebSocket path used by a connecting client indicates which virtual "chat roo
|
|||||||
It is possible to connect to a "chat room" in a relay as an observer, rather than a virtual controller. In this case, a text-based console can be used to observe what is going on in the "chat room". Tools like [`wscat`](https://github.com/websockets/wscat#readme) or [`websocat`](https://github.com/vi/websocat) can be used for that.
|
It is possible to connect to a "chat room" in a relay as an observer, rather than a virtual controller. In this case, a text-based console can be used to observe what is going on in the "chat room". Tools like [`wscat`](https://github.com/websockets/wscat#readme) or [`websocat`](https://github.com/vi/websocat) can be used for that.
|
||||||
|
|
||||||
Example: `wscat --connect ws://localhost:10723/test`
|
Example: `wscat --connect ws://localhost:10723/test`
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ USB PROBE TOOL
|
|||||||
|
|
||||||
This tool lists all the USB devices, with details about each device.
|
This tool lists all the USB devices, with details about each device.
|
||||||
For each device, the different possible Bumble transport strings that can
|
For each device, the different possible Bumble transport strings that can
|
||||||
refer to it are listed.
|
refer to it are listed.
|
||||||
If the device is known to be a Bluetooth HCI device, its identifier is printed
|
If the device is known to be a Bluetooth HCI device, its identifier is printed
|
||||||
in reverse colors, and the transport names in cyan color.
|
in reverse colors, and the transport names in cyan color.
|
||||||
For other devices, regardless of their type, the transport names are printed
|
For other devices, regardless of their type, the transport names are printed
|
||||||
in red. Whether that device is actually a Bluetooth device or not depends on
|
in red. Whether that device is actually a Bluetooth device or not depends on
|
||||||
@@ -30,7 +30,7 @@ When running from the source distribution:
|
|||||||
$ python3 apps/usb-probe.py
|
$ python3 apps/usb-probe.py
|
||||||
```
|
```
|
||||||
|
|
||||||
or
|
or
|
||||||
|
|
||||||
```
|
```
|
||||||
$ python3 apps/usb-probe.py --verbose
|
$ python3 apps/usb-probe.py --verbose
|
||||||
@@ -38,7 +38,7 @@ $ python3 apps/usb-probe.py --verbose
|
|||||||
|
|
||||||
!!! example
|
!!! example
|
||||||
```
|
```
|
||||||
$ python3 apps/usb_probe.py
|
$ python3 apps/usb_probe.py
|
||||||
|
|
||||||
ID 0A12:0001
|
ID 0A12:0001
|
||||||
Bumble Transport Names: usb:0 or usb:0A12:0001
|
Bumble Transport Names: usb:0 or usb:0A12:0001
|
||||||
@@ -47,4 +47,4 @@ $ python3 apps/usb-probe.py --verbose
|
|||||||
Subclass/Protocol: 1/1 [Bluetooth]
|
Subclass/Protocol: 1/1 [Bluetooth]
|
||||||
Manufacturer: None
|
Manufacturer: None
|
||||||
Product: USB2.0-BT
|
Product: USB2.0-BT
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
CONTROLLER
|
CONTROLLER
|
||||||
==========
|
==========
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
GATT
|
GATT
|
||||||
====
|
====
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
HOST
|
HOST
|
||||||
====
|
====
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
SECURITY MANAGER
|
SECURITY MANAGER
|
||||||
================
|
================
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
CODE STYLE
|
||||||
|
==========
|
||||||
|
|
||||||
|
The Python code style used in this project follows the [Black code style](https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html).
|
||||||
|
|
||||||
|
# Formatting
|
||||||
|
|
||||||
|
For now, we are configuring the `black` formatter with the option to leave quotes unchanged.
|
||||||
|
The preferred quote style is single quotes, which isn't a configurable option for `Black`, so we are not enforcing it. This may change in the future.
|
||||||
|
|
||||||
|
## Ignoring Commit for Git Blame
|
||||||
|
|
||||||
|
The adoption of `Black` as a formatter came in late in the project, with already a large code base. As a result, a large number of files were changed in a single commit, which gets in the way of tracing authorship with `git blame`. The file `git-blame-ignore-revs` contains the commit hash of when that mass-formatting event occurred, which you can use to skip it in a `git blame` analysis:
|
||||||
|
|
||||||
|
!!! example "Ignoring a commit with `git blame`"
|
||||||
|
```
|
||||||
|
$ git blame --ignore-revs-file .git-blame-ignore-revs
|
||||||
|
```
|
||||||
|
|
||||||
|
# Linting
|
||||||
|
|
||||||
|
The project includes a `pylint` configuration (see the `pyproject.toml` file for details).
|
||||||
|
The `pre-commit` checks only enforce that there are no errors. But we strongly recommend that you run the linter with warnings enabled at least, and possibly the "Refactor" ('R') and "Convention" ('C') categories as well.
|
||||||
|
To run the linter, use the `project.lint` invoke command.
|
||||||
|
|
||||||
|
!!! example "Running the linter with default options"
|
||||||
|
With the default settings, Errors and Warnings are enabled, but Refactor and Convention categories are not.
|
||||||
|
```
|
||||||
|
$ invoke project.lint
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! example "Running the linter with all categories"
|
||||||
|
```
|
||||||
|
$ invoke project.lint --disable=""
|
||||||
|
```
|
||||||
|
|
||||||
|
# Editor/IDE Integration
|
||||||
|
|
||||||
|
## Visual Studio Code
|
||||||
|
|
||||||
|
The project includes a `.vscode/settings.json` file that specifies the `black` formatter and enables an editor ruler at 88 columns.
|
||||||
|
You may want to configure your own environment to "format on save" with `black` if you find that useful. We are not making that choice at the workspace level.
|
||||||
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
CONTRIBUTING TO THE PROJECT
|
||||||
|
===========================
|
||||||
|
|
||||||
|
To contribute some code to the project, you will need to submit a GitHub Pull Request (a.k.a PR). Please familiarize yourself with how that works (see [GitHub Pull Requests](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests))
|
||||||
|
|
||||||
|
You should follow the project's [code style](code_style.md), and pre-check your code before submitting a PR. The GitHub project is set up with some [Actions](https://github.com/features/actions) that will check that a PR passes at least the basic tests and complies with the coding style, but it is still recommended to check that for yourself before submitting a PR.
|
||||||
|
To run the basic checks (essentially: running the tests, the linter, and the formatter), use the `project.pre-commit` `invoke` command, and address any issues found:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ invoke project.pre-commit
|
||||||
|
```
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
PYTHON ENVIRONMENTS
|
PYTHON ENVIRONMENTS
|
||||||
===================
|
===================
|
||||||
|
|
||||||
When you don't want to install Bumble in your main/default python environment,
|
When you don't want to install Bumble in your main/default python environment,
|
||||||
using a virtual environment, where the package and its dependencies can be
|
using a virtual environment, where the package and its dependencies can be
|
||||||
installed, isolated from the rest, may be useful.
|
installed, isolated from the rest, may be useful.
|
||||||
|
|
||||||
There are many flavors of python environments and dependency managers.
|
There are many flavors of python environments and dependency managers.
|
||||||
This page describes a few of the most common ones.
|
This page describes a few of the most common ones.
|
||||||
|
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ Visit the [`venv` documentation](https://docs.python.org/3/library/venv.html) pa
|
|||||||
|
|
||||||
## Pyenv
|
## Pyenv
|
||||||
|
|
||||||
`pyenv` lets you easily switch between multiple versions of Python. It's simple, unobtrusive, and follows the UNIX tradition of single-purpose tools that do one thing well.
|
`pyenv` lets you easily switch between multiple versions of Python. It's simple, unobtrusive, and follows the UNIX tradition of single-purpose tools that do one thing well.
|
||||||
Visit the [`pyenv` site](https://github.com/pyenv/pyenv) for instructions on how to install
|
Visit the [`pyenv` site](https://github.com/pyenv/pyenv) for instructions on how to install
|
||||||
and use `pyenv`
|
and use `pyenv`
|
||||||
|
|
||||||
@@ -25,10 +25,10 @@ and use `pyenv`
|
|||||||
Conda is a convenient package manager and virtual environment.
|
Conda is a convenient package manager and virtual environment.
|
||||||
The file `environment.yml` is a Conda environment file that you can use to create
|
The file `environment.yml` is a Conda environment file that you can use to create
|
||||||
a new Conda environment. Once created, you can simply activate this environment when
|
a new Conda environment. Once created, you can simply activate this environment when
|
||||||
working with Bumble.
|
working with Bumble.
|
||||||
Visit the [Conda site](https://docs.conda.io/en/latest/) for instructions on how to install
|
Visit the [Conda site](https://docs.conda.io/en/latest/) for instructions on how to install
|
||||||
and use Conda.
|
and use Conda.
|
||||||
A few useful commands:
|
A few useful commands:
|
||||||
|
|
||||||
### Create a new `bumble` Conda environment
|
### Create a new `bumble` Conda environment
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -69,4 +69,4 @@ An app that connects to an RFComm server and bridges the RFComm channel to a loc
|
|||||||
An app that implements an RFComm server and, when a connection is received, bridges the channel to a local TCP socket
|
An app that implements an RFComm server and, when a connection is received, bridges the channel to a local TCP socket
|
||||||
|
|
||||||
## `run_scanner.py`
|
## `run_scanner.py`
|
||||||
An app that scan for BLE devices and print the advertisements received.
|
An app that scan for BLE devices and print the advertisements received.
|
||||||
|
|||||||
@@ -5,18 +5,18 @@ GETTING STARTED WITH BUMBLE
|
|||||||
|
|
||||||
You need Python 3.8 or above. Python >= 3.9 is recommended, but 3.8 should be sufficient if
|
You need Python 3.8 or above. Python >= 3.9 is recommended, but 3.8 should be sufficient if
|
||||||
necessary (there may be some optional functionality that will not work on some platforms with
|
necessary (there may be some optional functionality that will not work on some platforms with
|
||||||
python 3.8).
|
python 3.8).
|
||||||
Visit the [Python site](https://www.python.org/) for instructions on how to install Python
|
Visit the [Python site](https://www.python.org/) for instructions on how to install Python
|
||||||
for your platform.
|
for your platform.
|
||||||
Throughout the documentation, when shell commands are shown, it is assumed that you can
|
Throughout the documentation, when shell commands are shown, it is assumed that you can
|
||||||
invoke Python as
|
invoke Python as
|
||||||
```
|
```
|
||||||
$ python
|
$ python
|
||||||
```
|
```
|
||||||
If invoking python is different on your platform (it may be `python3` for example, or just `py` or `py.exe`),
|
If invoking python is different on your platform (it may be `python3` for example, or just `py` or `py.exe`),
|
||||||
adjust accordingly.
|
adjust accordingly.
|
||||||
|
|
||||||
You may be simply using Bumble as a module for your own application or as a dependency to your own
|
You may be simply using Bumble as a module for your own application or as a dependency to your own
|
||||||
module, or you may be working on modifying or contributing to the Bumble module or example code
|
module, or you may be working on modifying or contributing to the Bumble module or example code
|
||||||
itself.
|
itself.
|
||||||
|
|
||||||
@@ -65,15 +65,17 @@ $ python -m pip install git+https://github.com/google/bumble.git@27c0551
|
|||||||
|
|
||||||
# Working On The Bumble Code
|
# Working On The Bumble Code
|
||||||
When you work on the Bumble code itself, and run some of the tests or example apps, or import the
|
When you work on the Bumble code itself, and run some of the tests or example apps, or import the
|
||||||
module in your own code, you typically either install the package from source in "development mode" as described above, or you may choose to skip the install phase.
|
module in your own code, you typically either install the package from source in "development mode" as described above, or you may choose to skip the install phase.
|
||||||
|
|
||||||
|
If you plan on contributing to the project, please read the [contributing](development/contributing.md) section.
|
||||||
|
|
||||||
## Without Installing
|
## Without Installing
|
||||||
If you prefer not to install the package (even in development mode), you can load the module directly from its location in the project.
|
If you prefer not to install the package (even in development mode), you can load the module directly from its location in the project.
|
||||||
A simple way to do that is to set your `PYTHONPATH` to
|
A simple way to do that is to set your `PYTHONPATH` to
|
||||||
point to the root project directory, where the `bumble` subdirectory is located. You may set
|
point to the root project directory, where the `bumble` subdirectory is located. You may set
|
||||||
`PYTHONPATH` globally, or locally with each command line execution (on Unix-like systems).
|
`PYTHONPATH` globally, or locally with each command line execution (on Unix-like systems).
|
||||||
|
|
||||||
Example with a global `PYTHONPATH`, from a unix shell, when the working directory is the root
|
Example with a global `PYTHONPATH`, from a unix shell, when the working directory is the root
|
||||||
directory of the project.
|
directory of the project.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -96,11 +98,11 @@ $ PYTHONPATH=. python examples/run_advertiser.py examples/device1.json serial:/d
|
|||||||
```
|
```
|
||||||
|
|
||||||
# Where To Go Next
|
# Where To Go Next
|
||||||
Once you've installed or downloaded Bumble, you can either start using some of the
|
Once you've installed or downloaded Bumble, you can either start using some of the
|
||||||
[Bundled apps and tools](apps_and_tools/index.md), or look at the [examples](examples/index.md)
|
[Bundled apps and tools](apps_and_tools/index.md), or look at the [examples](examples/index.md)
|
||||||
to get a feel for how to use the APIs, and start writing your own applications.
|
to get a feel for how to use the APIs, and start writing your own applications.
|
||||||
|
|
||||||
Depending on the use case you're interested in exploring, you may need to use a physical Bluetooth
|
Depending on the use case you're interested in exploring, you may need to use a physical Bluetooth
|
||||||
controller, like a USB dongle or a board with a Bluetooth radio. Visit the [Hardware page](hardware/index.md)
|
controller, like a USB dongle or a board with a Bluetooth radio. Visit the [Hardware page](hardware/index.md)
|
||||||
for more information on using a physical radio, and/or the [Transports page](transports/index.md) for more
|
for more information on using a physical radio, and/or the [Transports page](transports/index.md) for more
|
||||||
details on interfacing with either hardware modules or virtual controllers over various transports.
|
details on interfacing with either hardware modules or virtual controllers over various transports.
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
HARDWARE
|
HARDWARE
|
||||||
========
|
========
|
||||||
|
|
||||||
The Bumble Host connects to a controller over an [HCI Transport](../transports/index.md).
|
The Bumble Host connects to a controller over an [HCI Transport](../transports/index.md).
|
||||||
To use a hardware controller attached to the host on which the host application is running, the transport is typically either [HCI over UART](../transports/serial.md) or [HCI over USB](../transports/usb.md).
|
To use a hardware controller attached to the host on which the host application is running, the transport is typically either [HCI over UART](../transports/serial.md) or [HCI over USB](../transports/usb.md).
|
||||||
On Linux, the [VHCI Transport](../transports/vhci.md) can be used to communicate with any controller hardware managed by the operating system. Alternatively, a remote controller (a phyiscal controller attached to a remote host) can be used by connecting one of the networked transports (such as the [TCP Client transport](../transports/tcp_client.md), the [TCP Server transport](../transports/tcp_server.md) or the [UDP Transport](../transports/udp.md)) to an [HCI Bridge](../apps_and_tools/hci_bridge) bridging the network transport to a physical controller on a remote host.
|
On Linux, the [VHCI Transport](../transports/vhci.md) can be used to communicate with any controller hardware managed by the operating system. Alternatively, a remote controller (a phyiscal controller attached to a remote host) can be used by connecting one of the networked transports (such as the [TCP Client transport](../transports/tcp_client.md), the [TCP Server transport](../transports/tcp_server.md) or the [UDP Transport](../transports/udp.md)) to an [HCI Bridge](../apps_and_tools/hci_bridge) bridging the network transport to a physical controller on a remote host.
|
||||||
|
|
||||||
In theory, any controller that is compliant with the HCI over UART or HCI over USB protocols can be used.
|
In theory, any controller that is compliant with the HCI over UART or HCI over USB protocols can be used.
|
||||||
|
|
||||||
HCI over USB is very common, implemented by a number of commercial Bluetooth dongles.
|
HCI over USB is very common, implemented by a number of commercial Bluetooth dongles.
|
||||||
|
|
||||||
It is also possible to use an embedded development board, running a specialized application, such as the [`HCI UART`](https://docs.zephyrproject.org/latest/samples/bluetooth/hci_uart/README.html) and [`HCI USB`](https://docs.zephyrproject.org/latest/samples/bluetooth/hci_usb/README.html) demo applications from the [Zephyr project](https://www.zephyrproject.org/), or the [`blehci`](https://mynewt.apache.org/latest/tutorials/ble/blehci_project.html) application from [mynewt/nimble](https://mynewt.apache.org/)
|
It is also possible to use an embedded development board, running a specialized application, such as the [`HCI UART`](https://docs.zephyrproject.org/latest/samples/bluetooth/hci_uart/README.html) and [`HCI USB`](https://docs.zephyrproject.org/latest/samples/bluetooth/hci_usb/README.html) demo applications from the [Zephyr project](https://www.zephyrproject.org/), or the [`blehci`](https://mynewt.apache.org/latest/tutorials/ble/blehci_project.html) application from [mynewt/nimble](https://mynewt.apache.org/)
|
||||||
|
|
||||||
Some specific USB dongles and embedded boards that are known to work include:
|
Some specific USB dongles and embedded boards that are known to work include:
|
||||||
|
|
||||||
* [Nordic nRF52840 DK board](https://www.nordicsemi.com/Products/Development-hardware/nrf52840-dk) with the Zephyr `HCI UART` application
|
* [Nordic nRF52840 DK board](https://www.nordicsemi.com/Products/Development-hardware/nrf52840-dk) with the Zephyr `HCI UART` application
|
||||||
* [Nordic nRF52840 DK board](https://www.nordicsemi.com/Products/Development-hardware/nrf52840-dk) with the mynewt `blehci` application
|
* [Nordic nRF52840 DK board](https://www.nordicsemi.com/Products/Development-hardware/nrf52840-dk) with the mynewt `blehci` application
|
||||||
* [Nordic nrf52840 dongle](https://www.nordicsemi.com/Products/Development-hardware/nRF52840-Dongle) with the Zephyr `HCI USB` application
|
* [Nordic nrf52840 dongle](https://www.nordicsemi.com/Products/Development-hardware/nRF52840-Dongle) with the Zephyr `HCI USB` application
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 113 KiB |
@@ -163,4 +163,3 @@ Future features to be considered include:
|
|||||||
* Bindings for languages other than Python
|
* Bindings for languages other than Python
|
||||||
* RPC interface to expose most of the API for remote use
|
* RPC interface to expose most of the API for remote use
|
||||||
* (...suggest anything you want...)
|
* (...suggest anything you want...)
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
===================================
|
===================================
|
||||||
|
|
||||||
Using Bumble with Android is not about running the Bumble stack on the Android
|
Using Bumble with Android is not about running the Bumble stack on the Android
|
||||||
OS itself, but rather using Bumble with the Bluetooth support of the Android
|
OS itself, but rather using Bumble with the Bluetooth support of the Android
|
||||||
emulator.
|
emulator.
|
||||||
|
|
||||||
The two main use cases are:
|
The two main use cases are:
|
||||||
|
|
||||||
* Connecting the Bumble host stack to the Android emulator's virtual controller.
|
* Connecting the Bumble host stack to the Android emulator's virtual controller.
|
||||||
* Using Bumble as an HCI bridge to connect the Android emulator to a physical
|
* Using Bumble as an HCI bridge to connect the Android emulator to a physical
|
||||||
Bluetooth controller, such as a USB dongle
|
Bluetooth controller, such as a USB dongle
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
@@ -17,7 +17,7 @@ The two main use cases are:
|
|||||||
version of the emulator you are using.
|
version of the emulator you are using.
|
||||||
You will need version 31.3.8.0 or later.
|
You will need version 31.3.8.0 or later.
|
||||||
|
|
||||||
The Android emulator supports Bluetooth in two ways: either by exposing virtual
|
The Android emulator supports Bluetooth in two ways: either by exposing virtual
|
||||||
Bluetooth controllers to which you can connect a virtual Bluetooth host stack, or
|
Bluetooth controllers to which you can connect a virtual Bluetooth host stack, or
|
||||||
by exposing an way to connect your own virtual controller to the Android Bluetooth
|
by exposing an way to connect your own virtual controller to the Android Bluetooth
|
||||||
stack via a virtual HCI interface.
|
stack via a virtual HCI interface.
|
||||||
@@ -25,7 +25,7 @@ Both ways are controlled via gRPC requests to the Android emulator.
|
|||||||
|
|
||||||
## Launching the Emulator
|
## Launching the Emulator
|
||||||
|
|
||||||
If the version of the emulator you are running does not yet support enabling
|
If the version of the emulator you are running does not yet support enabling
|
||||||
Bluetooth support by default or automatically, you must launch the emulator from
|
Bluetooth support by default or automatically, you must launch the emulator from
|
||||||
the command line.
|
the command line.
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ communicate link layer packets between them, thus creating a virtual radio netwo
|
|||||||
Configuring a Bumble Device instance to use Root Canal as a virtual controller
|
Configuring a Bumble Device instance to use Root Canal as a virtual controller
|
||||||
allows that virtual device to communicate with the Android Bluetooth stack, and
|
allows that virtual device to communicate with the Android Bluetooth stack, and
|
||||||
through it with Android applications as well as system-managed profiles.
|
through it with Android applications as well as system-managed profiles.
|
||||||
To connect a Bumble host stack to a Root Canal virtual controller instance, use
|
To connect a Bumble host stack to a Root Canal virtual controller instance, use
|
||||||
the bumble `android-emulator` transport in `host` mode (the default).
|
the bumble `android-emulator` transport in `host` mode (the default).
|
||||||
|
|
||||||
!!! example "Run the example GATT server connected to the emulator"
|
!!! example "Run the example GATT server connected to the emulator"
|
||||||
@@ -78,7 +78,7 @@ To connect a virtual controller to the Android Bluetooth stack, use the bumble `
|
|||||||
## Other Tools
|
## Other Tools
|
||||||
|
|
||||||
The `show` application that's included with Bumble can be used to parse and pretty-print the HCI packets
|
The `show` application that's included with Bumble can be used to parse and pretty-print the HCI packets
|
||||||
from an Android HCI "snoop log" (see [this page](https://source.android.com/devices/bluetooth/verifying_debugging)
|
from an Android HCI "snoop log" (see [this page](https://source.android.com/devices/bluetooth/verifying_debugging)
|
||||||
for details on how to obtain HCI snoop logs from an Android device).
|
for details on how to obtain HCI snoop logs from an Android device).
|
||||||
Use the `--format snoop` option to specify that the file is in that specific format.
|
Use the `--format snoop` option to specify that the file is in that specific format.
|
||||||
|
|
||||||
@@ -86,4 +86,3 @@ Use the `--format snoop` option to specify that the file is in that specific for
|
|||||||
```shell
|
```shell
|
||||||
$ bumble-show --format snoop btsnoop_hci.log
|
$ bumble-show --format snoop btsnoop_hci.log
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ The 3 main types of physical Bluetooth controllers are:
|
|||||||
|
|
||||||
!!! tip "Conflicts with the kernel and BlueZ"
|
!!! tip "Conflicts with the kernel and BlueZ"
|
||||||
If your use a USB dongle that is recognized by your kernel as a supported Bluetooth device, it is
|
If your use a USB dongle that is recognized by your kernel as a supported Bluetooth device, it is
|
||||||
likely that the kernel driver will claim that USB device and attach it to the BlueZ stack.
|
likely that the kernel driver will claim that USB device and attach it to the BlueZ stack.
|
||||||
If you want to claim ownership of it to use with Bumble, you will need to set the state of the corresponding HCI interface as `DOWN`.
|
If you want to claim ownership of it to use with Bumble, you will need to set the state of the corresponding HCI interface as `DOWN`.
|
||||||
HCI interfaces are numbered, starting from 0 (i.e `hci0`, `hci1`, ...).
|
HCI interfaces are numbered, starting from 0 (i.e `hci0`, `hci1`, ...).
|
||||||
|
|
||||||
For example, to bring `hci0` down:
|
For example, to bring `hci0` down:
|
||||||
@@ -36,7 +36,7 @@ See the [USB Transport page](../transports/usb.md) for general information on ho
|
|||||||
!!! tip "USB Permissions"
|
!!! tip "USB Permissions"
|
||||||
By default, when running as a regular user, you won't have the permission to use
|
By default, when running as a regular user, you won't have the permission to use
|
||||||
arbitrary USB devices.
|
arbitrary USB devices.
|
||||||
You can change the permissions for a specific USB device based on its bus number and
|
You can change the permissions for a specific USB device based on its bus number and
|
||||||
device number (you can use `lsusb` to find the Bus and Device numbers for your Bluetooth
|
device number (you can use `lsusb` to find the Bus and Device numbers for your Bluetooth
|
||||||
dongle).
|
dongle).
|
||||||
|
|
||||||
@@ -69,9 +69,9 @@ You can bring a HCI controller `UP` or `DOWN` with `hciconfig hci<X> up` and `hc
|
|||||||
By default, when running as a regular user, you won't have the permission to use
|
By default, when running as a regular user, you won't have the permission to use
|
||||||
an HCI socket to a Bluetooth controller (you may see an exception like `PermissionError: [Errno 1] Operation not permitted`).
|
an HCI socket to a Bluetooth controller (you may see an exception like `PermissionError: [Errno 1] Operation not permitted`).
|
||||||
|
|
||||||
If you want to run without using `sudo`, you need to manage the capabilities by adding the appropriate entries in `/etc/security/capability.conf` to grant a user or group the `cap_net_admin` capability.
|
If you want to run without using `sudo`, you need to manage the capabilities by adding the appropriate entries in `/etc/security/capability.conf` to grant a user or group the `cap_net_admin` capability.
|
||||||
See [this manpage](https://manpages.ubuntu.com/manpages/bionic/man5/capability.conf.5.html) for details.
|
See [this manpage](https://manpages.ubuntu.com/manpages/bionic/man5/capability.conf.5.html) for details.
|
||||||
|
|
||||||
Alternatively, if you are just experimenting temporarily, the `capsh` command may be useful in order
|
Alternatively, if you are just experimenting temporarily, the `capsh` command may be useful in order
|
||||||
to execute a single command with enhanced permissions, as in this example:
|
to execute a single command with enhanced permissions, as in this example:
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ You can bring a HCI controller `UP` or `DOWN` with `hciconfig hci<X> up` and `hc
|
|||||||
$ sudo capsh --caps="cap_net_admin+eip cap_setpcap,cap_setuid,cap_setgid+ep" --keep=1 --user=$USER --addamb=cap_net_admin -- -c "<path/to/executable> <executable-args>"
|
$ sudo capsh --caps="cap_net_admin+eip cap_setpcap,cap_setuid,cap_setgid+ep" --keep=1 --user=$USER --addamb=cap_net_admin -- -c "<path/to/executable> <executable-args>"
|
||||||
```
|
```
|
||||||
Where `<path/to/executable>` is the path to your `python3` executable or to one of the Bumble bundled command-line applications.
|
Where `<path/to/executable>` is the path to your `python3` executable or to one of the Bumble bundled command-line applications.
|
||||||
|
|
||||||
!!! tip "List all available controllers"
|
!!! tip "List all available controllers"
|
||||||
The command
|
The command
|
||||||
```
|
```
|
||||||
@@ -94,22 +94,22 @@ You can bring a HCI controller `UP` or `DOWN` with `hciconfig hci<X> up` and `hc
|
|||||||
pi@raspberrypi:~ $ hciconfig
|
pi@raspberrypi:~ $ hciconfig
|
||||||
hci1: Type: Primary Bus: USB
|
hci1: Type: Primary Bus: USB
|
||||||
BD Address: 00:16:A4:5A:40:F2 ACL MTU: 1021:8 SCO MTU: 64:1
|
BD Address: 00:16:A4:5A:40:F2 ACL MTU: 1021:8 SCO MTU: 64:1
|
||||||
DOWN
|
DOWN
|
||||||
RX bytes:84056 acl:0 sco:0 events:51 errors:0
|
RX bytes:84056 acl:0 sco:0 events:51 errors:0
|
||||||
TX bytes:1980 acl:0 sco:0 commands:90 errors:0
|
TX bytes:1980 acl:0 sco:0 commands:90 errors:0
|
||||||
|
|
||||||
hci0: Type: Primary Bus: UART
|
hci0: Type: Primary Bus: UART
|
||||||
BD Address: DC:A6:32:75:2C:97 ACL MTU: 1021:8 SCO MTU: 64:1
|
BD Address: DC:A6:32:75:2C:97 ACL MTU: 1021:8 SCO MTU: 64:1
|
||||||
DOWN
|
DOWN
|
||||||
RX bytes:68038 acl:0 sco:0 events:692 errors:0
|
RX bytes:68038 acl:0 sco:0 events:692 errors:0
|
||||||
TX bytes:20105 acl:0 sco:0 commands:843 errors:0
|
TX bytes:20105 acl:0 sco:0 commands:843 errors:0
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! tip "Disabling `bluetoothd`"
|
!!! tip "Disabling `bluetoothd`"
|
||||||
When the Bluetooth daemon, `bluetoothd`, is running, it will try to use any HCI controller attached to the BlueZ stack, automatically. This means that whenever an HCI socket transport is released, it is likely that `bluetoothd` will take it over, so you will get a "device busy" condition (ex: `OSError: [Errno 16] Device or resource busy`). If that happens, you can always use
|
When the Bluetooth daemon, `bluetoothd`, is running, it will try to use any HCI controller attached to the BlueZ stack, automatically. This means that whenever an HCI socket transport is released, it is likely that `bluetoothd` will take it over, so you will get a "device busy" condition (ex: `OSError: [Errno 16] Device or resource busy`). If that happens, you can always use
|
||||||
```
|
```
|
||||||
$ hciconfig hci0 down
|
$ hciconfig hci0 down
|
||||||
```
|
```
|
||||||
(or `hci<X>` with `<X>` being the index of the controller device you want to use), but a simpler solution is to just stop the `bluetoothd` daemon, with a command like:
|
(or `hci<X>` with `<X>` being the index of the controller device you want to use), but a simpler solution is to just stop the `bluetoothd` daemon, with a command like:
|
||||||
```
|
```
|
||||||
$ sudo systemctl stop bluetooth.service
|
$ sudo systemctl stop bluetooth.service
|
||||||
@@ -181,13 +181,13 @@ With the [VHCI transport](../transports/vhci.md) you can attach a Bumble virtual
|
|||||||
```
|
```
|
||||||
python3 examples/run_controller.py F6:F7:F8:F9:FA:FB examples/device1.json vhci
|
python3 examples/run_controller.py F6:F7:F8:F9:FA:FB examples/device1.json vhci
|
||||||
```
|
```
|
||||||
|
|
||||||
You should see a 'Virtual Bus' controller. For example:
|
You should see a 'Virtual Bus' controller. For example:
|
||||||
```
|
```
|
||||||
$ hciconfig
|
$ hciconfig
|
||||||
hci0: Type: Primary Bus: Virtual
|
hci0: Type: Primary Bus: Virtual
|
||||||
BD Address: F6:F7:F8:F9:FA:FB ACL MTU: 27:64 SCO MTU: 0:0
|
BD Address: F6:F7:F8:F9:FA:FB ACL MTU: 27:64 SCO MTU: 0:0
|
||||||
UP RUNNING
|
UP RUNNING
|
||||||
RX bytes:0 acl:0 sco:0 events:43 errors:0
|
RX bytes:0 acl:0 sco:0 events:43 errors:0
|
||||||
TX bytes:274 acl:0 sco:0 commands:43 errors:0
|
TX bytes:274 acl:0 sco:0 commands:43 errors:0
|
||||||
```
|
```
|
||||||
@@ -204,5 +204,3 @@ With the [VHCI transport](../transports/vhci.md) you can attach a Bumble virtual
|
|||||||
### Using a Simulated UART HCI
|
### Using a Simulated UART HCI
|
||||||
|
|
||||||
### Bridge to a Remote Controller
|
### Bridge to a Remote Controller
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,4 +11,3 @@ To do that, use the following command:
|
|||||||
sudo nvram bluetoothHostControllerSwitchBehavior="never"
|
sudo nvram bluetoothHostControllerSwitchBehavior="never"
|
||||||
```
|
```
|
||||||
A reboot shouldn't be necessary after that. See [Tech Note 2295](https://developer.apple.com/library/archive/technotes/tn2295/_index.html)
|
A reboot shouldn't be necessary after that. See [Tech Note 2295](https://developer.apple.com/library/archive/technotes/tn2295/_index.html)
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ USB HCI
|
|||||||
To use a Bluetooth USB dongle on Windows, you need a USB dongle that does not require a vendor Windows driver (the dongle will be used directly through the [`WinUSB`](https://docs.microsoft.com/en-us/windows-hardware/drivers/usbcon/winusb) driver rather than through a vendor-supplied Windows driver).
|
To use a Bluetooth USB dongle on Windows, you need a USB dongle that does not require a vendor Windows driver (the dongle will be used directly through the [`WinUSB`](https://docs.microsoft.com/en-us/windows-hardware/drivers/usbcon/winusb) driver rather than through a vendor-supplied Windows driver).
|
||||||
|
|
||||||
In order to use the dongle, the `WinUSB` driver must be assigned to the USB device. It is likely that, by default, when you first plug in the dongle, it will be recognized by Windows as a Bluetooth USB device, and Windows will try to use it with its native Bluetooth stack. You will need to switch the driver, which can be done easily with the [Zadig tool](https://zadig.akeo.ie/).
|
In order to use the dongle, the `WinUSB` driver must be assigned to the USB device. It is likely that, by default, when you first plug in the dongle, it will be recognized by Windows as a Bluetooth USB device, and Windows will try to use it with its native Bluetooth stack. You will need to switch the driver, which can be done easily with the [Zadig tool](https://zadig.akeo.ie/).
|
||||||
In the Zadig tool, select your USB dongle device, and associate it with WinUSB.
|
In the Zadig tool, select your USB dongle device, and associate it with WinUSB.
|
||||||
Once the WinUSB driver is correctly assigned to your device, you can confirm that by checking the settings with the Windows Device Manager control panel. Your device should appear under "Universal Serial Bus Device" (not under "Bluetooth"), and inspecting the driver details, you should see `winusb.sys` in the list of driver files.
|
Once the WinUSB driver is correctly assigned to your device, you can confirm that by checking the settings with the Windows Device Manager control panel. Your device should appear under "Universal Serial Bus Device" (not under "Bluetooth"), and inspecting the driver details, you should see `winusb.sys` in the list of driver files.
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
@@ -6,17 +6,17 @@ The Android emulator transport either connects, as a host, to a "Root Canal" vir
|
|||||||
|
|
||||||
## Moniker
|
## Moniker
|
||||||
The moniker syntax for an Android Emulator transport is: `android-emulator:[mode=<host|controller>][<hostname>:<port>]`, where
|
The moniker syntax for an Android Emulator transport is: `android-emulator:[mode=<host|controller>][<hostname>:<port>]`, where
|
||||||
the `mode` parameter can specify running as a host or a controller, and `<hostname>:<port>` can specify a host name (or IP address) and TCP port number on which to reach the gRPC server for the emulator.
|
the `mode` parameter can specify running as a host or a controller, and `<hostname>:<port>` can specify a host name (or IP address) and TCP port number on which to reach the gRPC server for the emulator.
|
||||||
Both the `mode=<host|controller>` and `<hostname>:<port>` parameters are optional (so the moniker `android-emulator` by itself is a valid moniker, which will create a transport in `host` mode, connected to `localhost` on the default gRPC port for the emulator).
|
Both the `mode=<host|controller>` and `<hostname>:<port>` parameters are optional (so the moniker `android-emulator` by itself is a valid moniker, which will create a transport in `host` mode, connected to `localhost` on the default gRPC port for the emulator).
|
||||||
|
|
||||||
!!! example Example
|
!!! example Example
|
||||||
`android-emulator`
|
`android-emulator`
|
||||||
connect as a host to the emulator on localhost:8554
|
connect as a host to the emulator on localhost:8554
|
||||||
|
|
||||||
!!! example Example
|
!!! example Example
|
||||||
`android-emulator:mode=controller`
|
`android-emulator:mode=controller`
|
||||||
connect as a controller to the emulator on localhost:8554
|
connect as a controller to the emulator on localhost:8554
|
||||||
|
|
||||||
!!! example Example
|
!!! example Example
|
||||||
`android-emulator:localhost:8555`
|
`android-emulator:localhost:8555`
|
||||||
connect as a host to the emulator on localhost:8555
|
connect as a host to the emulator on localhost:8555
|
||||||
|
|||||||
@@ -8,5 +8,5 @@ This is typically used to open a PTY, or unix driver, not for real files.
|
|||||||
The moniker for a File transport is `file:<path>`
|
The moniker for a File transport is `file:<path>`
|
||||||
|
|
||||||
!!! example
|
!!! example
|
||||||
`file:/dev/ttys001`
|
`file:/dev/ttys001`
|
||||||
Opens the pseudo terminal `/dev/ttys001` as a transport
|
Opens the pseudo terminal `/dev/ttys001` as a transport
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ An HCI Socket can send/receive HCI packets to/from a Bluetooth HCI controller ma
|
|||||||
The moniker for an HCI Socket transport is either just `hci-socket` (to use the default/first Bluetooth controller), or `hci-socket:<index>` where `<index>` is the 0-based index of a Bluetooth controller device.
|
The moniker for an HCI Socket transport is either just `hci-socket` (to use the default/first Bluetooth controller), or `hci-socket:<index>` where `<index>` is the 0-based index of a Bluetooth controller device.
|
||||||
|
|
||||||
!!! example
|
!!! example
|
||||||
`hci-socket`
|
`hci-socket`
|
||||||
Use an HCI socket to the first Bluetooth controller (`hci0 on Linux`)
|
Use an HCI socket to the first Bluetooth controller (`hci0 on Linux`)
|
||||||
|
|
||||||
!!! tip "On Linux"
|
!!! tip "On Linux"
|
||||||
See the [Linux Platform](../platforms/linux.md) page for details on how to use HCI sockets on Linux
|
See the [Linux Platform](../platforms/linux.md) page for details on how to use HCI sockets on Linux
|
||||||
|
|||||||
@@ -8,5 +8,5 @@ The moniker syntax for a PTY transport is: `pty[:path]`.
|
|||||||
Where `path`, is used, is the path name where a symbolic link to the PTY will be created for convenience (the link will be removed when the transport is closed or when the process exits).
|
Where `path`, is used, is the path name where a symbolic link to the PTY will be created for convenience (the link will be removed when the transport is closed or when the process exits).
|
||||||
|
|
||||||
!!! example
|
!!! example
|
||||||
`pty:virtual_hci`
|
`pty:virtual_hci`
|
||||||
Creates a PTY entry and a symbolic link, named `virtual_hci`, linking to the PTY
|
Creates a PTY entry and a symbolic link, named `virtual_hci`, linking to the PTY
|
||||||
|
|||||||
@@ -8,5 +8,5 @@ The moniker syntax for a serial transport is: `serial:<device-path>[,<speed>]`
|
|||||||
When `<speed>` is omitted, the default value of 1000000 is used
|
When `<speed>` is omitted, the default value of 1000000 is used
|
||||||
|
|
||||||
!!! example
|
!!! example
|
||||||
`serial:/dev/tty.usbmodem0006839912172,1000000`
|
`serial:/dev/tty.usbmodem0006839912172,1000000`
|
||||||
Opens the serial port `/dev/tty.usbmodem0006839912172` at `1000000`bps
|
Opens the serial port `/dev/tty.usbmodem0006839912172` at `1000000`bps
|
||||||
|
|||||||
@@ -7,5 +7,5 @@ The TCP Client transport uses an outgoing TCP connection to a host:port address.
|
|||||||
The moniker syntax for a TCP client transport is: `tcp-client:<remote-host>:<remote-port>`
|
The moniker syntax for a TCP client transport is: `tcp-client:<remote-host>:<remote-port>`
|
||||||
|
|
||||||
!!! example
|
!!! example
|
||||||
`tcp-client:127.0.0.1:9001`
|
`tcp-client:127.0.0.1:9001`
|
||||||
Connects to port 9001 on the local host
|
Connects to port 9001 on the local host
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ The TCP Client transport uses an incoming TCP connection to a host:port address.
|
|||||||
|
|
||||||
## Moniker
|
## Moniker
|
||||||
The moniker syntax for a TCP server transport is: `tcp-server:<local-host>:<local-port>`
|
The moniker syntax for a TCP server transport is: `tcp-server:<local-host>:<local-port>`
|
||||||
where `<local-host>` may be the address of a local network interface, or `_` to accept
|
where `<local-host>` may be the address of a local network interface, or `_` to accept
|
||||||
connections on all local network interfaces.
|
connections on all local network interfaces.
|
||||||
|
|
||||||
!!! example
|
!!! example
|
||||||
`tcp-server:_:9001`
|
`tcp-server:_:9001`
|
||||||
Waits for and accepts connections on port `9001`
|
Waits for and accepts connections on port `9001`
|
||||||
|
|||||||
@@ -7,5 +7,5 @@ The UDP transport is a UDP socket, receiving packets on a specified port number,
|
|||||||
The moniker syntax for a UDP transport is: `udp:<local-host>:<local-port>,<remote-host>:<remote-port>`.
|
The moniker syntax for a UDP transport is: `udp:<local-host>:<local-port>,<remote-host>:<remote-port>`.
|
||||||
|
|
||||||
!!! example
|
!!! example
|
||||||
`udp:0.0.0.0:9000,127.0.0.1:9001`
|
`udp:0.0.0.0:9000,127.0.0.1:9001`
|
||||||
UDP transport where packets are received on port `9000` and sent to `127.0.0.1` on port `9001`
|
UDP transport where packets are received on port `9000` and sent to `127.0.0.1` on port `9001`
|
||||||
|
|||||||
@@ -10,5 +10,5 @@ The VHCI transport allows attaching a virtual controller to the Bluetooth stack
|
|||||||
The moniker for a VHCI transport is either just `vhci` (to use the default VHCI device path at `/dev/vhci`), or `vhci:<path>` where `<path>` is the path of a VHCI device.
|
The moniker for a VHCI transport is either just `vhci` (to use the default VHCI device path at `/dev/vhci`), or `vhci:<path>` where `<path>` is the path of a VHCI device.
|
||||||
|
|
||||||
!!! example
|
!!! example
|
||||||
`vhci`
|
`vhci`
|
||||||
Attaches a virtual controller transport to `/dev/vhci`
|
Attaches a virtual controller transport to `/dev/vhci`
|
||||||
|
|||||||
@@ -7,5 +7,5 @@ The UDP transport is a UDP socket, receiving packets on a specified port number,
|
|||||||
The moniker syntax for a UDP transport is: `udp:<local-host>:<local-port>,<remote-host>:<remote-port>`.
|
The moniker syntax for a UDP transport is: `udp:<local-host>:<local-port>,<remote-host>:<remote-port>`.
|
||||||
|
|
||||||
!!! example
|
!!! example
|
||||||
`udp:0.0.0.0:9000,127.0.0.1:9001`
|
`udp:0.0.0.0:9000,127.0.0.1:9001`
|
||||||
UDP transport where packets are received on port `9000` and sent to `127.0.0.1` on port `9001`
|
UDP transport where packets are received on port `9000` and sent to `127.0.0.1` on port `9001`
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user