mirror of
https://github.com/google/bumble.git
synced 2026-04-16 00:25:31 +00:00
Compare commits
1 Commits
v0.0.221
...
pimp_a2dp_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7237619d3b |
10
.github/workflows/code-check.yml
vendored
10
.github/workflows/code-check.yml
vendored
@@ -6,8 +6,6 @@ on:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
workflow_dispatch:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -18,24 +16,24 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10", "3.11", "3.12", "3.13.0", "3.14"]
|
||||
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13.0"]
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- name: Check out from Git
|
||||
uses: actions/checkout@v6
|
||||
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@v6
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install ".[build,test,development]"
|
||||
python -m pip install ".[build,examples,test,development]"
|
||||
- name: Check
|
||||
run: |
|
||||
invoke project.pre-commit
|
||||
|
||||
4
.github/workflows/codeql-analysis.yml
vendored
4
.github/workflows/codeql-analysis.yml
vendored
@@ -17,8 +17,6 @@ on:
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ main ]
|
||||
workflow_dispatch:
|
||||
branches: [main]
|
||||
schedule:
|
||||
- cron: '39 21 * * 4'
|
||||
|
||||
@@ -40,7 +38,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
|
||||
8
.github/workflows/gradle-btbench.yml
vendored
8
.github/workflows/gradle-btbench.yml
vendored
@@ -7,10 +7,6 @@ on:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- 'extras/android/BtBench/**'
|
||||
workflow_dispatch:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'extras/android/BtBench/**'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -22,10 +18,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out from Git
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v5
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: 17
|
||||
|
||||
8
.github/workflows/python-avatar.yml
vendored
8
.github/workflows/python-avatar.yml
vendored
@@ -5,8 +5,6 @@ on:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
workflow_dispatch:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -26,9 +24,9 @@ jobs:
|
||||
21/24, 22/24, 23/24, 24/24,
|
||||
]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set Up Python 3.11
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.11
|
||||
- name: Install
|
||||
@@ -46,7 +44,7 @@ jobs:
|
||||
run: cat rootcanal.log
|
||||
- name: Upload Mobly logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: mobly-logs-${{ strategy.job-index }}
|
||||
path: /tmp/logs/mobly/bumble.bumbles/
|
||||
|
||||
21
.github/workflows/python-build-test.yml
vendored
21
.github/workflows/python-build-test.yml
vendored
@@ -6,8 +6,6 @@ on:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
workflow_dispatch:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -18,18 +16,18 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: ['ubuntu-latest', 'macos-latest', 'windows-latest']
|
||||
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
||||
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- name: Check out from Git
|
||||
uses: actions/checkout@v6
|
||||
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 ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
@@ -48,15 +46,14 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
# Rust runtime doesn't support 3.14 yet.
|
||||
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
||||
rust-version: [ "1.80.0", "1.91.0" ]
|
||||
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
||||
rust-version: [ "1.76.0", "stable" ]
|
||||
fail-fast: false
|
||||
steps:
|
||||
- name: Check out from Git
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install Python dependencies
|
||||
@@ -69,11 +66,11 @@ jobs:
|
||||
components: clippy,rustfmt
|
||||
toolchain: ${{ matrix.rust-version }}
|
||||
- name: Install Rust dependencies
|
||||
run: cargo install cargo-all-features --version 1.11.0 # allows building/testing combinations of features
|
||||
run: cargo install cargo-all-features # allows building/testing combinations of features
|
||||
- name: Check License Headers
|
||||
run: cd rust && cargo run --features dev-tools --bin file-header check-all
|
||||
- name: Rust Build
|
||||
run: cd rust && cargo build --all-targets && cargo build-all-features
|
||||
run: cd rust && cargo build --all-targets && cargo build-all-features --all-targets
|
||||
# Lints after build so what clippy needs is already built
|
||||
- name: Rust Lints
|
||||
run: cd rust && cargo fmt --check && cargo clippy --all-targets -- --deny warnings && cargo clippy --all-features --all-targets -- --deny warnings
|
||||
|
||||
6
.github/workflows/python-publish.yml
vendored
6
.github/workflows/python-publish.yml
vendored
@@ -14,13 +14,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out from Git
|
||||
uses: actions/checkout@v6
|
||||
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@v6
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- name: Install dependencies
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
run: python -m build
|
||||
- name: Publish package to PyPI
|
||||
if: github.event_name == 'release' && startsWith(github.ref, 'refs/tags')
|
||||
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # release/v1.13
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
|
||||
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
@@ -102,10 +102,5 @@
|
||||
"."
|
||||
],
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.pytestEnabled": true,
|
||||
"python-envs.defaultEnvManager": "ms-python.python:system",
|
||||
"python-envs.pythonProjects": [],
|
||||
"nrf-connect.applications": [
|
||||
"${workspaceFolder}/extras/zephyr/hci_usb"
|
||||
]
|
||||
"python.testing.pytestEnabled": true
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ Bumble is easiest to use with a dedicated USB dongle.
|
||||
This is because internal Bluetooth interfaces tend to be locked down by the operating system.
|
||||
You can use the [usb_probe](/docs/mkdocs/src/apps_and_tools/usb_probe.md) tool (all platforms) or `lsusb` (Linux or macOS) to list the available USB devices on your system.
|
||||
|
||||
See the [USB Transport](/docs/mkdocs/src/transports/usb.md) page for details on how to refer to USB devices. Also, if you are on a mac, see [these instructions](docs/mkdocs/src/platforms/macos.md).
|
||||
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
|
||||
|
||||
|
||||
@@ -12,6 +12,9 @@ Apps
|
||||
## `show.py`
|
||||
Parse a file with HCI packets and print the details of each packet in a human readable form
|
||||
|
||||
## `link_relay.py`
|
||||
Simple WebSocket relay for virtual RemoteLink instances to communicate with each other through.
|
||||
|
||||
## `hci_bridge.py`
|
||||
This app acts as a simple bridge between two HCI transports, with a host on one side and
|
||||
a controller on the other. All the HCI packets bridged between the two are printed on the console
|
||||
|
||||
1275
apps/auracast.py
1275
apps/auracast.py
File diff suppressed because it is too large
Load Diff
531
apps/bench.py
531
apps/bench.py
@@ -19,45 +19,33 @@ import asyncio
|
||||
import dataclasses
|
||||
import enum
|
||||
import logging
|
||||
import os
|
||||
import statistics
|
||||
import struct
|
||||
import time
|
||||
|
||||
import click
|
||||
|
||||
import bumble.core
|
||||
import bumble.logging
|
||||
import bumble.rfcomm
|
||||
from bumble import l2cap
|
||||
from bumble.colors import color
|
||||
from bumble.core import (
|
||||
PhysicalTransport,
|
||||
BT_L2CAP_PROTOCOL_ID,
|
||||
BT_RFCOMM_PROTOCOL_ID,
|
||||
UUID,
|
||||
CommandTimeoutError,
|
||||
ConnectionPHY,
|
||||
PhysicalTransport,
|
||||
)
|
||||
from bumble.device import (
|
||||
CigParameters,
|
||||
CisLink,
|
||||
Connection,
|
||||
ConnectionParametersPreferences,
|
||||
Device,
|
||||
Peer,
|
||||
)
|
||||
from bumble.colors import color
|
||||
from bumble.device import Connection, ConnectionParametersPreferences, Device, Peer
|
||||
from bumble.gatt import Characteristic, CharacteristicValue, Service
|
||||
from bumble.hci import (
|
||||
HCI_LE_1M_PHY,
|
||||
HCI_LE_2M_PHY,
|
||||
HCI_LE_CODED_PHY,
|
||||
Role,
|
||||
HCI_Constant,
|
||||
HCI_Error,
|
||||
HCI_IsoDataPacket,
|
||||
HCI_StatusError,
|
||||
Role,
|
||||
)
|
||||
from bumble.pairing import PairingConfig
|
||||
from bumble.sdp import (
|
||||
SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
|
||||
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
@@ -67,8 +55,12 @@ from bumble.sdp import (
|
||||
DataElement,
|
||||
ServiceAttribute,
|
||||
)
|
||||
from bumble.transport import open_transport
|
||||
from bumble.transport import open_transport_or_link
|
||||
import bumble.rfcomm
|
||||
import bumble.core
|
||||
from bumble.utils import AsyncRunner
|
||||
from bumble.pairing import PairingConfig
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -83,28 +75,17 @@ DEFAULT_CENTRAL_ADDRESS = 'F0:F0:F0:F0:F0:F0'
|
||||
DEFAULT_CENTRAL_NAME = 'Speed Central'
|
||||
DEFAULT_PERIPHERAL_ADDRESS = 'F1:F1:F1:F1:F1:F1'
|
||||
DEFAULT_PERIPHERAL_NAME = 'Speed Peripheral'
|
||||
DEFAULT_ADVERTISING_INTERVAL = 100
|
||||
|
||||
SPEED_SERVICE_UUID = '50DB505C-8AC4-4738-8448-3B1D9CC09CC5'
|
||||
SPEED_TX_UUID = 'E789C754-41A1-45F4-A948-A0A1A90DBA53'
|
||||
SPEED_RX_UUID = '016A2CC7-E14B-4819-935F-1F56EAE4098D'
|
||||
|
||||
DEFAULT_RFCOMM_UUID = 'E6D55659-C8B4-4B85-96BB-B1143AF6D3AE'
|
||||
|
||||
DEFAULT_L2CAP_PSM = 128
|
||||
DEFAULT_L2CAP_MAX_CREDITS = 128
|
||||
DEFAULT_L2CAP_MTU = 1024
|
||||
DEFAULT_L2CAP_MPS = 1024
|
||||
|
||||
DEFAULT_ISO_MAX_SDU_C_TO_P = 251
|
||||
DEFAULT_ISO_MAX_SDU_P_TO_C = 251
|
||||
DEFAULT_ISO_SDU_INTERVAL_C_TO_P = 10000
|
||||
DEFAULT_ISO_SDU_INTERVAL_P_TO_C = 10000
|
||||
DEFAULT_ISO_MAX_TRANSPORT_LATENCY_C_TO_P = 35
|
||||
DEFAULT_ISO_MAX_TRANSPORT_LATENCY_P_TO_C = 35
|
||||
DEFAULT_ISO_RTN_C_TO_P = 3
|
||||
DEFAULT_ISO_RTN_P_TO_C = 3
|
||||
|
||||
DEFAULT_LINGER_TIME = 1.0
|
||||
DEFAULT_POST_CONNECTION_WAIT_TIME = 1.0
|
||||
|
||||
@@ -121,14 +102,14 @@ def le_phy_name(phy_id):
|
||||
)
|
||||
|
||||
|
||||
def print_connection_phy(phy: ConnectionPHY) -> None:
|
||||
def print_connection_phy(phy):
|
||||
logging.info(
|
||||
color('@@@ PHY: ', 'yellow') + f'TX:{le_phy_name(phy.tx_phy)}/'
|
||||
f'RX:{le_phy_name(phy.rx_phy)}'
|
||||
)
|
||||
|
||||
|
||||
def print_connection(connection: Connection) -> None:
|
||||
def print_connection(connection):
|
||||
params = []
|
||||
if connection.transport == PhysicalTransport.LE:
|
||||
params.append(
|
||||
@@ -153,34 +134,6 @@ def print_connection(connection: Connection) -> None:
|
||||
logging.info(color('@@@ Connection: ', 'yellow') + ' '.join(params))
|
||||
|
||||
|
||||
def print_cis_link(cis_link: CisLink) -> None:
|
||||
logging.info(color("@@@ CIS established", "green"))
|
||||
logging.info(color('@@@ ISO interval: ', 'green') + f"{cis_link.iso_interval}ms")
|
||||
logging.info(color('@@@ NSE: ', 'green') + f"{cis_link.nse}")
|
||||
logging.info(color('@@@ Central->Peripheral:', 'green'))
|
||||
if cis_link.phy_c_to_p is not None:
|
||||
logging.info(
|
||||
color('@@@ PHY: ', 'green') + f"{cis_link.phy_c_to_p.name}"
|
||||
)
|
||||
logging.info(
|
||||
color('@@@ Latency: ', 'green') + f"{cis_link.transport_latency_c_to_p}µs"
|
||||
)
|
||||
logging.info(color('@@@ BN: ', 'green') + f"{cis_link.bn_c_to_p}")
|
||||
logging.info(color('@@@ FT: ', 'green') + f"{cis_link.ft_c_to_p}")
|
||||
logging.info(color('@@@ Max PDU: ', 'green') + f"{cis_link.max_pdu_c_to_p}")
|
||||
logging.info(color('@@@ Peripheral->Central:', 'green'))
|
||||
if cis_link.phy_p_to_c is not None:
|
||||
logging.info(
|
||||
color('@@@ PHY: ', 'green') + f"{cis_link.phy_p_to_c.name}"
|
||||
)
|
||||
logging.info(
|
||||
color('@@@ Latency: ', 'green') + f"{cis_link.transport_latency_p_to_c}µs"
|
||||
)
|
||||
logging.info(color('@@@ BN: ', 'green') + f"{cis_link.bn_p_to_c}")
|
||||
logging.info(color('@@@ FT: ', 'green') + f"{cis_link.ft_p_to_c}")
|
||||
logging.info(color('@@@ Max PDU: ', 'green') + f"{cis_link.max_pdu_p_to_c}")
|
||||
|
||||
|
||||
def make_sdp_records(channel):
|
||||
return {
|
||||
0x00010001: [
|
||||
@@ -244,51 +197,6 @@ async def switch_roles(connection, role):
|
||||
logging.info(f'{color("### Role switch failed:", "red")} {error}')
|
||||
|
||||
|
||||
async def pre_power_on(device: Device, classic: bool) -> None:
|
||||
device.classic_enabled = classic
|
||||
|
||||
# Set up a pairing config factory with minimal requirements.
|
||||
device.config.keystore = "JsonKeyStore"
|
||||
device.pairing_config_factory = lambda _: PairingConfig(
|
||||
sc=False, mitm=False, bonding=False
|
||||
)
|
||||
|
||||
|
||||
async def post_power_on(
|
||||
device: Device,
|
||||
le_scan: tuple[int, int] | None,
|
||||
le_advertise: int | None,
|
||||
classic_page_scan: bool,
|
||||
classic_inquiry_scan: bool,
|
||||
) -> None:
|
||||
if classic_page_scan:
|
||||
logging.info(color("*** Enabling page scan", "blue"))
|
||||
await device.set_connectable(True)
|
||||
if classic_inquiry_scan:
|
||||
logging.info(color("*** Enabling inquiry scan", "blue"))
|
||||
await device.set_discoverable(True)
|
||||
|
||||
if le_scan:
|
||||
scan_window, scan_interval = le_scan
|
||||
logging.info(
|
||||
color(
|
||||
f"*** Starting LE scanning [{scan_window}ms/{scan_interval}ms]",
|
||||
"blue",
|
||||
)
|
||||
)
|
||||
await device.start_scanning(
|
||||
scan_interval=scan_interval, scan_window=scan_window
|
||||
)
|
||||
|
||||
if le_advertise:
|
||||
logging.info(color(f"*** Starting LE advertising [{le_advertise}ms]", "blue"))
|
||||
await device.start_advertising(
|
||||
advertising_interval_min=le_advertise,
|
||||
advertising_interval_max=le_advertise,
|
||||
auto_restart=True,
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Packet
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -506,8 +414,7 @@ class Sender:
|
||||
self.bytes_sent += len(packet)
|
||||
await self.packet_io.send_packet(packet)
|
||||
|
||||
if self.packet_io.can_receive():
|
||||
await self.done.wait()
|
||||
await self.done.wait()
|
||||
|
||||
run_counter = f'[{run + 1} of {self.repeat + 1}]' if self.repeat else ''
|
||||
logging.info(color(f'=== {run_counter} Done!', 'magenta'))
|
||||
@@ -537,9 +444,6 @@ class Sender:
|
||||
)
|
||||
self.done.set()
|
||||
|
||||
def is_sender(self):
|
||||
return True
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Receiver
|
||||
@@ -587,8 +491,7 @@ class Receiver:
|
||||
logging.info(
|
||||
color(
|
||||
f'!!! Unexpected packet, expected {self.expected_packet_index} '
|
||||
f'but received {packet.sequence}',
|
||||
'red',
|
||||
f'but received {packet.sequence}'
|
||||
)
|
||||
)
|
||||
|
||||
@@ -631,9 +534,6 @@ class Receiver:
|
||||
await self.done.wait()
|
||||
logging.info(color('=== Done!', 'magenta'))
|
||||
|
||||
def is_sender(self):
|
||||
return False
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Ping
|
||||
@@ -769,8 +669,7 @@ class Ping:
|
||||
color(
|
||||
f'!!! Unexpected packet, '
|
||||
f'expected {self.next_expected_packet_index} '
|
||||
f'but received {packet.sequence}',
|
||||
'red',
|
||||
f'but received {packet.sequence}'
|
||||
)
|
||||
)
|
||||
|
||||
@@ -778,9 +677,6 @@ class Ping:
|
||||
self.done.set()
|
||||
return
|
||||
|
||||
def is_sender(self):
|
||||
return True
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Pong
|
||||
@@ -825,8 +721,7 @@ class Pong:
|
||||
logging.info(
|
||||
color(
|
||||
f'!!! Unexpected packet, expected {self.expected_packet_index} '
|
||||
f'but received {packet.sequence}',
|
||||
'red',
|
||||
f'but received {packet.sequence}'
|
||||
)
|
||||
)
|
||||
|
||||
@@ -848,9 +743,6 @@ class Pong:
|
||||
await self.done.wait()
|
||||
logging.info(color('=== Done!', 'magenta'))
|
||||
|
||||
def is_sender(self):
|
||||
return False
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# GattClient
|
||||
@@ -1014,9 +906,6 @@ class StreamedPacketIO:
|
||||
# pylint: disable-next=not-callable
|
||||
self.io_sink(struct.pack('>H', len(packet)) + packet)
|
||||
|
||||
def can_receive(self):
|
||||
return True
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# L2capClient
|
||||
@@ -1288,96 +1177,6 @@ class RfcommServer(StreamedPacketIO):
|
||||
await self.dlc.drain()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# IsoClient
|
||||
# -----------------------------------------------------------------------------
|
||||
class IsoClient(StreamedPacketIO):
|
||||
def __init__(
|
||||
self,
|
||||
device: Device,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.device = device
|
||||
self.ready = asyncio.Event()
|
||||
self.cis_link: CisLink | None = None
|
||||
|
||||
async def on_connection(
|
||||
self, connection: Connection, cis_link: CisLink, sender: bool
|
||||
) -> None:
|
||||
connection.on(connection.EVENT_DISCONNECTION, self.on_disconnection)
|
||||
self.cis_link = cis_link
|
||||
self.io_sink = cis_link.write
|
||||
await cis_link.setup_data_path(
|
||||
cis_link.Direction.HOST_TO_CONTROLLER
|
||||
if sender
|
||||
else cis_link.Direction.CONTROLLER_TO_HOST
|
||||
)
|
||||
cis_link.sink = self.on_iso_packet
|
||||
self.ready.set()
|
||||
|
||||
def on_iso_packet(self, iso_packet: HCI_IsoDataPacket) -> None:
|
||||
self.on_packet(iso_packet.iso_sdu_fragment)
|
||||
|
||||
def on_disconnection(self, _):
|
||||
pass
|
||||
|
||||
async def drain(self):
|
||||
if self.cis_link is None:
|
||||
return
|
||||
await self.cis_link.drain()
|
||||
|
||||
def can_receive(self):
|
||||
return False
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# IsoServer
|
||||
# -----------------------------------------------------------------------------
|
||||
class IsoServer(StreamedPacketIO):
|
||||
def __init__(
|
||||
self,
|
||||
device: Device,
|
||||
):
|
||||
super().__init__()
|
||||
self.device = device
|
||||
self.cis_link: CisLink | None = None
|
||||
self.ready = asyncio.Event()
|
||||
|
||||
logging.info(
|
||||
color(
|
||||
'### Listening for ISO connection',
|
||||
'yellow',
|
||||
)
|
||||
)
|
||||
|
||||
async def on_connection(
|
||||
self, connection: Connection, cis_link: CisLink, sender: bool
|
||||
) -> None:
|
||||
connection.on(connection.EVENT_DISCONNECTION, self.on_disconnection)
|
||||
self.io_sink = cis_link.write
|
||||
await cis_link.setup_data_path(
|
||||
cis_link.Direction.HOST_TO_CONTROLLER
|
||||
if sender
|
||||
else cis_link.Direction.CONTROLLER_TO_HOST
|
||||
)
|
||||
cis_link.sink = self.on_iso_packet
|
||||
self.ready.set()
|
||||
|
||||
def on_iso_packet(self, iso_packet: HCI_IsoDataPacket) -> None:
|
||||
self.on_packet(iso_packet.iso_sdu_fragment)
|
||||
|
||||
def on_disconnection(self, _):
|
||||
pass
|
||||
|
||||
async def drain(self):
|
||||
if self.cis_link is None:
|
||||
return
|
||||
await self.cis_link.drain()
|
||||
|
||||
def can_receive(self):
|
||||
return False
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Central
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -1386,52 +1185,26 @@ class Central(Connection.Listener):
|
||||
self,
|
||||
transport,
|
||||
peripheral_address,
|
||||
classic,
|
||||
scenario_factory,
|
||||
mode_factory,
|
||||
connection_interval,
|
||||
phy,
|
||||
authenticate,
|
||||
encrypt,
|
||||
iso,
|
||||
iso_sdu_interval_c_to_p,
|
||||
iso_sdu_interval_p_to_c,
|
||||
iso_max_sdu_c_to_p,
|
||||
iso_max_sdu_p_to_c,
|
||||
iso_max_transport_latency_c_to_p,
|
||||
iso_max_transport_latency_p_to_c,
|
||||
iso_rtn_c_to_p,
|
||||
iso_rtn_p_to_c,
|
||||
classic,
|
||||
extended_data_length,
|
||||
role_switch,
|
||||
le_scan,
|
||||
le_advertise,
|
||||
classic_page_scan,
|
||||
classic_inquiry_scan,
|
||||
):
|
||||
super().__init__()
|
||||
self.transport = transport
|
||||
self.peripheral_address = peripheral_address
|
||||
self.classic = classic
|
||||
self.iso = iso
|
||||
self.iso_sdu_interval_c_to_p = iso_sdu_interval_c_to_p
|
||||
self.iso_sdu_interval_p_to_c = iso_sdu_interval_p_to_c
|
||||
self.iso_max_sdu_c_to_p = iso_max_sdu_c_to_p
|
||||
self.iso_max_sdu_p_to_c = iso_max_sdu_p_to_c
|
||||
self.iso_max_transport_latency_c_to_p = iso_max_transport_latency_c_to_p
|
||||
self.iso_max_transport_latency_p_to_c = iso_max_transport_latency_p_to_c
|
||||
self.iso_rtn_c_to_p = iso_rtn_c_to_p
|
||||
self.iso_rtn_p_to_c = iso_rtn_p_to_c
|
||||
self.scenario_factory = scenario_factory
|
||||
self.mode_factory = mode_factory
|
||||
self.authenticate = authenticate
|
||||
self.encrypt = encrypt or authenticate
|
||||
self.extended_data_length = extended_data_length
|
||||
self.role_switch = role_switch
|
||||
self.le_scan = le_scan
|
||||
self.le_advertise = le_advertise
|
||||
self.classic_page_scan = classic_page_scan
|
||||
self.classic_inquiry_scan = classic_inquiry_scan
|
||||
self.device = None
|
||||
self.connection = None
|
||||
|
||||
@@ -1468,7 +1241,7 @@ class Central(Connection.Listener):
|
||||
|
||||
async def run(self):
|
||||
logging.info(color('>>> Connecting to HCI...', 'green'))
|
||||
async with await open_transport(self.transport) as (
|
||||
async with await open_transport_or_link(self.transport) as (
|
||||
hci_source,
|
||||
hci_sink,
|
||||
):
|
||||
@@ -1481,22 +1254,18 @@ class Central(Connection.Listener):
|
||||
mode = self.mode_factory(self.device)
|
||||
scenario = self.scenario_factory(mode)
|
||||
self.device.classic_enabled = self.classic
|
||||
self.device.cis_enabled = self.iso
|
||||
|
||||
# Set up a pairing config factory with minimal requirements.
|
||||
self.device.config.keystore = "JsonKeyStore"
|
||||
self.device.pairing_config_factory = lambda _: PairingConfig(
|
||||
sc=False, mitm=False, bonding=False
|
||||
)
|
||||
|
||||
await pre_power_on(self.device, self.classic)
|
||||
await self.device.power_on()
|
||||
await post_power_on(
|
||||
self.device,
|
||||
self.le_scan,
|
||||
self.le_advertise,
|
||||
self.classic_page_scan,
|
||||
self.classic_inquiry_scan,
|
||||
)
|
||||
|
||||
if self.classic:
|
||||
await self.device.set_discoverable(False)
|
||||
await self.device.set_connectable(False)
|
||||
|
||||
logging.info(
|
||||
color(f'### Connecting to {self.peripheral_address}...', 'cyan')
|
||||
@@ -1571,72 +1340,7 @@ class Central(Connection.Listener):
|
||||
)
|
||||
)
|
||||
|
||||
# Setup ISO streams.
|
||||
if self.iso:
|
||||
if scenario.is_sender():
|
||||
sdu_interval_c_to_p = (
|
||||
self.iso_sdu_interval_c_to_p or DEFAULT_ISO_SDU_INTERVAL_C_TO_P
|
||||
)
|
||||
sdu_interval_p_to_c = self.iso_sdu_interval_p_to_c or 0
|
||||
max_transport_latency_c_to_p = (
|
||||
self.iso_max_transport_latency_c_to_p
|
||||
or DEFAULT_ISO_MAX_TRANSPORT_LATENCY_C_TO_P
|
||||
)
|
||||
max_transport_latency_p_to_c = (
|
||||
self.iso_max_transport_latency_p_to_c or 0
|
||||
)
|
||||
max_sdu_c_to_p = (
|
||||
self.iso_max_sdu_c_to_p or DEFAULT_ISO_MAX_SDU_C_TO_P
|
||||
)
|
||||
max_sdu_p_to_c = self.iso_max_sdu_p_to_c or 0
|
||||
rtn_c_to_p = self.iso_rtn_c_to_p or DEFAULT_ISO_RTN_C_TO_P
|
||||
rtn_p_to_c = self.iso_rtn_p_to_c or 0
|
||||
else:
|
||||
sdu_interval_p_to_c = (
|
||||
self.iso_sdu_interval_p_to_c or DEFAULT_ISO_SDU_INTERVAL_P_TO_C
|
||||
)
|
||||
sdu_interval_c_to_p = self.iso_sdu_interval_c_to_p or 0
|
||||
max_transport_latency_p_to_c = (
|
||||
self.iso_max_transport_latency_p_to_c
|
||||
or DEFAULT_ISO_MAX_TRANSPORT_LATENCY_P_TO_C
|
||||
)
|
||||
max_transport_latency_c_to_p = (
|
||||
self.iso_max_transport_latency_c_to_p or 0
|
||||
)
|
||||
max_sdu_p_to_c = (
|
||||
self.iso_max_sdu_p_to_c or DEFAULT_ISO_MAX_SDU_P_TO_C
|
||||
)
|
||||
max_sdu_c_to_p = self.iso_max_sdu_c_to_p or 0
|
||||
rtn_p_to_c = self.iso_rtn_p_to_c or DEFAULT_ISO_RTN_P_TO_C
|
||||
rtn_c_to_p = self.iso_rtn_c_to_p or 0
|
||||
cis_handles = await self.device.setup_cig(
|
||||
CigParameters(
|
||||
cig_id=1,
|
||||
sdu_interval_c_to_p=sdu_interval_c_to_p,
|
||||
sdu_interval_p_to_c=sdu_interval_p_to_c,
|
||||
max_transport_latency_c_to_p=max_transport_latency_c_to_p,
|
||||
max_transport_latency_p_to_c=max_transport_latency_p_to_c,
|
||||
cis_parameters=[
|
||||
CigParameters.CisParameters(
|
||||
cis_id=2,
|
||||
max_sdu_c_to_p=max_sdu_c_to_p,
|
||||
max_sdu_p_to_c=max_sdu_p_to_c,
|
||||
rtn_c_to_p=rtn_c_to_p,
|
||||
rtn_p_to_c=rtn_p_to_c,
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
cis_link = (
|
||||
await self.device.create_cis([(cis_handles[0], self.connection)])
|
||||
)[0]
|
||||
print_cis_link(cis_link)
|
||||
|
||||
await mode.on_connection(
|
||||
self.connection, cis_link, scenario.is_sender()
|
||||
)
|
||||
else:
|
||||
await mode.on_connection(self.connection)
|
||||
await mode.on_connection(self.connection)
|
||||
|
||||
await scenario.run()
|
||||
await asyncio.sleep(DEFAULT_LINGER_TIME)
|
||||
@@ -1672,38 +1376,24 @@ class Peripheral(Device.Listener, Connection.Listener):
|
||||
scenario_factory,
|
||||
mode_factory,
|
||||
classic,
|
||||
iso,
|
||||
extended_data_length,
|
||||
role_switch,
|
||||
le_scan,
|
||||
le_advertise,
|
||||
classic_page_scan,
|
||||
classic_inquiry_scan,
|
||||
):
|
||||
self.transport = transport
|
||||
self.classic = classic
|
||||
self.iso = iso
|
||||
self.scenario_factory = scenario_factory
|
||||
self.mode_factory = mode_factory
|
||||
self.extended_data_length = extended_data_length
|
||||
self.role_switch = role_switch
|
||||
self.le_scan = le_scan
|
||||
self.classic_page_scan = classic_page_scan
|
||||
self.classic_inquiry_scan = classic_inquiry_scan
|
||||
self.scenario = None
|
||||
self.mode = None
|
||||
self.device = None
|
||||
self.connection = None
|
||||
self.connected = asyncio.Event()
|
||||
|
||||
if le_advertise:
|
||||
self.le_advertise = le_advertise
|
||||
else:
|
||||
self.le_advertise = 0 if classic else DEFAULT_ADVERTISING_INTERVAL
|
||||
|
||||
async def run(self):
|
||||
logging.info(color('>>> Connecting to HCI...', 'green'))
|
||||
async with await open_transport(self.transport) as (
|
||||
async with await open_transport_or_link(self.transport) as (
|
||||
hci_source,
|
||||
hci_sink,
|
||||
):
|
||||
@@ -1717,22 +1407,20 @@ class Peripheral(Device.Listener, Connection.Listener):
|
||||
self.mode = self.mode_factory(self.device)
|
||||
self.scenario = self.scenario_factory(self.mode)
|
||||
self.device.classic_enabled = self.classic
|
||||
self.device.cis_enabled = self.iso
|
||||
|
||||
# Set up a pairing config factory with minimal requirements.
|
||||
self.device.config.keystore = "JsonKeyStore"
|
||||
self.device.pairing_config_factory = lambda _: PairingConfig(
|
||||
sc=False, mitm=False, bonding=False
|
||||
)
|
||||
|
||||
await pre_power_on(self.device, self.classic)
|
||||
await self.device.power_on()
|
||||
await post_power_on(
|
||||
self.device,
|
||||
self.le_scan,
|
||||
self.le_advertise,
|
||||
self.classic or self.classic_page_scan,
|
||||
self.classic or self.classic_inquiry_scan,
|
||||
)
|
||||
|
||||
if self.classic:
|
||||
await self.device.set_discoverable(True)
|
||||
await self.device.set_connectable(True)
|
||||
else:
|
||||
await self.device.start_advertising(auto_restart=True)
|
||||
|
||||
if self.classic:
|
||||
logging.info(
|
||||
@@ -1754,21 +1442,7 @@ class Peripheral(Device.Listener, Connection.Listener):
|
||||
logging.info(color('### Connected', 'cyan'))
|
||||
print_connection(self.connection)
|
||||
|
||||
if self.iso:
|
||||
|
||||
async def on_cis_request(cis_link: CisLink) -> None:
|
||||
logging.info(color("@@@ Accepting CIS", "green"))
|
||||
await self.device.accept_cis_request(cis_link)
|
||||
print_cis_link(cis_link)
|
||||
|
||||
await self.mode.on_connection(
|
||||
self.connection, cis_link, self.scenario.is_sender()
|
||||
)
|
||||
|
||||
self.connection.on(self.connection.EVENT_CIS_REQUEST, on_cis_request)
|
||||
else:
|
||||
await self.mode.on_connection(self.connection)
|
||||
|
||||
await self.mode.on_connection(self.connection)
|
||||
await self.scenario.run()
|
||||
await asyncio.sleep(DEFAULT_LINGER_TIME)
|
||||
|
||||
@@ -1777,14 +1451,10 @@ class Peripheral(Device.Listener, Connection.Listener):
|
||||
self.connection = connection
|
||||
self.connected.set()
|
||||
|
||||
# Stop being discoverable and connectable if possible
|
||||
# Stop being discoverable and connectable
|
||||
if self.classic:
|
||||
if not self.classic_inquiry_scan:
|
||||
logging.info(color("*** Stopping inquiry scan", "blue"))
|
||||
AsyncRunner.spawn(self.device.set_discoverable(False))
|
||||
if not self.classic_page_scan:
|
||||
logging.info(color("*** Stopping page scan", "blue"))
|
||||
AsyncRunner.spawn(self.device.set_connectable(False))
|
||||
AsyncRunner.spawn(self.device.set_discoverable(False))
|
||||
AsyncRunner.spawn(self.device.set_connectable(False))
|
||||
|
||||
# Request a new data length if needed
|
||||
if not self.classic and self.extended_data_length:
|
||||
@@ -1805,9 +1475,7 @@ class Peripheral(Device.Listener, Connection.Listener):
|
||||
self.scenario.reset()
|
||||
|
||||
if self.classic:
|
||||
logging.info(color("*** Enabling inquiry scan", "blue"))
|
||||
AsyncRunner.spawn(self.device.set_discoverable(True))
|
||||
logging.info(color("*** Enabling page scan", "blue"))
|
||||
AsyncRunner.spawn(self.device.set_connectable(True))
|
||||
|
||||
def on_connection_parameters_update(self):
|
||||
@@ -1880,12 +1548,6 @@ def create_mode_factory(ctx, default_mode):
|
||||
credits_threshold=ctx.obj['rfcomm_credits_threshold'],
|
||||
)
|
||||
|
||||
if mode == 'iso-server':
|
||||
return IsoServer(device)
|
||||
|
||||
if mode == 'iso-client':
|
||||
return IsoClient(device)
|
||||
|
||||
raise ValueError('invalid mode')
|
||||
|
||||
return create_mode
|
||||
@@ -1913,9 +1575,6 @@ def create_scenario_factory(ctx, default_scenario):
|
||||
return Receiver(packet_io, ctx.obj['linger'])
|
||||
|
||||
if scenario == 'ping':
|
||||
if isinstance(packet_io, (IsoClient, IsoServer)):
|
||||
raise ValueError('ping not supported with ISO')
|
||||
|
||||
return Ping(
|
||||
packet_io,
|
||||
start_delay=ctx.obj['start_delay'],
|
||||
@@ -1927,9 +1586,6 @@ def create_scenario_factory(ctx, default_scenario):
|
||||
)
|
||||
|
||||
if scenario == 'pong':
|
||||
if isinstance(packet_io, (IsoClient, IsoServer)):
|
||||
raise ValueError('pong not supported with ISO')
|
||||
|
||||
return Pong(packet_io, ctx.obj['linger'])
|
||||
|
||||
raise ValueError('invalid scenario')
|
||||
@@ -1953,8 +1609,6 @@ def create_scenario_factory(ctx, default_scenario):
|
||||
'l2cap-server',
|
||||
'rfcomm-client',
|
||||
'rfcomm-server',
|
||||
'iso-client',
|
||||
'iso-server',
|
||||
]
|
||||
),
|
||||
)
|
||||
@@ -1967,7 +1621,6 @@ def create_scenario_factory(ctx, default_scenario):
|
||||
)
|
||||
@click.option(
|
||||
'--extended-data-length',
|
||||
metavar='<TX-OCTETS>/<TX-TIME>',
|
||||
help='Request a data length upon connection, specified as tx_octets/tx_time',
|
||||
)
|
||||
@click.option(
|
||||
@@ -1975,26 +1628,6 @@ def create_scenario_factory(ctx, default_scenario):
|
||||
type=click.Choice(['central', 'peripheral']),
|
||||
help='Request role switch upon connection (central or peripheral)',
|
||||
)
|
||||
@click.option(
|
||||
'--le-scan',
|
||||
metavar='<WINDOW>/<INTERVAL>',
|
||||
help='Perform an LE scan with a given window and interval (milliseconds)',
|
||||
)
|
||||
@click.option(
|
||||
'--le-advertise',
|
||||
metavar='<INTERVAL>',
|
||||
help='Advertise with a given interval (milliseconds)',
|
||||
)
|
||||
@click.option(
|
||||
'--classic-page-scan',
|
||||
is_flag=True,
|
||||
help='Enable Classic page scanning',
|
||||
)
|
||||
@click.option(
|
||||
'--classic-inquiry-scan',
|
||||
is_flag=True,
|
||||
help='Enable Classic enquiry scanning',
|
||||
)
|
||||
@click.option(
|
||||
'--rfcomm-channel',
|
||||
type=int,
|
||||
@@ -2120,10 +1753,6 @@ def bench(
|
||||
att_mtu,
|
||||
extended_data_length,
|
||||
role_switch,
|
||||
le_scan,
|
||||
le_advertise,
|
||||
classic_page_scan,
|
||||
classic_inquiry_scan,
|
||||
packet_size,
|
||||
packet_count,
|
||||
start_delay,
|
||||
@@ -2172,12 +1801,7 @@ def bench(
|
||||
else None
|
||||
)
|
||||
ctx.obj['role_switch'] = role_switch
|
||||
ctx.obj['le_scan'] = [float(x) for x in le_scan.split('/')] if le_scan else None
|
||||
ctx.obj['le_advertise'] = float(le_advertise) if le_advertise else None
|
||||
ctx.obj['classic_page_scan'] = classic_page_scan
|
||||
ctx.obj['classic_inquiry_scan'] = classic_inquiry_scan
|
||||
ctx.obj['classic'] = mode in ('rfcomm-client', 'rfcomm-server')
|
||||
ctx.obj['iso'] = mode in ('iso-client', 'iso-server')
|
||||
|
||||
|
||||
@bench.command()
|
||||
@@ -2199,94 +1823,28 @@ def bench(
|
||||
@click.option('--phy', type=click.Choice(['1m', '2m', 'coded']), help='PHY to use')
|
||||
@click.option('--authenticate', is_flag=True, help='Authenticate (RFComm only)')
|
||||
@click.option('--encrypt', is_flag=True, help='Encrypt the connection (RFComm only)')
|
||||
@click.option(
|
||||
'--iso-sdu-interval-c-to-p',
|
||||
type=int,
|
||||
help='ISO SDU central -> peripheral (microseconds)',
|
||||
)
|
||||
@click.option(
|
||||
'--iso-sdu-interval-p-to-c',
|
||||
type=int,
|
||||
help='ISO SDU interval peripheral -> central (microseconds)',
|
||||
)
|
||||
@click.option(
|
||||
'--iso-max-sdu-c-to-p',
|
||||
type=int,
|
||||
help='ISO max SDU central -> peripheral',
|
||||
)
|
||||
@click.option(
|
||||
'--iso-max-sdu-p-to-c',
|
||||
type=int,
|
||||
help='ISO max SDU peripheral -> central',
|
||||
)
|
||||
@click.option(
|
||||
'--iso-max-transport-latency-c-to-p',
|
||||
type=int,
|
||||
help='ISO max transport latency central -> peripheral (milliseconds)',
|
||||
)
|
||||
@click.option(
|
||||
'--iso-max-transport-latency-p-to-c',
|
||||
type=int,
|
||||
help='ISO max transport latency peripheral -> central (milliseconds)',
|
||||
)
|
||||
@click.option(
|
||||
'--iso-rtn-c-to-p',
|
||||
type=int,
|
||||
help='ISO RTN central -> peripheral (integer count)',
|
||||
)
|
||||
@click.option(
|
||||
'--iso-rtn-p-to-c',
|
||||
type=int,
|
||||
help='ISO RTN peripheral -> central (integer count)',
|
||||
)
|
||||
@click.pass_context
|
||||
def central(
|
||||
ctx,
|
||||
transport,
|
||||
peripheral_address,
|
||||
connection_interval,
|
||||
phy,
|
||||
authenticate,
|
||||
encrypt,
|
||||
iso_sdu_interval_c_to_p,
|
||||
iso_sdu_interval_p_to_c,
|
||||
iso_max_sdu_c_to_p,
|
||||
iso_max_sdu_p_to_c,
|
||||
iso_max_transport_latency_c_to_p,
|
||||
iso_max_transport_latency_p_to_c,
|
||||
iso_rtn_c_to_p,
|
||||
iso_rtn_p_to_c,
|
||||
ctx, transport, peripheral_address, connection_interval, phy, authenticate, encrypt
|
||||
):
|
||||
"""Run as a central (initiates the connection)"""
|
||||
scenario_factory = create_scenario_factory(ctx, 'send')
|
||||
mode_factory = create_mode_factory(ctx, 'gatt-client')
|
||||
classic = ctx.obj['classic']
|
||||
|
||||
async def run_central():
|
||||
await Central(
|
||||
transport,
|
||||
peripheral_address,
|
||||
classic,
|
||||
scenario_factory,
|
||||
mode_factory,
|
||||
connection_interval,
|
||||
phy,
|
||||
authenticate,
|
||||
encrypt or authenticate,
|
||||
ctx.obj['iso'],
|
||||
iso_sdu_interval_c_to_p,
|
||||
iso_sdu_interval_p_to_c,
|
||||
iso_max_sdu_c_to_p,
|
||||
iso_max_sdu_p_to_c,
|
||||
iso_max_transport_latency_c_to_p,
|
||||
iso_max_transport_latency_p_to_c,
|
||||
iso_rtn_c_to_p,
|
||||
iso_rtn_p_to_c,
|
||||
ctx.obj['classic'],
|
||||
ctx.obj['extended_data_length'],
|
||||
ctx.obj['role_switch'],
|
||||
ctx.obj['le_scan'],
|
||||
ctx.obj['le_advertise'],
|
||||
ctx.obj['classic_page_scan'],
|
||||
ctx.obj['classic_inquiry_scan'],
|
||||
).run()
|
||||
|
||||
asyncio.run(run_central())
|
||||
@@ -2306,20 +1864,19 @@ def peripheral(ctx, transport):
|
||||
scenario_factory,
|
||||
mode_factory,
|
||||
ctx.obj['classic'],
|
||||
ctx.obj['iso'],
|
||||
ctx.obj['extended_data_length'],
|
||||
ctx.obj['role_switch'],
|
||||
ctx.obj['le_scan'],
|
||||
ctx.obj['le_advertise'],
|
||||
ctx.obj['classic_page_scan'],
|
||||
ctx.obj['classic_inquiry_scan'],
|
||||
).run()
|
||||
|
||||
asyncio.run(run_peripheral())
|
||||
|
||||
|
||||
def main():
|
||||
bumble.logging.setup_basic_logging('INFO')
|
||||
logging.basicConfig(
|
||||
level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper(),
|
||||
format="[%(asctime)s.%(msecs)03d] %(levelname)s:%(name)s:%(message)s",
|
||||
datefmt="%H:%M:%S",
|
||||
)
|
||||
bench()
|
||||
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
# limitations under the License.
|
||||
|
||||
import click
|
||||
|
||||
from bumble.colors import color
|
||||
from bumble.hci import Address
|
||||
from bumble.helpers import generate_irk, verify_rpa_with_irk
|
||||
|
||||
@@ -23,54 +23,58 @@ import asyncio
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import humanize
|
||||
from typing import Optional, Union
|
||||
from collections import OrderedDict
|
||||
|
||||
import click
|
||||
import humanize
|
||||
from prettytable import PrettyTable
|
||||
|
||||
from prompt_toolkit import Application
|
||||
from prompt_toolkit.completion import Completer, Completion, NestedCompleter
|
||||
from prompt_toolkit.data_structures import Point
|
||||
from prompt_toolkit.filters import Condition
|
||||
from prompt_toolkit.formatted_text import ANSI
|
||||
from prompt_toolkit.history import FileHistory
|
||||
from prompt_toolkit.completion import Completer, Completion, NestedCompleter
|
||||
from prompt_toolkit.key_binding import KeyBindings
|
||||
from prompt_toolkit.formatted_text import ANSI
|
||||
from prompt_toolkit.styles import Style
|
||||
from prompt_toolkit.filters import Condition
|
||||
from prompt_toolkit.widgets import TextArea, Frame
|
||||
from prompt_toolkit.widgets.toolbars import FormattedTextToolbar
|
||||
from prompt_toolkit.data_structures import Point
|
||||
from prompt_toolkit.layout import (
|
||||
Layout,
|
||||
HSplit,
|
||||
Window,
|
||||
CompletionsMenu,
|
||||
Float,
|
||||
FormattedTextControl,
|
||||
FloatContainer,
|
||||
ConditionalContainer,
|
||||
Dimension,
|
||||
Float,
|
||||
FloatContainer,
|
||||
FormattedTextControl,
|
||||
HSplit,
|
||||
Layout,
|
||||
Window,
|
||||
)
|
||||
from prompt_toolkit.styles import Style
|
||||
from prompt_toolkit.widgets import Frame, TextArea
|
||||
from prompt_toolkit.widgets.toolbars import FormattedTextToolbar
|
||||
|
||||
from bumble import __version__
|
||||
import bumble.core
|
||||
from bumble import __version__, colors
|
||||
from bumble.core import UUID, AdvertisingData
|
||||
from bumble import colors
|
||||
from bumble.core import UUID, AdvertisingData, PhysicalTransport
|
||||
from bumble.device import (
|
||||
Connection,
|
||||
ConnectionParametersPreferences,
|
||||
ConnectionPHY,
|
||||
Device,
|
||||
Connection,
|
||||
Peer,
|
||||
)
|
||||
from bumble.gatt import Characteristic, CharacteristicDeclaration, Descriptor, Service
|
||||
from bumble.utils import AsyncRunner
|
||||
from bumble.transport import open_transport_or_link
|
||||
from bumble.gatt import Characteristic, Service, CharacteristicDeclaration, Descriptor
|
||||
from bumble.gatt_client import CharacteristicProxy
|
||||
from bumble.hci import (
|
||||
Address,
|
||||
HCI_Constant,
|
||||
HCI_LE_1M_PHY,
|
||||
HCI_LE_2M_PHY,
|
||||
HCI_LE_CODED_PHY,
|
||||
Address,
|
||||
HCI_Constant,
|
||||
)
|
||||
from bumble.transport import open_transport
|
||||
from bumble.utils import AsyncRunner
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
@@ -125,8 +129,8 @@ def parse_phys(phys):
|
||||
# Console App
|
||||
# -----------------------------------------------------------------------------
|
||||
class ConsoleApp:
|
||||
connected_peer: Peer | None
|
||||
connection_phy: ConnectionPHY | None
|
||||
connected_peer: Optional[Peer]
|
||||
connection_phy: Optional[ConnectionPHY]
|
||||
|
||||
def __init__(self):
|
||||
self.known_addresses = set()
|
||||
@@ -287,7 +291,7 @@ class ConsoleApp:
|
||||
async def run_async(self, device_config, transport):
|
||||
rssi_monitoring_task = asyncio.create_task(self.rssi_monitor_loop())
|
||||
|
||||
async with await open_transport(transport) as (hci_source, hci_sink):
|
||||
async with await open_transport_or_link(transport) as (hci_source, hci_sink):
|
||||
if device_config:
|
||||
self.device = Device.from_config_file_with_hci(
|
||||
device_config, hci_source, hci_sink
|
||||
@@ -519,7 +523,7 @@ class ConsoleApp:
|
||||
|
||||
self.show_attributes(attributes)
|
||||
|
||||
def find_remote_characteristic(self, param) -> CharacteristicProxy | None:
|
||||
def find_remote_characteristic(self, param) -> Optional[CharacteristicProxy]:
|
||||
if not self.connected_peer:
|
||||
return None
|
||||
parts = param.split('.')
|
||||
@@ -541,7 +545,9 @@ class ConsoleApp:
|
||||
|
||||
return None
|
||||
|
||||
def find_local_attribute(self, param) -> Characteristic | Descriptor | None:
|
||||
def find_local_attribute(
|
||||
self, param
|
||||
) -> Optional[Union[Characteristic, Descriptor]]:
|
||||
parts = param.split('.')
|
||||
if len(parts) == 3:
|
||||
service_uuid = UUID(parts[0])
|
||||
@@ -1093,7 +1099,9 @@ class DeviceListener(Device.Listener, Connection.Listener):
|
||||
if self.app.connected_peer.connection.is_encrypted
|
||||
else 'not encrypted'
|
||||
)
|
||||
self.app.append_to_output(f'connection encryption change: {encryption_state}')
|
||||
self.app.append_to_output(
|
||||
'connection encryption change: ' f'{encryption_state}'
|
||||
)
|
||||
|
||||
def on_connection_data_length_change(self):
|
||||
self.app.append_to_output(
|
||||
|
||||
@@ -16,47 +16,49 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import os
|
||||
import logging
|
||||
import time
|
||||
|
||||
import click
|
||||
|
||||
import bumble.logging
|
||||
from bumble.colors import color
|
||||
from bumble.company_ids import COMPANY_IDENTIFIERS
|
||||
from bumble.colors import color
|
||||
from bumble.core import name_or_number
|
||||
from bumble.hci import (
|
||||
HCI_LE_READ_BUFFER_SIZE_COMMAND,
|
||||
HCI_LE_READ_BUFFER_SIZE_V2_COMMAND,
|
||||
HCI_LE_READ_MAXIMUM_ADVERTISING_DATA_LENGTH_COMMAND,
|
||||
HCI_LE_READ_MAXIMUM_DATA_LENGTH_COMMAND,
|
||||
HCI_LE_READ_NUMBER_OF_SUPPORTED_ADVERTISING_SETS_COMMAND,
|
||||
HCI_LE_READ_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND,
|
||||
HCI_READ_BD_ADDR_COMMAND,
|
||||
HCI_READ_BUFFER_SIZE_COMMAND,
|
||||
HCI_READ_LOCAL_NAME_COMMAND,
|
||||
HCI_SUCCESS,
|
||||
map_null_terminated_utf8_string,
|
||||
CodecID,
|
||||
LeFeature,
|
||||
HCI_SUCCESS,
|
||||
HCI_VERSION_NAMES,
|
||||
LMP_VERSION_NAMES,
|
||||
HCI_Command,
|
||||
HCI_Command_Complete_Event,
|
||||
HCI_Command_Status_Event,
|
||||
HCI_LE_Read_Buffer_Size_Command,
|
||||
HCI_LE_Read_Buffer_Size_V2_Command,
|
||||
HCI_LE_Read_Maximum_Advertising_Data_Length_Command,
|
||||
HCI_LE_Read_Maximum_Data_Length_Command,
|
||||
HCI_LE_Read_Number_Of_Supported_Advertising_Sets_Command,
|
||||
HCI_LE_Read_Suggested_Default_Data_Length_Command,
|
||||
HCI_Read_BD_ADDR_Command,
|
||||
HCI_READ_BUFFER_SIZE_COMMAND,
|
||||
HCI_Read_Buffer_Size_Command,
|
||||
HCI_LE_READ_BUFFER_SIZE_V2_COMMAND,
|
||||
HCI_LE_Read_Buffer_Size_V2_Command,
|
||||
HCI_READ_BD_ADDR_COMMAND,
|
||||
HCI_Read_BD_ADDR_Command,
|
||||
HCI_READ_LOCAL_NAME_COMMAND,
|
||||
HCI_Read_Local_Name_Command,
|
||||
HCI_LE_READ_BUFFER_SIZE_COMMAND,
|
||||
HCI_LE_Read_Buffer_Size_Command,
|
||||
HCI_LE_READ_MAXIMUM_DATA_LENGTH_COMMAND,
|
||||
HCI_LE_Read_Maximum_Data_Length_Command,
|
||||
HCI_LE_READ_NUMBER_OF_SUPPORTED_ADVERTISING_SETS_COMMAND,
|
||||
HCI_LE_Read_Number_Of_Supported_Advertising_Sets_Command,
|
||||
HCI_LE_READ_MAXIMUM_ADVERTISING_DATA_LENGTH_COMMAND,
|
||||
HCI_LE_Read_Maximum_Advertising_Data_Length_Command,
|
||||
HCI_LE_READ_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND,
|
||||
HCI_LE_Read_Suggested_Default_Data_Length_Command,
|
||||
HCI_Read_Local_Supported_Codecs_Command,
|
||||
HCI_Read_Local_Supported_Codecs_V2_Command,
|
||||
HCI_Read_Local_Version_Information_Command,
|
||||
LeFeature,
|
||||
SpecificationVersion,
|
||||
map_null_terminated_utf8_string,
|
||||
)
|
||||
from bumble.host import Host
|
||||
from bumble.transport import open_transport
|
||||
from bumble.transport import open_transport_or_link
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -240,43 +242,28 @@ async def get_codecs_info(host: Host) -> None:
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def async_main(
|
||||
latency_probes, latency_probe_interval, latency_probe_command, transport
|
||||
):
|
||||
async def async_main(latency_probes, transport):
|
||||
print('<<< connecting to HCI...')
|
||||
async with await open_transport(transport) as (hci_source, hci_sink):
|
||||
async with await open_transport_or_link(transport) as (hci_source, hci_sink):
|
||||
print('<<< connected')
|
||||
|
||||
host = Host(hci_source, hci_sink)
|
||||
await host.reset()
|
||||
|
||||
# Measure the latency if requested
|
||||
# (we add an extra probe at the start, that we ignore, just to ensure that
|
||||
# the transport is primed)
|
||||
latencies = []
|
||||
if latency_probes:
|
||||
if latency_probe_command:
|
||||
probe_hci_command = HCI_Command.from_bytes(
|
||||
bytes.fromhex(latency_probe_command)
|
||||
)
|
||||
else:
|
||||
probe_hci_command = HCI_Read_Local_Version_Information_Command()
|
||||
|
||||
for iteration in range(1 + latency_probes):
|
||||
if latency_probe_interval:
|
||||
await asyncio.sleep(latency_probe_interval / 1000)
|
||||
for _ in range(latency_probes):
|
||||
start = time.time()
|
||||
await host.send_command(probe_hci_command)
|
||||
if iteration:
|
||||
latencies.append(1000 * (time.time() - start))
|
||||
await host.send_command(HCI_Read_Local_Version_Information_Command())
|
||||
latencies.append(1000 * (time.time() - start))
|
||||
print(
|
||||
color('HCI Command Latency:', 'yellow'),
|
||||
(
|
||||
f'min={min(latencies):.2f}, '
|
||||
f'max={max(latencies):.2f}, '
|
||||
f'average={sum(latencies) / len(latencies):.2f},'
|
||||
f'average={sum(latencies)/len(latencies):.2f}'
|
||||
),
|
||||
[f'{latency:.4}' for latency in latencies],
|
||||
'\n',
|
||||
)
|
||||
|
||||
@@ -288,20 +275,14 @@ async def async_main(
|
||||
)
|
||||
print(
|
||||
color(' HCI Version: ', 'green'),
|
||||
SpecificationVersion(host.local_version.hci_version).name,
|
||||
)
|
||||
print(
|
||||
color(' HCI Subversion:', 'green'),
|
||||
f'0x{host.local_version.hci_subversion:04x}',
|
||||
name_or_number(HCI_VERSION_NAMES, host.local_version.hci_version),
|
||||
)
|
||||
print(color(' HCI Subversion:', 'green'), host.local_version.hci_subversion)
|
||||
print(
|
||||
color(' LMP Version: ', 'green'),
|
||||
SpecificationVersion(host.local_version.lmp_version).name,
|
||||
)
|
||||
print(
|
||||
color(' LMP Subversion:', 'green'),
|
||||
f'0x{host.local_version.lmp_subversion:04x}',
|
||||
name_or_number(LMP_VERSION_NAMES, host.local_version.lmp_version),
|
||||
)
|
||||
print(color(' LMP Subversion:', 'green'), host.local_version.lmp_subversion)
|
||||
|
||||
# Get the Classic info
|
||||
await get_classic_info(host)
|
||||
@@ -330,28 +311,10 @@ async def async_main(
|
||||
type=int,
|
||||
help='Send N commands to measure HCI transport latency statistics',
|
||||
)
|
||||
@click.option(
|
||||
'--latency-probe-interval',
|
||||
metavar='INTERVAL',
|
||||
type=int,
|
||||
help='Interval between latency probes (milliseconds)',
|
||||
)
|
||||
@click.option(
|
||||
'--latency-probe-command',
|
||||
metavar='COMMAND_HEX',
|
||||
help=(
|
||||
'Probe command (HCI Command packet bytes, in hex. Use 0177FC00 for'
|
||||
' a loopback test with the HCI remote proxy app)'
|
||||
),
|
||||
)
|
||||
@click.argument('transport')
|
||||
def main(latency_probes, latency_probe_interval, latency_probe_command, transport):
|
||||
bumble.logging.setup_basic_logging()
|
||||
asyncio.run(
|
||||
async_main(
|
||||
latency_probes, latency_probe_interval, latency_probe_command, transport
|
||||
)
|
||||
)
|
||||
def main(latency_probes, transport):
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
|
||||
asyncio.run(async_main(latency_probes, transport))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -16,21 +16,21 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
|
||||
import click
|
||||
|
||||
import bumble.logging
|
||||
from typing import Optional
|
||||
from bumble.colors import color
|
||||
from bumble.hci import (
|
||||
HCI_READ_LOOPBACK_MODE_COMMAND,
|
||||
HCI_WRITE_LOOPBACK_MODE_COMMAND,
|
||||
HCI_Read_Loopback_Mode_Command,
|
||||
HCI_WRITE_LOOPBACK_MODE_COMMAND,
|
||||
HCI_Write_Loopback_Mode_Command,
|
||||
LoopbackMode,
|
||||
)
|
||||
from bumble.host import Host
|
||||
from bumble.transport import open_transport
|
||||
from bumble.transport import open_transport_or_link
|
||||
import click
|
||||
|
||||
|
||||
class Loopback:
|
||||
@@ -40,7 +40,7 @@ class Loopback:
|
||||
self.transport = transport
|
||||
self.packet_size = packet_size
|
||||
self.packet_count = packet_count
|
||||
self.connection_handle: int | None = None
|
||||
self.connection_handle: Optional[int] = None
|
||||
self.connection_event = asyncio.Event()
|
||||
self.done = asyncio.Event()
|
||||
self.expected_cid = 0
|
||||
@@ -88,7 +88,7 @@ class Loopback:
|
||||
async def run(self):
|
||||
"""Run a loopback throughput test"""
|
||||
print(color('>>> Connecting to HCI...', 'green'))
|
||||
async with await open_transport(self.transport) as (
|
||||
async with await open_transport_or_link(self.transport) as (
|
||||
hci_source,
|
||||
hci_sink,
|
||||
):
|
||||
@@ -194,7 +194,8 @@ class Loopback:
|
||||
)
|
||||
@click.argument('transport')
|
||||
def main(packet_size, packet_count, transport):
|
||||
bumble.logging.setup_basic_logging()
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
|
||||
|
||||
loopback = Loopback(packet_size, packet_count, transport)
|
||||
asyncio.run(loopback.run())
|
||||
|
||||
|
||||
@@ -15,13 +15,14 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import logging
|
||||
import asyncio
|
||||
import sys
|
||||
import os
|
||||
|
||||
import bumble.logging
|
||||
from bumble.controller import Controller
|
||||
from bumble.link import LocalLink
|
||||
from bumble.transport import open_transport
|
||||
from bumble.transport import open_transport_or_link
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -41,7 +42,7 @@ async def async_main():
|
||||
transports = []
|
||||
controllers = []
|
||||
for index, transport_name in enumerate(sys.argv[1:]):
|
||||
transport = await open_transport(transport_name)
|
||||
transport = await open_transport_or_link(transport_name)
|
||||
transports.append(transport)
|
||||
controller = Controller(
|
||||
f'C{index}',
|
||||
@@ -61,7 +62,7 @@ async def async_main():
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def main():
|
||||
bumble.logging.setup_basic_logging()
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||
asyncio.run(async_main())
|
||||
|
||||
|
||||
|
||||
@@ -16,22 +16,23 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
from collections.abc import Callable, Iterable
|
||||
import os
|
||||
import logging
|
||||
from typing import Callable, Iterable, Optional
|
||||
|
||||
import click
|
||||
|
||||
import bumble.logging
|
||||
from bumble.colors import color
|
||||
from bumble.core import ProtocolError
|
||||
from bumble.colors import color
|
||||
from bumble.device import Device, Peer
|
||||
from bumble.gatt import Service
|
||||
from bumble.profiles.battery_service import BatteryServiceProxy
|
||||
from bumble.profiles.device_information_service import DeviceInformationServiceProxy
|
||||
from bumble.profiles.battery_service import BatteryServiceProxy
|
||||
from bumble.profiles.gap import GenericAccessServiceProxy
|
||||
from bumble.profiles.pacs import PublishedAudioCapabilitiesServiceProxy
|
||||
from bumble.profiles.tmap import TelephonyAndMediaAudioServiceProxy
|
||||
from bumble.profiles.vcs import VolumeControlServiceProxy
|
||||
from bumble.transport import open_transport
|
||||
from bumble.transport import open_transport_or_link
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -174,7 +175,7 @@ async def show_vcs(vcs: VolumeControlServiceProxy) -> None:
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def show_device_info(peer, done: asyncio.Future | None) -> None:
|
||||
async def show_device_info(peer, done: Optional[asyncio.Future]) -> None:
|
||||
try:
|
||||
# Discover all services
|
||||
print(color('### Discovering Services and Characteristics', 'magenta'))
|
||||
@@ -214,7 +215,8 @@ async def show_device_info(peer, done: asyncio.Future | None) -> None:
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def async_main(device_config, encrypt, transport, address_or_name):
|
||||
async with await open_transport(transport) as (hci_source, hci_sink):
|
||||
async with await open_transport_or_link(transport) as (hci_source, hci_sink):
|
||||
|
||||
# Create a device
|
||||
if device_config:
|
||||
device = Device.from_config_file_with_hci(
|
||||
@@ -265,7 +267,7 @@ def main(device_config, encrypt, transport, address_or_name):
|
||||
Dump the GATT database on a remote device. If ADDRESS_OR_NAME is not specified,
|
||||
wait for an incoming connection.
|
||||
"""
|
||||
bumble.logging.setup_basic_logging()
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||
asyncio.run(async_main(device_config, encrypt, transport, address_or_name))
|
||||
|
||||
|
||||
|
||||
@@ -16,15 +16,15 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
|
||||
import os
|
||||
import logging
|
||||
import click
|
||||
|
||||
import bumble.core
|
||||
import bumble.logging
|
||||
from bumble.colors import color
|
||||
from bumble.device import Device, Peer
|
||||
from bumble.gatt import show_services
|
||||
from bumble.transport import open_transport
|
||||
from bumble.transport import open_transport_or_link
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -60,7 +60,8 @@ async def dump_gatt_db(peer, done):
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def async_main(device_config, encrypt, transport, address_or_name):
|
||||
async with await open_transport(transport) as (hci_source, hci_sink):
|
||||
async with await open_transport_or_link(transport) as (hci_source, hci_sink):
|
||||
|
||||
# Create a device
|
||||
if device_config:
|
||||
device = Device.from_config_file_with_hci(
|
||||
@@ -111,7 +112,7 @@ def main(device_config, encrypt, transport, address_or_name):
|
||||
Dump the GATT database on a remote device. If ADDRESS_OR_NAME is not specified,
|
||||
wait for an incoming connection.
|
||||
"""
|
||||
bumble.logging.setup_basic_logging()
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||
asyncio.run(async_main(device_config, encrypt, transport, address_or_name))
|
||||
|
||||
|
||||
|
||||
@@ -16,19 +16,20 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import os
|
||||
import struct
|
||||
|
||||
import logging
|
||||
import click
|
||||
|
||||
import bumble.logging
|
||||
from bumble import l2cap
|
||||
from bumble.colors import color
|
||||
from bumble.core import AdvertisingData
|
||||
from bumble.device import Device, Peer
|
||||
from bumble.gatt import Characteristic, CharacteristicValue, Service
|
||||
from bumble.hci import HCI_Constant
|
||||
from bumble.transport import open_transport
|
||||
from bumble.core import AdvertisingData
|
||||
from bumble.gatt import Service, Characteristic, CharacteristicValue
|
||||
from bumble.utils import AsyncRunner
|
||||
from bumble.transport import open_transport_or_link
|
||||
from bumble.hci import HCI_Constant
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
@@ -324,7 +325,7 @@ async def run(
|
||||
receive_port,
|
||||
):
|
||||
print('<<< connecting to HCI...')
|
||||
async with await open_transport(hci_transport) as (hci_source, hci_sink):
|
||||
async with await open_transport_or_link(hci_transport) as (hci_source, hci_sink):
|
||||
print('<<< connected')
|
||||
|
||||
# Instantiate a bridge object
|
||||
@@ -382,7 +383,6 @@ def main(
|
||||
receive_host,
|
||||
receive_port,
|
||||
):
|
||||
bumble.logging.setup_basic_logging('WARNING')
|
||||
asyncio.run(
|
||||
run(
|
||||
hci_transport,
|
||||
@@ -397,5 +397,6 @@ def main(
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
@@ -12,15 +12,14 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import asyncio
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import logging
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
import bumble.logging
|
||||
from bumble import hci, transport
|
||||
from bumble.bridge import HCI_Bridge
|
||||
|
||||
@@ -47,14 +46,14 @@ async def async_main():
|
||||
return
|
||||
|
||||
print('>>> connecting to HCI...')
|
||||
async with await transport.open_transport(sys.argv[1]) as (
|
||||
async with await transport.open_transport_or_link(sys.argv[1]) as (
|
||||
hci_host_source,
|
||||
hci_host_sink,
|
||||
):
|
||||
print('>>> connected')
|
||||
|
||||
print('>>> connecting to HCI...')
|
||||
async with await transport.open_transport(sys.argv[2]) as (
|
||||
async with await transport.open_transport_or_link(sys.argv[2]) as (
|
||||
hci_controller_source,
|
||||
hci_controller_sink,
|
||||
):
|
||||
@@ -101,7 +100,7 @@ async def async_main():
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def main():
|
||||
bumble.logging.setup_basic_logging()
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||
asyncio.run(async_main())
|
||||
|
||||
|
||||
|
||||
@@ -16,16 +16,16 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
|
||||
import logging
|
||||
import os
|
||||
import click
|
||||
|
||||
import bumble.logging
|
||||
from bumble import l2cap
|
||||
from bumble.colors import color
|
||||
from bumble.transport import open_transport_or_link
|
||||
from bumble.device import Device
|
||||
from bumble.hci import HCI_Constant
|
||||
from bumble.transport import open_transport
|
||||
from bumble.utils import FlowControlAsyncPipe
|
||||
from bumble.hci import HCI_Constant
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -258,7 +258,7 @@ class ClientBridge:
|
||||
# -----------------------------------------------------------------------------
|
||||
async def run(device_config, hci_transport, bridge):
|
||||
print('<<< connecting to HCI...')
|
||||
async with await open_transport(hci_transport) as (hci_source, hci_sink):
|
||||
async with await open_transport_or_link(hci_transport) as (hci_source, hci_sink):
|
||||
print('<<< connected')
|
||||
|
||||
device = Device.from_config_file_with_hci(device_config, hci_source, hci_sink)
|
||||
@@ -356,6 +356,6 @@ def client(context, bluetooth_address, tcp_host, tcp_port):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
|
||||
if __name__ == '__main__':
|
||||
bumble.logging.setup_basic_logging('WARNING')
|
||||
cli(obj={}) # pylint: disable=no-value-for-parameter
|
||||
|
||||
@@ -20,30 +20,31 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import datetime
|
||||
import functools
|
||||
from importlib import resources
|
||||
import json
|
||||
import os
|
||||
import logging
|
||||
import pathlib
|
||||
import wave
|
||||
import weakref
|
||||
from importlib import resources
|
||||
import wave
|
||||
|
||||
try:
|
||||
import lc3 # type: ignore # pylint: disable=E0401
|
||||
except ImportError as e:
|
||||
raise ImportError("Try `python -m pip install \".[lc3]\"`.") from e
|
||||
|
||||
import aiohttp.web
|
||||
import click
|
||||
import aiohttp.web
|
||||
|
||||
import bumble
|
||||
import bumble.logging
|
||||
from bumble import data_types, utils
|
||||
from bumble.colors import color
|
||||
from bumble import utils
|
||||
from bumble.core import AdvertisingData
|
||||
from bumble.device import AdvertisingParameters, CisLink, Device, DeviceConfiguration
|
||||
from bumble.hci import Address, CodecID, CodingFormat, HCI_IsoDataPacket
|
||||
from bumble.profiles import ascs, bap, pacs
|
||||
from bumble.colors import color
|
||||
from bumble.device import Device, DeviceConfiguration, AdvertisingParameters, CisLink
|
||||
from bumble.transport import open_transport
|
||||
from bumble.profiles import ascs, bap, pacs
|
||||
from bumble.hci import Address, CodecID, CodingFormat, HCI_IsoDataPacket
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -268,6 +269,7 @@ class UiServer:
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Speaker:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device_config_path: str | None,
|
||||
@@ -298,7 +300,6 @@ class Speaker:
|
||||
advertising_interval_max=25,
|
||||
address=Address('F1:F2:F3:F4:F5:F6'),
|
||||
identity_address_type=Address.RANDOM_DEVICE_ADDRESS,
|
||||
eatt_enabled=True,
|
||||
)
|
||||
|
||||
device_config.le_enabled = True
|
||||
@@ -330,13 +331,17 @@ class Speaker:
|
||||
advertising_data = bytes(
|
||||
AdvertisingData(
|
||||
[
|
||||
data_types.CompleteLocalName(device_config.name),
|
||||
data_types.Flags(
|
||||
AdvertisingData.Flags.LE_GENERAL_DISCOVERABLE_MODE
|
||||
| AdvertisingData.Flags.BR_EDR_NOT_SUPPORTED
|
||||
(
|
||||
AdvertisingData.COMPLETE_LOCAL_NAME,
|
||||
bytes(device_config.name, 'utf-8'),
|
||||
),
|
||||
data_types.IncompleteListOf16BitServiceUUIDs(
|
||||
[pacs.PublishedAudioCapabilitiesService.UUID]
|
||||
(
|
||||
AdvertisingData.FLAGS,
|
||||
bytes([AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG]),
|
||||
),
|
||||
(
|
||||
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
|
||||
bytes(pacs.PublishedAudioCapabilitiesService.UUID),
|
||||
),
|
||||
]
|
||||
)
|
||||
@@ -444,7 +449,7 @@ def speaker(ui_port: int, device_config: str, transport: str, lc3_file: str) ->
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def main():
|
||||
bumble.logging.setup_basic_logging()
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||
speaker()
|
||||
|
||||
|
||||
|
||||
0
apps/link_relay/__init__.py
Normal file
0
apps/link_relay/__init__.py
Normal file
289
apps/link_relay/link_relay.py
Normal file
289
apps/link_relay/link_relay.py
Normal file
@@ -0,0 +1,289 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# ----------------------------------------------------------------------------
|
||||
import sys
|
||||
import logging
|
||||
import json
|
||||
import asyncio
|
||||
import argparse
|
||||
import uuid
|
||||
import os
|
||||
from urllib.parse import urlparse
|
||||
import websockets
|
||||
|
||||
from bumble.colors import color
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Constants
|
||||
# ----------------------------------------------------------------------------
|
||||
DEFAULT_RELAY_PORT = 10723
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Utils
|
||||
# ----------------------------------------------------------------------------
|
||||
def error_to_json(error):
|
||||
return json.dumps({'error': error})
|
||||
|
||||
|
||||
def error_to_result(error):
|
||||
return f'result:{error_to_json(error)}'
|
||||
|
||||
|
||||
async def broadcast_message(message, connections):
|
||||
# Send to all the connections
|
||||
tasks = [connection.send_message(message) for connection in connections]
|
||||
if tasks:
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Connection class
|
||||
# ----------------------------------------------------------------------------
|
||||
class Connection:
|
||||
"""
|
||||
A Connection represents a client connected to the relay over a websocket
|
||||
"""
|
||||
|
||||
def __init__(self, room, websocket):
|
||||
self.room = room
|
||||
self.websocket = websocket
|
||||
self.address = str(uuid.uuid4())
|
||||
|
||||
async def send_message(self, message):
|
||||
try:
|
||||
logger.debug(color(f'->{self.address}: {message}', 'yellow'))
|
||||
return await self.websocket.send(message)
|
||||
except websockets.exceptions.WebSocketException as error:
|
||||
logger.info(f'! client "{self}" disconnected: {error}')
|
||||
await self.cleanup()
|
||||
|
||||
async def send_error(self, error):
|
||||
return await self.send_message(f'result:{error_to_json(error)}')
|
||||
|
||||
async def receive_message(self):
|
||||
try:
|
||||
message = await self.websocket.recv()
|
||||
logger.debug(color(f'<-{self.address}: {message}', 'blue'))
|
||||
return message
|
||||
except websockets.exceptions.WebSocketException as error:
|
||||
logger.info(color(f'! client "{self}" disconnected: {error}', 'red'))
|
||||
await self.cleanup()
|
||||
|
||||
async def cleanup(self):
|
||||
if self.room:
|
||||
await self.room.remove_connection(self)
|
||||
|
||||
def set_address(self, address):
|
||||
logger.info(f'Connection address changed: {self.address} -> {address}')
|
||||
self.address = address
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f'Connection(address="{self.address}", '
|
||||
f'client={self.websocket.remote_address[0]}:'
|
||||
f'{self.websocket.remote_address[1]})'
|
||||
)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Room class
|
||||
# ----------------------------------------------------------------------------
|
||||
class Room:
|
||||
"""
|
||||
A Room is a collection of bridged connections
|
||||
"""
|
||||
|
||||
def __init__(self, relay, name):
|
||||
self.relay = relay
|
||||
self.name = name
|
||||
self.observers = []
|
||||
self.connections = []
|
||||
|
||||
async def add_connection(self, connection):
|
||||
logger.info(f'New participant in {self.name}: {connection}')
|
||||
self.connections.append(connection)
|
||||
await self.broadcast_message(connection, f'joined:{connection.address}')
|
||||
|
||||
async def remove_connection(self, connection):
|
||||
if connection in self.connections:
|
||||
self.connections.remove(connection)
|
||||
await self.broadcast_message(connection, f'left:{connection.address}')
|
||||
|
||||
def find_connections_by_address(self, address):
|
||||
return [c for c in self.connections if c.address == address]
|
||||
|
||||
async def bridge_connection(self, connection):
|
||||
while True:
|
||||
# Wait for a message
|
||||
message = await connection.receive_message()
|
||||
|
||||
# Skip empty messages
|
||||
if message is None:
|
||||
return
|
||||
|
||||
# Parse the message to decide how to handle it
|
||||
if message.startswith('@'):
|
||||
# This is a targeted message
|
||||
await self.on_targeted_message(connection, message)
|
||||
elif message.startswith('/'):
|
||||
# This is an RPC request
|
||||
await self.on_rpc_request(connection, message)
|
||||
else:
|
||||
await connection.send_message(
|
||||
f'result:{error_to_json("error: invalid message")}'
|
||||
)
|
||||
|
||||
async def broadcast_message(self, sender, message):
|
||||
'''
|
||||
Send to all connections in the room except back to the sender
|
||||
'''
|
||||
await broadcast_message(message, [c for c in self.connections if c != sender])
|
||||
|
||||
async def on_rpc_request(self, connection, message):
|
||||
command, *params = message.split(' ', 1)
|
||||
if handler := getattr(
|
||||
self, f'on_{command[1:].lower().replace("-","_")}_command', None
|
||||
):
|
||||
try:
|
||||
result = await handler(connection, params)
|
||||
except Exception as error:
|
||||
result = error_to_result(error)
|
||||
else:
|
||||
result = error_to_result('unknown command')
|
||||
|
||||
await connection.send_message(result or 'result:{}')
|
||||
|
||||
async def on_targeted_message(self, connection, message):
|
||||
target, *payload = message.split(' ', 1)
|
||||
if not payload:
|
||||
return error_to_json('missing arguments')
|
||||
payload = payload[0]
|
||||
target = target[1:]
|
||||
|
||||
# Determine what targets to send to
|
||||
if target == '*':
|
||||
# Send to all connections in the room except the connection from which the
|
||||
# message was received
|
||||
connections = [c for c in self.connections if c != connection]
|
||||
else:
|
||||
connections = self.find_connections_by_address(target)
|
||||
if not connections:
|
||||
# Unicast with no recipient, let the sender know
|
||||
await connection.send_message(f'unreachable:{target}')
|
||||
|
||||
# Send to targets
|
||||
await broadcast_message(f'message:{connection.address}/{payload}', connections)
|
||||
|
||||
async def on_set_address_command(self, connection, params):
|
||||
if not params:
|
||||
return error_to_result('missing address')
|
||||
|
||||
current_address = connection.address
|
||||
new_address = params[0]
|
||||
connection.set_address(new_address)
|
||||
await self.broadcast_message(
|
||||
connection, f'address-changed:from={current_address},to={new_address}'
|
||||
)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
class Relay:
|
||||
"""
|
||||
A relay accepts connections with the following url: ws://<hostname>/<room>.
|
||||
Participants in a room can communicate with each other
|
||||
"""
|
||||
|
||||
def __init__(self, port):
|
||||
self.port = port
|
||||
self.rooms = {}
|
||||
self.observers = []
|
||||
|
||||
def start(self):
|
||||
logger.info(f'Starting Relay on port {self.port}')
|
||||
|
||||
# pylint: disable-next=no-member
|
||||
return websockets.serve(self.serve, '0.0.0.0', self.port, ping_interval=None)
|
||||
|
||||
async def serve_as_controller(self, connection):
|
||||
pass
|
||||
|
||||
async def serve(self, websocket, path):
|
||||
logger.debug(f'New connection with path {path}')
|
||||
|
||||
# Parse the path
|
||||
parsed = urlparse(path)
|
||||
|
||||
# Check if this is a controller client
|
||||
if parsed.path == '/':
|
||||
return await self.serve_as_controller(Connection('', websocket))
|
||||
|
||||
# Find or create a room for this connection
|
||||
room_name = parsed.path[1:].split('/')[0]
|
||||
if room_name not in self.rooms:
|
||||
self.rooms[room_name] = Room(self, room_name)
|
||||
room = self.rooms[room_name]
|
||||
|
||||
# Add the connection to the room
|
||||
connection = Connection(room, websocket)
|
||||
await room.add_connection(connection)
|
||||
|
||||
# Bridge until the connection is closed
|
||||
await room.bridge_connection(connection)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
def main():
|
||||
# Check the Python version
|
||||
if sys.version_info < (3, 6, 1):
|
||||
print('ERROR: Python 3.6.1 or higher is required')
|
||||
sys.exit(1)
|
||||
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||
|
||||
# Parse arguments
|
||||
arg_parser = argparse.ArgumentParser(description='Bumble Link Relay')
|
||||
arg_parser.add_argument('--log-level', default='INFO', help='logger level')
|
||||
arg_parser.add_argument('--log-config', help='logger config file (YAML)')
|
||||
arg_parser.add_argument(
|
||||
'--port', type=int, default=DEFAULT_RELAY_PORT, help='Port to listen on'
|
||||
)
|
||||
args = arg_parser.parse_args()
|
||||
|
||||
# Setup logger
|
||||
if args.log_config:
|
||||
from logging import config # pylint: disable=import-outside-toplevel
|
||||
|
||||
config.fileConfig(args.log_config)
|
||||
else:
|
||||
logging.basicConfig(level=getattr(logging, args.log_level.upper()))
|
||||
|
||||
# Start a relay
|
||||
relay = Relay(args.port)
|
||||
asyncio.get_event_loop().run_until_complete(relay.start())
|
||||
asyncio.get_event_loop().run_forever()
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
21
apps/link_relay/logging.yml
Normal file
21
apps/link_relay/logging.yml
Normal file
@@ -0,0 +1,21 @@
|
||||
[loggers]
|
||||
keys=root
|
||||
|
||||
[handlers]
|
||||
keys=stream_handler
|
||||
|
||||
[formatters]
|
||||
keys=formatter
|
||||
|
||||
[logger_root]
|
||||
level=DEBUG
|
||||
handlers=stream_handler
|
||||
|
||||
[handler_stream_handler]
|
||||
class=StreamHandler
|
||||
level=DEBUG
|
||||
formatter=formatter
|
||||
args=(sys.stderr,)
|
||||
|
||||
[formatter_formatter]
|
||||
format=%(asctime)s %(name)-12s %(levelname)-8s %(message)s
|
||||
192
apps/pair.py
192
apps/pair.py
@@ -15,46 +15,43 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import logging
|
||||
import struct
|
||||
|
||||
import click
|
||||
from prompt_toolkit.shortcuts import PromptSession
|
||||
|
||||
from bumble import data_types
|
||||
from bumble.a2dp import make_audio_sink_service_sdp_records
|
||||
from bumble.att import (
|
||||
ATT_INSUFFICIENT_AUTHENTICATION_ERROR,
|
||||
ATT_INSUFFICIENT_ENCRYPTION_ERROR,
|
||||
ATT_Error,
|
||||
)
|
||||
from bumble.colors import color
|
||||
from bumble.device import Device, Peer
|
||||
from bumble.transport import open_transport_or_link
|
||||
from bumble.pairing import OobData, PairingDelegate, PairingConfig
|
||||
from bumble.smp import OobContext, OobLegacyContext
|
||||
from bumble.smp import error_name as smp_error_name
|
||||
from bumble.keys import JsonKeyStore
|
||||
from bumble.core import (
|
||||
UUID,
|
||||
AdvertisingData,
|
||||
Appearance,
|
||||
DataType,
|
||||
PhysicalTransport,
|
||||
ProtocolError,
|
||||
PhysicalTransport,
|
||||
UUID,
|
||||
)
|
||||
from bumble.device import Device, Peer
|
||||
from bumble.gatt import (
|
||||
GATT_DEVICE_NAME_CHARACTERISTIC,
|
||||
GATT_GENERIC_ACCESS_SERVICE,
|
||||
GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC,
|
||||
GATT_HEART_RATE_SERVICE,
|
||||
Characteristic,
|
||||
GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC,
|
||||
Service,
|
||||
Characteristic,
|
||||
)
|
||||
from bumble.hci import OwnAddressType
|
||||
from bumble.keys import JsonKeyStore
|
||||
from bumble.pairing import OobData, PairingConfig, PairingDelegate
|
||||
from bumble.smp import OobContext, OobLegacyContext
|
||||
from bumble.smp import error_name as smp_error_name
|
||||
from bumble.transport import open_transport
|
||||
from bumble.att import (
|
||||
ATT_Error,
|
||||
ATT_INSUFFICIENT_AUTHENTICATION_ERROR,
|
||||
ATT_INSUFFICIENT_ENCRYPTION_ERROR,
|
||||
)
|
||||
from bumble.utils import AsyncRunner
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -65,7 +62,7 @@ POST_PAIRING_DELAY = 1
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Waiter:
|
||||
instance: Waiter | None = None
|
||||
instance = None
|
||||
|
||||
def __init__(self, linger=False):
|
||||
self.done = asyncio.get_running_loop().create_future()
|
||||
@@ -329,30 +326,30 @@ async def on_pairing_failure(connection, reason):
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def pair(
|
||||
mode: str,
|
||||
sc: bool,
|
||||
mitm: bool,
|
||||
bond: bool,
|
||||
ctkd: bool,
|
||||
advertising_address: str,
|
||||
identity_address: str,
|
||||
linger: bool,
|
||||
io: str,
|
||||
oob: str,
|
||||
prompt: bool,
|
||||
request: bool,
|
||||
print_keys: bool,
|
||||
keystore_file: str,
|
||||
advertise_service_uuids: str,
|
||||
advertise_appearance: str,
|
||||
device_config: str,
|
||||
hci_transport: str,
|
||||
address_or_name: str,
|
||||
mode,
|
||||
sc,
|
||||
mitm,
|
||||
bond,
|
||||
ctkd,
|
||||
advertising_address,
|
||||
identity_address,
|
||||
linger,
|
||||
io,
|
||||
oob,
|
||||
prompt,
|
||||
request,
|
||||
print_keys,
|
||||
keystore_file,
|
||||
advertise_service_uuids,
|
||||
advertise_appearance,
|
||||
device_config,
|
||||
hci_transport,
|
||||
address_or_name,
|
||||
):
|
||||
Waiter.instance = Waiter(linger=linger)
|
||||
|
||||
print('<<< connecting to HCI...')
|
||||
async with await open_transport(hci_transport) as (hci_source, hci_sink):
|
||||
async with await open_transport_or_link(hci_transport) as (hci_source, hci_sink):
|
||||
print('<<< connected')
|
||||
|
||||
# Create a device to manage the host
|
||||
@@ -405,20 +402,14 @@ async def pair(
|
||||
# Create an OOB context if needed
|
||||
if oob:
|
||||
our_oob_context = OobContext()
|
||||
legacy_context: OobLegacyContext | None
|
||||
if oob == '-':
|
||||
shared_data = None
|
||||
legacy_context = OobLegacyContext()
|
||||
else:
|
||||
oob_data = OobData.from_ad(
|
||||
shared_data = (
|
||||
None
|
||||
if oob == '-'
|
||||
else OobData.from_ad(
|
||||
AdvertisingData.from_bytes(bytes.fromhex(oob))
|
||||
)
|
||||
shared_data = oob_data.shared_data
|
||||
legacy_context = oob_data.legacy_context
|
||||
if legacy_context is None and not sc:
|
||||
print(color('OOB pairing in legacy mode requires TK', 'red'))
|
||||
return
|
||||
|
||||
).shared_data
|
||||
)
|
||||
legacy_context = OobLegacyContext()
|
||||
oob_contexts = PairingConfig.OobConfig(
|
||||
our_context=our_oob_context,
|
||||
peer_data=shared_data,
|
||||
@@ -428,9 +419,7 @@ async def pair(
|
||||
print(color('@@@ OOB Data:', 'yellow'))
|
||||
if shared_data is None:
|
||||
oob_data = OobData(
|
||||
address=device.random_address,
|
||||
shared_data=our_oob_context.share(),
|
||||
legacy_context=(None if sc else legacy_context),
|
||||
address=device.random_address, shared_data=our_oob_context.share()
|
||||
)
|
||||
print(
|
||||
color(
|
||||
@@ -438,8 +427,7 @@ async def pair(
|
||||
'yellow',
|
||||
)
|
||||
)
|
||||
if legacy_context:
|
||||
print(color(f'@@@ TK={legacy_context.tk.hex()}', 'yellow'))
|
||||
print(color(f'@@@ TK={legacy_context.tk.hex()}', 'yellow'))
|
||||
print(color('@@@-----------------------------------', 'yellow'))
|
||||
else:
|
||||
oob_contexts = None
|
||||
@@ -510,29 +498,39 @@ async def pair(
|
||||
if mode == 'dual':
|
||||
flags |= AdvertisingData.Flags.SIMULTANEOUS_LE_BR_EDR_CAPABLE
|
||||
|
||||
advertising_data_types: list[DataType] = [
|
||||
data_types.Flags(flags),
|
||||
data_types.CompleteLocalName('Bumble'),
|
||||
ad_structs = [
|
||||
(
|
||||
AdvertisingData.FLAGS,
|
||||
bytes([flags]),
|
||||
),
|
||||
(AdvertisingData.COMPLETE_LOCAL_NAME, 'Bumble'.encode()),
|
||||
]
|
||||
if service_uuids_16:
|
||||
advertising_data_types.append(
|
||||
data_types.IncompleteListOf16BitServiceUUIDs(service_uuids_16)
|
||||
ad_structs.append(
|
||||
(
|
||||
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
|
||||
b"".join(bytes(uuid) for uuid in service_uuids_16),
|
||||
)
|
||||
)
|
||||
if service_uuids_32:
|
||||
advertising_data_types.append(
|
||||
data_types.IncompleteListOf32BitServiceUUIDs(service_uuids_32)
|
||||
ad_structs.append(
|
||||
(
|
||||
AdvertisingData.INCOMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS,
|
||||
b"".join(bytes(uuid) for uuid in service_uuids_32),
|
||||
)
|
||||
)
|
||||
if service_uuids_128:
|
||||
advertising_data_types.append(
|
||||
data_types.IncompleteListOf128BitServiceUUIDs(service_uuids_128)
|
||||
ad_structs.append(
|
||||
(
|
||||
AdvertisingData.INCOMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS,
|
||||
b"".join(bytes(uuid) for uuid in service_uuids_128),
|
||||
)
|
||||
)
|
||||
|
||||
if advertise_appearance:
|
||||
advertise_appearance = advertise_appearance.upper()
|
||||
try:
|
||||
appearance = data_types.Appearance.from_int(
|
||||
int(advertise_appearance)
|
||||
)
|
||||
advertise_appearance_int = int(advertise_appearance)
|
||||
except ValueError:
|
||||
category, subcategory = advertise_appearance.split('/')
|
||||
try:
|
||||
@@ -550,12 +548,16 @@ async def pair(
|
||||
except ValueError:
|
||||
print(color(f'Invalid subcategory {subcategory}', 'red'))
|
||||
return
|
||||
appearance = data_types.Appearance(
|
||||
category_enum, subcategory_enum
|
||||
advertise_appearance_int = int(
|
||||
Appearance(category_enum, subcategory_enum)
|
||||
)
|
||||
|
||||
advertising_data_types.append(appearance)
|
||||
device.advertising_data = bytes(AdvertisingData(advertising_data_types))
|
||||
ad_structs.append(
|
||||
(
|
||||
AdvertisingData.APPEARANCE,
|
||||
struct.pack('<H', advertise_appearance_int),
|
||||
)
|
||||
)
|
||||
device.advertising_data = bytes(AdvertisingData(ad_structs))
|
||||
await device.start_advertising(
|
||||
auto_restart=True,
|
||||
own_address_type=(
|
||||
@@ -664,25 +666,25 @@ class LogHandler(logging.Handler):
|
||||
@click.argument('hci_transport')
|
||||
@click.argument('address-or-name', required=False)
|
||||
def main(
|
||||
mode: str,
|
||||
sc: bool,
|
||||
mitm: bool,
|
||||
bond: bool,
|
||||
ctkd: bool,
|
||||
advertising_address: str,
|
||||
identity_address: str,
|
||||
linger: bool,
|
||||
io: str,
|
||||
oob: str,
|
||||
prompt: bool,
|
||||
request: bool,
|
||||
print_keys: bool,
|
||||
keystore_file: str,
|
||||
advertise_service_uuid: str,
|
||||
advertise_appearance: str,
|
||||
device_config: str,
|
||||
hci_transport: str,
|
||||
address_or_name: str,
|
||||
mode,
|
||||
sc,
|
||||
mitm,
|
||||
bond,
|
||||
ctkd,
|
||||
advertising_address,
|
||||
identity_address,
|
||||
linger,
|
||||
io,
|
||||
oob,
|
||||
prompt,
|
||||
request,
|
||||
print_keys,
|
||||
keystore_file,
|
||||
advertise_service_uuid,
|
||||
advertise_appearance,
|
||||
device_config,
|
||||
hci_transport,
|
||||
address_or_name,
|
||||
):
|
||||
# Setup logging
|
||||
log_handler = LogHandler()
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import click
|
||||
import logging
|
||||
import json
|
||||
|
||||
from bumble.pandora import Config, PandoraDevice, serve
|
||||
from bumble.pandora import PandoraDevice, Config, serve
|
||||
from typing import Dict, Any
|
||||
|
||||
BUMBLE_SERVER_GRPC_PORT = 7999
|
||||
ROOTCANAL_PORT_CUTTLEFISH = 7300
|
||||
@@ -19,7 +18,7 @@ ROOTCANAL_PORT_CUTTLEFISH = 7300
|
||||
@click.option(
|
||||
'--transport',
|
||||
help='HCI transport',
|
||||
default='tcp-client:127.0.0.1:<rootcanal-port>',
|
||||
default=f'tcp-client:127.0.0.1:<rootcanal-port>',
|
||||
)
|
||||
@click.option(
|
||||
'--config',
|
||||
@@ -40,11 +39,11 @@ def main(grpc_port: int, rootcanal_port: int, transport: str, config: str) -> No
|
||||
asyncio.run(serve(device, config=server_config, port=grpc_port))
|
||||
|
||||
|
||||
def retrieve_config(config: str) -> dict[str, Any]:
|
||||
def retrieve_config(config: str) -> Dict[str, Any]:
|
||||
if not config:
|
||||
return {}
|
||||
|
||||
with open(config) as f:
|
||||
with open(config, 'r') as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
|
||||
@@ -16,49 +16,55 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import asyncio.subprocess
|
||||
import os
|
||||
import logging
|
||||
from typing import Optional, Union
|
||||
|
||||
import click
|
||||
|
||||
import bumble.logging
|
||||
from bumble.a2dp import (
|
||||
make_audio_source_service_sdp_records,
|
||||
A2DP_SBC_CODEC_TYPE,
|
||||
A2DP_MPEG_2_4_AAC_CODEC_TYPE,
|
||||
A2DP_NON_A2DP_CODEC_TYPE,
|
||||
A2DP_SBC_CODEC_TYPE,
|
||||
AacFrame,
|
||||
AacMediaCodecInformation,
|
||||
AacPacketSource,
|
||||
AacParser,
|
||||
OpusMediaCodecInformation,
|
||||
OpusPacket,
|
||||
OpusPacketSource,
|
||||
OpusParser,
|
||||
AacPacketSource,
|
||||
AacMediaCodecInformation,
|
||||
SbcFrame,
|
||||
SbcMediaCodecInformation,
|
||||
SbcPacketSource,
|
||||
SbcParser,
|
||||
make_audio_source_service_sdp_records,
|
||||
SbcPacketSource,
|
||||
SbcMediaCodecInformation,
|
||||
OpusPacket,
|
||||
OpusParser,
|
||||
OpusPacketSource,
|
||||
OpusMediaCodecInformation,
|
||||
)
|
||||
from bumble.avrcp import Protocol as AvrcpProtocol
|
||||
from bumble.avdtp import (
|
||||
find_avdtp_service_with_connection,
|
||||
AVDTP_AUDIO_MEDIA_TYPE,
|
||||
AVDTP_DELAY_REPORTING_SERVICE_CATEGORY,
|
||||
MediaCodecCapabilities,
|
||||
MediaPacketPump,
|
||||
find_avdtp_service_with_connection,
|
||||
Protocol as AvdtpProtocol,
|
||||
)
|
||||
from bumble.avdtp import Protocol as AvdtpProtocol
|
||||
from bumble.avrcp import Protocol as AvrcpProtocol
|
||||
from bumble.colors import color
|
||||
from bumble.core import AdvertisingData, DeviceClass, PhysicalTransport
|
||||
from bumble.core import ConnectionError as BumbleConnectionError
|
||||
from bumble.core import (
|
||||
AdvertisingData,
|
||||
ConnectionError as BumbleConnectionError,
|
||||
DeviceClass,
|
||||
PhysicalTransport,
|
||||
)
|
||||
from bumble.device import Connection, Device, DeviceConfiguration
|
||||
from bumble.hci import HCI_CONNECTION_ALREADY_EXISTS_ERROR, Address, HCI_Constant
|
||||
from bumble.hci import Address, HCI_CONNECTION_ALREADY_EXISTS_ERROR, HCI_Constant
|
||||
from bumble.pairing import PairingConfig
|
||||
from bumble.transport import open_transport
|
||||
from bumble.utils import AsyncRunner
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -189,7 +195,7 @@ class Player:
|
||||
def __init__(
|
||||
self,
|
||||
transport: str,
|
||||
device_config: str | None,
|
||||
device_config: Optional[str],
|
||||
authenticate: bool,
|
||||
encrypt: bool,
|
||||
) -> None:
|
||||
@@ -197,8 +203,8 @@ class Player:
|
||||
self.device_config = device_config
|
||||
self.authenticate = authenticate
|
||||
self.encrypt = encrypt
|
||||
self.avrcp_protocol: AvrcpProtocol | None = None
|
||||
self.done: asyncio.Event | None
|
||||
self.avrcp_protocol: Optional[AvrcpProtocol] = None
|
||||
self.done: Optional[asyncio.Event]
|
||||
|
||||
async def run(self, workload) -> None:
|
||||
self.done = asyncio.Event()
|
||||
@@ -313,7 +319,7 @@ class Player:
|
||||
codec_type: int,
|
||||
vendor_id: int,
|
||||
codec_id: int,
|
||||
packet_source: SbcPacketSource | AacPacketSource | OpusPacketSource,
|
||||
packet_source: Union[SbcPacketSource, AacPacketSource, OpusPacketSource],
|
||||
codec_capabilities: MediaCodecCapabilities,
|
||||
):
|
||||
# Discover all endpoints on the remote device
|
||||
@@ -379,11 +385,11 @@ class Player:
|
||||
print(f">>> {color(address.to_string(False), 'yellow')}:")
|
||||
print(f" Device Class (raw): {class_of_device:06X}")
|
||||
major_class_name = DeviceClass.major_device_class_name(major_device_class)
|
||||
print(f" Device Major Class: {major_class_name}")
|
||||
print(" Device Major Class: " f"{major_class_name}")
|
||||
minor_class_name = DeviceClass.minor_device_class_name(
|
||||
major_device_class, minor_device_class
|
||||
)
|
||||
print(f" Device Minor Class: {minor_class_name}")
|
||||
print(" Device Minor Class: " f"{minor_class_name}")
|
||||
print(
|
||||
" Device Services: "
|
||||
f"{', '.join(DeviceClass.service_class_labels(service_classes))}"
|
||||
@@ -418,7 +424,7 @@ class Player:
|
||||
async def play(
|
||||
self,
|
||||
device: Device,
|
||||
address: str | None,
|
||||
address: Optional[str],
|
||||
audio_format: str,
|
||||
audio_file: str,
|
||||
) -> None:
|
||||
@@ -447,7 +453,7 @@ class Player:
|
||||
return input_file.read(byte_count)
|
||||
|
||||
# Obtain the codec capabilities from the stream
|
||||
packet_source: SbcPacketSource | AacPacketSource | OpusPacketSource
|
||||
packet_source: Union[SbcPacketSource, AacPacketSource, OpusPacketSource]
|
||||
vendor_id = 0
|
||||
codec_id = 0
|
||||
if audio_format == "sbc":
|
||||
@@ -593,7 +599,7 @@ def play(context, address, audio_format, audio_file):
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def main():
|
||||
bumble.logging.setup_basic_logging("WARNING")
|
||||
logging.basicConfig(level=os.environ.get("BUMBLE_LOGLEVEL", "WARNING").upper())
|
||||
player_cli()
|
||||
|
||||
|
||||
|
||||
@@ -16,14 +16,21 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
import click
|
||||
|
||||
import bumble.logging
|
||||
from bumble import core, hci, rfcomm, transport, utils
|
||||
from bumble.colors import color
|
||||
from bumble.device import Connection, Device, DeviceConfiguration
|
||||
from bumble.device import Device, DeviceConfiguration, Connection
|
||||
from bumble import core
|
||||
from bumble import hci
|
||||
from bumble import rfcomm
|
||||
from bumble import transport
|
||||
from bumble import utils
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
@@ -81,14 +88,14 @@ class ServerBridge:
|
||||
def __init__(
|
||||
self, channel: int, uuid: str, trace: bool, tcp_host: str, tcp_port: int
|
||||
) -> None:
|
||||
self.device: Device | None = None
|
||||
self.device: Optional[Device] = None
|
||||
self.channel = channel
|
||||
self.uuid = uuid
|
||||
self.tcp_host = tcp_host
|
||||
self.tcp_port = tcp_port
|
||||
self.rfcomm_channel: rfcomm.DLC | None = None
|
||||
self.tcp_tracer: Tracer | None
|
||||
self.rfcomm_tracer: Tracer | None
|
||||
self.rfcomm_channel: Optional[rfcomm.DLC] = None
|
||||
self.tcp_tracer: Optional[Tracer]
|
||||
self.rfcomm_tracer: Optional[Tracer]
|
||||
|
||||
if trace:
|
||||
self.tcp_tracer = Tracer(color("RFCOMM->TCP", "cyan"))
|
||||
@@ -241,14 +248,14 @@ class ClientBridge:
|
||||
self.tcp_port = tcp_port
|
||||
self.authenticate = authenticate
|
||||
self.encrypt = encrypt
|
||||
self.device: Device | None = None
|
||||
self.connection: Connection | None = None
|
||||
self.rfcomm_client: rfcomm.Client | None
|
||||
self.rfcomm_mux: rfcomm.Multiplexer | None
|
||||
self.device: Optional[Device] = None
|
||||
self.connection: Optional[Connection] = None
|
||||
self.rfcomm_client: Optional[rfcomm.Client]
|
||||
self.rfcomm_mux: Optional[rfcomm.Multiplexer]
|
||||
self.tcp_connected: bool = False
|
||||
|
||||
self.tcp_tracer: Tracer | None
|
||||
self.rfcomm_tracer: Tracer | None
|
||||
self.tcp_tracer: Optional[Tracer]
|
||||
self.rfcomm_tracer: Optional[Tracer]
|
||||
|
||||
if trace:
|
||||
self.tcp_tracer = Tracer(color("RFCOMM->TCP", "cyan"))
|
||||
@@ -399,7 +406,7 @@ class ClientBridge:
|
||||
# -----------------------------------------------------------------------------
|
||||
async def run(device_config, hci_transport, bridge):
|
||||
print("<<< connecting to HCI...")
|
||||
async with await transport.open_transport(hci_transport) as (
|
||||
async with await transport.open_transport_or_link(hci_transport) as (
|
||||
hci_source,
|
||||
hci_sink,
|
||||
):
|
||||
@@ -508,6 +515,6 @@ def client(context, bluetooth_address, tcp_host, tcp_port, authenticate, encrypt
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
logging.basicConfig(level=os.environ.get("BUMBLE_LOGLEVEL", "WARNING").upper())
|
||||
if __name__ == "__main__":
|
||||
bumble.logging.setup_basic_logging("WARNING")
|
||||
cli(obj={}) # pylint: disable=no-value-for-parameter
|
||||
|
||||
31
apps/scan.py
31
apps/scan.py
@@ -16,17 +16,17 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
|
||||
import os
|
||||
import logging
|
||||
import click
|
||||
|
||||
import bumble.logging
|
||||
from bumble import data_types
|
||||
from bumble.colors import color
|
||||
from bumble.device import Advertisement, Device
|
||||
from bumble.hci import HCI_LE_1M_PHY, HCI_LE_CODED_PHY, Address, HCI_Constant
|
||||
from bumble.device import Device
|
||||
from bumble.transport import open_transport_or_link
|
||||
from bumble.keys import JsonKeyStore
|
||||
from bumble.smp import AddressResolver
|
||||
from bumble.transport import open_transport
|
||||
from bumble.device import Advertisement
|
||||
from bumble.hci import Address, HCI_Constant, HCI_LE_1M_PHY, HCI_LE_CODED_PHY
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -95,22 +95,13 @@ class AdvertisementPrinter:
|
||||
else:
|
||||
phy_info = ''
|
||||
|
||||
details = separator.join(
|
||||
[
|
||||
data_type.to_string(use_label=True)
|
||||
for data_type in data_types.data_types_from_advertising_data(
|
||||
advertisement.data
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
print(
|
||||
f'>>> {color(address, address_color)} '
|
||||
f'[{color(address_type_string, type_color)}]{address_qualifier}'
|
||||
f'{resolution_qualifier}:{separator}'
|
||||
f'{phy_info}'
|
||||
f'RSSI:{advertisement.rssi:4} {rssi_bar}{separator}'
|
||||
f'{details}\n'
|
||||
f'{advertisement.data.to_string(separator)}\n'
|
||||
)
|
||||
|
||||
def on_advertisement(self, advertisement):
|
||||
@@ -136,7 +127,7 @@ async def scan(
|
||||
transport,
|
||||
):
|
||||
print('<<< connecting to HCI...')
|
||||
async with await open_transport(transport) as (hci_source, hci_sink):
|
||||
async with await open_transport_or_link(transport) as (hci_source, hci_sink):
|
||||
print('<<< connected')
|
||||
|
||||
if device_config:
|
||||
@@ -217,7 +208,9 @@ async def scan(
|
||||
@click.option(
|
||||
'--irk',
|
||||
metavar='<IRK_HEX>:<ADDRESS>',
|
||||
help=('Use this IRK for resolving private addresses (may be used more than once)'),
|
||||
help=(
|
||||
'Use this IRK for resolving private addresses ' '(may be used more than once)'
|
||||
),
|
||||
multiple=True,
|
||||
)
|
||||
@click.option(
|
||||
@@ -244,7 +237,7 @@ def main(
|
||||
device_config,
|
||||
transport,
|
||||
):
|
||||
bumble.logging.setup_basic_logging('WARNING')
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
|
||||
asyncio.run(
|
||||
scan(
|
||||
min_rssi,
|
||||
|
||||
15
apps/show.py
15
apps/show.py
@@ -16,17 +16,17 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import datetime
|
||||
import importlib
|
||||
import logging
|
||||
import os
|
||||
import struct
|
||||
|
||||
import click
|
||||
|
||||
import bumble.logging
|
||||
from bumble import hci
|
||||
from bumble.colors import color
|
||||
from bumble.helpers import PacketTracer
|
||||
from bumble import hci
|
||||
from bumble.transport.common import PacketReader
|
||||
from bumble.helpers import PacketTracer
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -154,10 +154,9 @@ class Printer:
|
||||
def main(format, vendor, filename):
|
||||
for vendor_name in vendor:
|
||||
if vendor_name == 'android':
|
||||
# Prevent being deleted by linter.
|
||||
importlib.import_module('bumble.vendor.android.hci')
|
||||
import bumble.vendor.android.hci
|
||||
elif vendor_name == 'zephyr':
|
||||
importlib.import_module('bumble.vendor.zephyr.hci')
|
||||
import bumble.vendor.zephyr.hci
|
||||
|
||||
input = open(filename, 'rb')
|
||||
if format == 'h4':
|
||||
@@ -187,5 +186,5 @@ def main(format, vendor, filename):
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
if __name__ == '__main__':
|
||||
bumble.logging.setup_basic_logging('WARNING')
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
|
||||
main() # pylint: disable=no-value-for-parameter
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
<tr><td>Codec</td><td><span id="codecText"></span></td></tr>
|
||||
<tr><td>Packets</td><td><span id="packetsReceivedText"></span></td></tr>
|
||||
<tr><td>Bytes</td><td><span id="bytesReceivedText"></span></td></tr>
|
||||
<tr><td>Bitrate</td><td><span id="bitrate"></span></td></tr>
|
||||
</table>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -7,19 +7,17 @@ let connectionText;
|
||||
let codecText;
|
||||
let packetsReceivedText;
|
||||
let bytesReceivedText;
|
||||
let bitrateText;
|
||||
let streamStateText;
|
||||
let connectionStateText;
|
||||
let controlsDiv;
|
||||
let audioOnButton;
|
||||
let audioDecoder;
|
||||
let audioCodec;
|
||||
let mediaSource;
|
||||
let sourceBuffer;
|
||||
let audioElement;
|
||||
let audioContext;
|
||||
let audioAnalyzer;
|
||||
let audioFrequencyBinCount;
|
||||
let audioFrequencyData;
|
||||
let nextAudioStartPosition = 0;
|
||||
let audioStartTime = 0;
|
||||
let packetsReceived = 0;
|
||||
let bytesReceived = 0;
|
||||
let audioState = "stopped";
|
||||
@@ -31,17 +29,20 @@ let bandwidthCanvas;
|
||||
let bandwidthCanvasContext;
|
||||
let bandwidthBinCount;
|
||||
let bandwidthBins = [];
|
||||
let bitrateSamples = [];
|
||||
|
||||
const FFT_WIDTH = 800;
|
||||
const FFT_HEIGHT = 256;
|
||||
const BANDWIDTH_WIDTH = 500;
|
||||
const BANDWIDTH_HEIGHT = 100;
|
||||
const BITRATE_WINDOW = 30;
|
||||
|
||||
function hexToBytes(hex) {
|
||||
return Uint8Array.from(hex.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)));
|
||||
}
|
||||
|
||||
function init() {
|
||||
initUI();
|
||||
initAudioContext();
|
||||
initMediaSource();
|
||||
initAudioElement();
|
||||
initAnalyzer();
|
||||
|
||||
connect();
|
||||
@@ -55,7 +56,6 @@ function initUI() {
|
||||
codecText = document.getElementById("codecText");
|
||||
packetsReceivedText = document.getElementById("packetsReceivedText");
|
||||
bytesReceivedText = document.getElementById("bytesReceivedText");
|
||||
bitrateText = document.getElementById("bitrate");
|
||||
streamStateText = document.getElementById("streamStateText");
|
||||
connectionStateText = document.getElementById("connectionStateText");
|
||||
audioSupportMessageText = document.getElementById("audioSupportMessageText");
|
||||
@@ -67,9 +67,17 @@ function initUI() {
|
||||
requestAnimationFrame(onAnimationFrame);
|
||||
}
|
||||
|
||||
function initAudioContext() {
|
||||
audioContext = new AudioContext();
|
||||
audioContext.onstatechange = () => console.log("AudioContext state:", audioContext.state);
|
||||
function initMediaSource() {
|
||||
mediaSource = new MediaSource();
|
||||
mediaSource.onsourceopen = onMediaSourceOpen;
|
||||
mediaSource.onsourceclose = onMediaSourceClose;
|
||||
mediaSource.onsourceended = onMediaSourceEnd;
|
||||
}
|
||||
|
||||
function initAudioElement() {
|
||||
audioElement = document.getElementById("audio");
|
||||
audioElement.src = URL.createObjectURL(mediaSource);
|
||||
// audioElement.controls = true;
|
||||
}
|
||||
|
||||
function initAnalyzer() {
|
||||
@@ -86,16 +94,24 @@ function initAnalyzer() {
|
||||
bandwidthCanvasContext = bandwidthCanvas.getContext('2d');
|
||||
bandwidthCanvasContext.fillStyle = "rgb(255, 255, 255)";
|
||||
bandwidthCanvasContext.fillRect(0, 0, BANDWIDTH_WIDTH, BANDWIDTH_HEIGHT);
|
||||
}
|
||||
|
||||
function startAnalyzer() {
|
||||
// FFT
|
||||
if (audioElement.captureStream !== undefined) {
|
||||
audioContext = new AudioContext();
|
||||
audioAnalyzer = audioContext.createAnalyser();
|
||||
audioAnalyzer.fftSize = 128;
|
||||
audioFrequencyBinCount = audioAnalyzer.frequencyBinCount;
|
||||
audioFrequencyData = new Uint8Array(audioFrequencyBinCount);
|
||||
const stream = audioElement.captureStream();
|
||||
const source = audioContext.createMediaStreamSource(stream);
|
||||
source.connect(audioAnalyzer);
|
||||
}
|
||||
|
||||
// Bandwidth
|
||||
bandwidthBinCount = BANDWIDTH_WIDTH / 2;
|
||||
bandwidthBins = [];
|
||||
bitrateSamples = [];
|
||||
|
||||
audioAnalyzer = audioContext.createAnalyser();
|
||||
audioAnalyzer.fftSize = 128;
|
||||
audioFrequencyBinCount = audioAnalyzer.frequencyBinCount;
|
||||
audioFrequencyData = new Uint8Array(audioFrequencyBinCount);
|
||||
|
||||
audioAnalyzer.connect(audioContext.destination)
|
||||
}
|
||||
|
||||
function setConnectionText(message) {
|
||||
@@ -132,8 +148,7 @@ function onAnimationFrame() {
|
||||
bandwidthCanvasContext.fillRect(0, 0, BANDWIDTH_WIDTH, BANDWIDTH_HEIGHT);
|
||||
bandwidthCanvasContext.fillStyle = `rgb(100, 100, 100)`;
|
||||
for (let t = 0; t < bandwidthBins.length; t++) {
|
||||
const bytesReceived = bandwidthBins[t]
|
||||
const lineHeight = (bytesReceived / 1000) * BANDWIDTH_HEIGHT;
|
||||
const lineHeight = (bandwidthBins[t] / 1000) * BANDWIDTH_HEIGHT;
|
||||
bandwidthCanvasContext.fillRect(t * 2, BANDWIDTH_HEIGHT - lineHeight, 2, lineHeight);
|
||||
}
|
||||
|
||||
@@ -141,14 +156,28 @@ function onAnimationFrame() {
|
||||
requestAnimationFrame(onAnimationFrame);
|
||||
}
|
||||
|
||||
function onMediaSourceOpen() {
|
||||
console.log(this.readyState);
|
||||
sourceBuffer = mediaSource.addSourceBuffer("audio/aac");
|
||||
}
|
||||
|
||||
function onMediaSourceClose() {
|
||||
console.log(this.readyState);
|
||||
}
|
||||
|
||||
function onMediaSourceEnd() {
|
||||
console.log(this.readyState);
|
||||
}
|
||||
|
||||
async function startAudio() {
|
||||
try {
|
||||
console.log("starting audio...");
|
||||
audioOnButton.disabled = true;
|
||||
audioState = "starting";
|
||||
audioContext.resume();
|
||||
await audioElement.play();
|
||||
console.log("audio started");
|
||||
audioState = "playing";
|
||||
startAnalyzer();
|
||||
} catch(error) {
|
||||
console.error(`play failed: ${error}`);
|
||||
audioState = "stopped";
|
||||
@@ -156,47 +185,12 @@ async function startAudio() {
|
||||
}
|
||||
}
|
||||
|
||||
function onDecodedAudio(audioData) {
|
||||
const bufferSource = audioContext.createBufferSource()
|
||||
|
||||
const now = audioContext.currentTime;
|
||||
let nextAudioStartTime = audioStartTime + (nextAudioStartPosition / audioData.sampleRate);
|
||||
if (nextAudioStartTime < now) {
|
||||
console.log("starting new audio time base")
|
||||
audioStartTime = now;
|
||||
nextAudioStartTime = now;
|
||||
nextAudioStartPosition = 0;
|
||||
} else {
|
||||
console.log(`audio buffer scheduled in ${nextAudioStartTime - now}`)
|
||||
function onAudioPacket(packet) {
|
||||
if (audioState != "stopped") {
|
||||
// Queue the audio packet.
|
||||
sourceBuffer.appendBuffer(packet);
|
||||
}
|
||||
|
||||
const audioBuffer = audioContext.createBuffer(
|
||||
audioData.numberOfChannels,
|
||||
audioData.numberOfFrames,
|
||||
audioData.sampleRate
|
||||
);
|
||||
|
||||
for (let channel = 0; channel < audioData.numberOfChannels; channel++) {
|
||||
audioData.copyTo(
|
||||
audioBuffer.getChannelData(channel),
|
||||
{
|
||||
planeIndex: channel,
|
||||
format: "f32-planar"
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
bufferSource.buffer = audioBuffer;
|
||||
bufferSource.connect(audioAnalyzer)
|
||||
bufferSource.start(nextAudioStartTime);
|
||||
nextAudioStartPosition += audioData.numberOfFrames;
|
||||
}
|
||||
|
||||
function onCodecError(error) {
|
||||
console.log("Codec error:", error)
|
||||
}
|
||||
|
||||
async function onAudioPacket(packet) {
|
||||
packetsReceived += 1;
|
||||
packetsReceivedText.innerText = packetsReceived;
|
||||
bytesReceived += packet.byteLength;
|
||||
@@ -206,48 +200,6 @@ async function onAudioPacket(packet) {
|
||||
if (bandwidthBins.length > bandwidthBinCount) {
|
||||
bandwidthBins.shift();
|
||||
}
|
||||
bitrateSamples[bitrateSamples.length] = {ts: Date.now(), bytes: packet.byteLength}
|
||||
if (bitrateSamples.length > BITRATE_WINDOW) {
|
||||
bitrateSamples.shift();
|
||||
}
|
||||
if (bitrateSamples.length >= 2) {
|
||||
const windowBytes = bitrateSamples.reduce((accumulator, x) => accumulator + x.bytes, 0) - bitrateSamples[0].bytes;
|
||||
const elapsed = bitrateSamples[bitrateSamples.length-1].ts - bitrateSamples[0].ts;
|
||||
const bitrate = Math.floor(8 * windowBytes / elapsed)
|
||||
bitrateText.innerText = `${bitrate} kb/s`
|
||||
}
|
||||
|
||||
if (audioState == "stopped") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (audioDecoder === undefined) {
|
||||
let audioConfig;
|
||||
if (audioCodec == 'aac') {
|
||||
audioConfig = {
|
||||
codec: 'mp4a.40.2',
|
||||
sampleRate: 44100, // ignored
|
||||
numberOfChannels: 2, // ignored
|
||||
}
|
||||
} else if (audioCodec == 'opus') {
|
||||
audioConfig = {
|
||||
codec: 'opus',
|
||||
sampleRate: 48000, // ignored
|
||||
numberOfChannels: 2, // ignored
|
||||
}
|
||||
}
|
||||
audioDecoder = new AudioDecoder({ output: onDecodedAudio, error: onCodecError });
|
||||
audioDecoder.configure(audioConfig)
|
||||
}
|
||||
|
||||
const encodedAudio = new EncodedAudioChunk({
|
||||
type: "key",
|
||||
data: packet,
|
||||
timestamp: 0,
|
||||
transfer: [packet],
|
||||
});
|
||||
|
||||
audioDecoder.decode(encodedAudio);
|
||||
}
|
||||
|
||||
function onChannelOpen() {
|
||||
@@ -297,19 +249,16 @@ function onChannelMessage(message) {
|
||||
}
|
||||
}
|
||||
|
||||
async function onHelloMessage(params) {
|
||||
function onHelloMessage(params) {
|
||||
codecText.innerText = params.codec;
|
||||
|
||||
if (params.codec == "aac" || params.codec == "opus") {
|
||||
audioCodec = params.codec
|
||||
if (params.codec != "aac") {
|
||||
audioOnButton.disabled = true;
|
||||
audioSupportMessageText.innerText = "Only AAC can be played, audio will be disabled";
|
||||
audioSupportMessageText.style.display = "inline-block";
|
||||
} else {
|
||||
audioSupportMessageText.innerText = "";
|
||||
audioSupportMessageText.style.display = "none";
|
||||
} else {
|
||||
audioOnButton.disabled = true;
|
||||
audioSupportMessageText.innerText = "Only AAC and Opus can be played, audio will be disabled";
|
||||
audioSupportMessageText.style.display = "inline-block";
|
||||
}
|
||||
|
||||
if (params.streamState) {
|
||||
setStreamState(params.streamState);
|
||||
}
|
||||
|
||||
@@ -16,48 +16,47 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import asyncio.subprocess
|
||||
from importlib import resources
|
||||
import enum
|
||||
import json
|
||||
import os
|
||||
import logging
|
||||
import pathlib
|
||||
import subprocess
|
||||
from typing import Dict, List, Optional
|
||||
import weakref
|
||||
from importlib import resources
|
||||
|
||||
import aiohttp
|
||||
import click
|
||||
import aiohttp
|
||||
from aiohttp import web
|
||||
|
||||
import bumble
|
||||
import bumble.logging
|
||||
from bumble.a2dp import (
|
||||
A2DP_MPEG_2_4_AAC_CODEC_TYPE,
|
||||
A2DP_NON_A2DP_CODEC_TYPE,
|
||||
A2DP_SBC_CODEC_TYPE,
|
||||
AacMediaCodecInformation,
|
||||
OpusMediaCodecInformation,
|
||||
SbcMediaCodecInformation,
|
||||
make_audio_sink_service_sdp_records,
|
||||
)
|
||||
from bumble.colors import color
|
||||
from bumble.core import PhysicalTransport, CommandTimeoutError
|
||||
from bumble.device import Connection, Device, DeviceConfiguration
|
||||
from bumble.hci import HCI_StatusError
|
||||
from bumble.pairing import PairingConfig
|
||||
from bumble.sdp import ServiceAttribute
|
||||
from bumble.transport import open_transport
|
||||
from bumble.avdtp import (
|
||||
AVDTP_AUDIO_MEDIA_TYPE,
|
||||
Listener,
|
||||
MediaCodecCapabilities,
|
||||
Protocol,
|
||||
)
|
||||
from bumble.codecs import AacAudioRtpPacket
|
||||
from bumble.colors import color
|
||||
from bumble.core import CommandTimeoutError, PhysicalTransport
|
||||
from bumble.device import Connection, Device, DeviceConfiguration
|
||||
from bumble.hci import HCI_StatusError
|
||||
from bumble.pairing import PairingConfig
|
||||
from bumble.rtp import MediaPacket
|
||||
from bumble.sdp import ServiceAttribute
|
||||
from bumble.transport import open_transport
|
||||
from bumble.a2dp import (
|
||||
make_audio_sink_service_sdp_records,
|
||||
A2DP_SBC_CODEC_TYPE,
|
||||
A2DP_MPEG_2_4_AAC_CODEC_TYPE,
|
||||
SbcMediaCodecInformation,
|
||||
AacMediaCodecInformation,
|
||||
)
|
||||
from bumble.utils import AsyncRunner
|
||||
from bumble.codecs import AacAudioRtpPacket
|
||||
from bumble.rtp import MediaPacket
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -79,8 +78,6 @@ class AudioExtractor:
|
||||
return AacAudioExtractor()
|
||||
if codec == 'sbc':
|
||||
return SbcAudioExtractor()
|
||||
if codec == 'opus':
|
||||
return OpusAudioExtractor()
|
||||
|
||||
def extract_audio(self, packet: MediaPacket) -> bytes:
|
||||
raise NotImplementedError()
|
||||
@@ -105,13 +102,6 @@ class SbcAudioExtractor:
|
||||
return packet.payload[1:]
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class OpusAudioExtractor:
|
||||
def extract_audio(self, packet: MediaPacket) -> bytes:
|
||||
# TODO: parse fields
|
||||
return packet.payload[1:]
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Output:
|
||||
async def start(self) -> None:
|
||||
@@ -155,7 +145,7 @@ class QueuedOutput(Output):
|
||||
|
||||
packets: asyncio.Queue
|
||||
extractor: AudioExtractor
|
||||
packet_pump_task: asyncio.Task | None
|
||||
packet_pump_task: Optional[asyncio.Task]
|
||||
started: bool
|
||||
|
||||
def __init__(self, extractor):
|
||||
@@ -229,8 +219,8 @@ class WebSocketOutput(QueuedOutput):
|
||||
class FfplayOutput(QueuedOutput):
|
||||
MAX_QUEUE_SIZE = 32768
|
||||
|
||||
subprocess: asyncio.subprocess.Process | None
|
||||
ffplay_task: asyncio.Task | None
|
||||
subprocess: Optional[asyncio.subprocess.Process]
|
||||
ffplay_task: Optional[asyncio.Task]
|
||||
|
||||
def __init__(self, codec: str) -> None:
|
||||
super().__init__(AudioExtractor.create(codec))
|
||||
@@ -245,7 +235,7 @@ class FfplayOutput(QueuedOutput):
|
||||
await super().start()
|
||||
|
||||
self.subprocess = await asyncio.create_subprocess_shell(
|
||||
f'ffplay -probesize 32 -f {self.codec} pipe:0',
|
||||
f'ffplay -f {self.codec} pipe:0',
|
||||
stdin=asyncio.subprocess.PIPE,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
@@ -409,24 +399,10 @@ class Speaker:
|
||||
STARTED = 2
|
||||
SUSPENDED = 3
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device_config,
|
||||
transport,
|
||||
codec,
|
||||
sampling_frequencies,
|
||||
bitrate,
|
||||
vbr,
|
||||
discover,
|
||||
outputs,
|
||||
ui_port,
|
||||
):
|
||||
def __init__(self, device_config, transport, codec, discover, outputs, ui_port):
|
||||
self.device_config = device_config
|
||||
self.transport = transport
|
||||
self.codec = codec
|
||||
self.sampling_frequencies = sampling_frequencies
|
||||
self.bitrate = bitrate
|
||||
self.vbr = vbr
|
||||
self.discover = discover
|
||||
self.ui_port = ui_port
|
||||
self.device = None
|
||||
@@ -447,7 +423,7 @@ class Speaker:
|
||||
# Create an HTTP server for the UI
|
||||
self.ui_server = UiServer(speaker=self, port=ui_port)
|
||||
|
||||
def sdp_records(self) -> dict[int, list[ServiceAttribute]]:
|
||||
def sdp_records(self) -> Dict[int, List[ServiceAttribute]]:
|
||||
service_record_handle = 0x00010001
|
||||
return {
|
||||
service_record_handle: make_audio_sink_service_sdp_records(
|
||||
@@ -462,56 +438,32 @@ class Speaker:
|
||||
if self.codec == 'sbc':
|
||||
return self.sbc_codec_capabilities()
|
||||
|
||||
if self.codec == 'opus':
|
||||
return self.opus_codec_capabilities()
|
||||
|
||||
raise RuntimeError('unsupported codec')
|
||||
|
||||
def aac_codec_capabilities(self) -> MediaCodecCapabilities:
|
||||
supported_sampling_frequencies = AacMediaCodecInformation.SamplingFrequency(0)
|
||||
for sampling_frequency in self.sampling_frequencies or [
|
||||
8000,
|
||||
11025,
|
||||
12000,
|
||||
16000,
|
||||
22050,
|
||||
24000,
|
||||
32000,
|
||||
44100,
|
||||
48000,
|
||||
]:
|
||||
supported_sampling_frequencies |= (
|
||||
AacMediaCodecInformation.SamplingFrequency.from_int(sampling_frequency)
|
||||
)
|
||||
return MediaCodecCapabilities(
|
||||
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
||||
media_codec_type=A2DP_MPEG_2_4_AAC_CODEC_TYPE,
|
||||
media_codec_information=AacMediaCodecInformation(
|
||||
object_type=AacMediaCodecInformation.ObjectType.MPEG_2_AAC_LC,
|
||||
sampling_frequency=supported_sampling_frequencies,
|
||||
sampling_frequency=AacMediaCodecInformation.SamplingFrequency.SF_48000
|
||||
| AacMediaCodecInformation.SamplingFrequency.SF_44100,
|
||||
channels=AacMediaCodecInformation.Channels.MONO
|
||||
| AacMediaCodecInformation.Channels.STEREO,
|
||||
vbr=1 if self.vbr else 0,
|
||||
bitrate=self.bitrate or 256000,
|
||||
vbr=1,
|
||||
bitrate=256000,
|
||||
),
|
||||
)
|
||||
|
||||
def sbc_codec_capabilities(self) -> MediaCodecCapabilities:
|
||||
supported_sampling_frequencies = SbcMediaCodecInformation.SamplingFrequency(0)
|
||||
for sampling_frequency in self.sampling_frequencies or [
|
||||
16000,
|
||||
32000,
|
||||
44100,
|
||||
48000,
|
||||
]:
|
||||
supported_sampling_frequencies |= (
|
||||
SbcMediaCodecInformation.SamplingFrequency.from_int(sampling_frequency)
|
||||
)
|
||||
return MediaCodecCapabilities(
|
||||
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
||||
media_codec_type=A2DP_SBC_CODEC_TYPE,
|
||||
media_codec_information=SbcMediaCodecInformation(
|
||||
sampling_frequency=supported_sampling_frequencies,
|
||||
sampling_frequency=SbcMediaCodecInformation.SamplingFrequency.SF_48000
|
||||
| SbcMediaCodecInformation.SamplingFrequency.SF_44100
|
||||
| SbcMediaCodecInformation.SamplingFrequency.SF_32000
|
||||
| SbcMediaCodecInformation.SamplingFrequency.SF_16000,
|
||||
channel_mode=SbcMediaCodecInformation.ChannelMode.MONO
|
||||
| SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL
|
||||
| SbcMediaCodecInformation.ChannelMode.STEREO
|
||||
@@ -529,25 +481,6 @@ class Speaker:
|
||||
),
|
||||
)
|
||||
|
||||
def opus_codec_capabilities(self) -> MediaCodecCapabilities:
|
||||
supported_sampling_frequencies = OpusMediaCodecInformation.SamplingFrequency(0)
|
||||
for sampling_frequency in self.sampling_frequencies or [48000]:
|
||||
supported_sampling_frequencies |= (
|
||||
OpusMediaCodecInformation.SamplingFrequency.from_int(sampling_frequency)
|
||||
)
|
||||
return MediaCodecCapabilities(
|
||||
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
||||
media_codec_type=A2DP_NON_A2DP_CODEC_TYPE,
|
||||
media_codec_information=OpusMediaCodecInformation(
|
||||
frame_size=OpusMediaCodecInformation.FrameSize.FS_10MS
|
||||
| OpusMediaCodecInformation.FrameSize.FS_20MS,
|
||||
channel_mode=OpusMediaCodecInformation.ChannelMode.MONO
|
||||
| OpusMediaCodecInformation.ChannelMode.STEREO
|
||||
| OpusMediaCodecInformation.ChannelMode.DUAL_MONO,
|
||||
sampling_frequency=supported_sampling_frequencies,
|
||||
),
|
||||
)
|
||||
|
||||
async def dispatch_to_outputs(self, function):
|
||||
for output in self.outputs:
|
||||
await function(output)
|
||||
@@ -742,26 +675,7 @@ def speaker_cli(ctx, device_config):
|
||||
|
||||
@click.command()
|
||||
@click.option(
|
||||
'--codec',
|
||||
type=click.Choice(['sbc', 'aac', 'opus']),
|
||||
default='aac',
|
||||
show_default=True,
|
||||
)
|
||||
@click.option(
|
||||
'--sampling-frequency',
|
||||
metavar='SAMPLING-FREQUENCY',
|
||||
type=int,
|
||||
multiple=True,
|
||||
help='Enable a sampling frequency (may be specified more than once)',
|
||||
)
|
||||
@click.option(
|
||||
'--bitrate',
|
||||
metavar='BITRATE',
|
||||
type=int,
|
||||
help='Supported bitrate (AAC only)',
|
||||
)
|
||||
@click.option(
|
||||
'--vbr/--no-vbr', is_flag=True, default=True, help='Enable VBR (AAC only)'
|
||||
'--codec', type=click.Choice(['sbc', 'aac']), default='aac', show_default=True
|
||||
)
|
||||
@click.option(
|
||||
'--discover', is_flag=True, help='Discover remote endpoints once connected'
|
||||
@@ -792,16 +706,7 @@ def speaker_cli(ctx, device_config):
|
||||
@click.option('--device-config', metavar='FILENAME', help='Device configuration file')
|
||||
@click.argument('transport')
|
||||
def speaker(
|
||||
transport,
|
||||
codec,
|
||||
sampling_frequency,
|
||||
bitrate,
|
||||
vbr,
|
||||
connect_address,
|
||||
discover,
|
||||
output,
|
||||
ui_port,
|
||||
device_config,
|
||||
transport, codec, connect_address, discover, output, ui_port, device_config
|
||||
):
|
||||
"""Run the speaker."""
|
||||
|
||||
@@ -816,23 +721,15 @@ def speaker(
|
||||
output = list(filter(lambda x: x != '@ffplay', output))
|
||||
|
||||
asyncio.run(
|
||||
Speaker(
|
||||
device_config,
|
||||
transport,
|
||||
codec,
|
||||
sampling_frequency,
|
||||
bitrate,
|
||||
vbr,
|
||||
discover,
|
||||
output,
|
||||
ui_port,
|
||||
).run(connect_address)
|
||||
Speaker(device_config, transport, codec, discover, output, ui_port).run(
|
||||
connect_address
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def main():
|
||||
bumble.logging.setup_basic_logging('WARNING')
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
|
||||
speaker()
|
||||
|
||||
|
||||
|
||||
@@ -16,10 +16,10 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
|
||||
import os
|
||||
import logging
|
||||
import click
|
||||
|
||||
import bumble.logging
|
||||
from bumble.device import Device
|
||||
from bumble.keys import JsonKeyStore
|
||||
from bumble.transport import open_transport
|
||||
@@ -68,7 +68,7 @@ def main(keystore_file, hci_transport, device_config, address):
|
||||
instantiated.
|
||||
If no address is passed, the existing pairing keys for all addresses are printed.
|
||||
"""
|
||||
bumble.logging.setup_basic_logging()
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||
|
||||
if not keystore_file and not hci_transport:
|
||||
print('either --keystore-file or --hci-transport must be specified.')
|
||||
|
||||
@@ -26,13 +26,15 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import os
|
||||
import logging
|
||||
import click
|
||||
import usb1
|
||||
|
||||
import bumble.logging
|
||||
from bumble.colors import color
|
||||
from bumble.transport.usb import load_libusb
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -167,7 +169,7 @@ def is_bluetooth_hci(device):
|
||||
@click.command()
|
||||
@click.option('--verbose', is_flag=True, default=False, help='Print more details')
|
||||
def main(verbose):
|
||||
bumble.logging.setup_basic_logging('WARNING')
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
|
||||
|
||||
load_libusb()
|
||||
with usb1.USBContext() as context:
|
||||
|
||||
114
bumble/a2dp.py
114
bumble/a2dp.py
@@ -17,37 +17,37 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import AsyncGenerator
|
||||
import dataclasses
|
||||
import enum
|
||||
import logging
|
||||
import struct
|
||||
from collections.abc import AsyncGenerator, Awaitable, Callable
|
||||
from typing import ClassVar
|
||||
from typing import Awaitable, Callable
|
||||
from typing_extensions import ClassVar, Self
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from bumble import utils
|
||||
from bumble.codecs import AacAudioRtpPacket
|
||||
from bumble.company_ids import COMPANY_IDENTIFIERS
|
||||
from bumble.sdp import (
|
||||
DataElement,
|
||||
ServiceAttribute,
|
||||
SDP_PUBLIC_BROWSE_ROOT,
|
||||
SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
|
||||
SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
|
||||
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
|
||||
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
)
|
||||
from bumble.core import (
|
||||
BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE,
|
||||
BT_AUDIO_SINK_SERVICE,
|
||||
BT_AUDIO_SOURCE_SERVICE,
|
||||
BT_AVDTP_PROTOCOL_ID,
|
||||
BT_L2CAP_PROTOCOL_ID,
|
||||
BT_AUDIO_SOURCE_SERVICE,
|
||||
BT_AUDIO_SINK_SERVICE,
|
||||
BT_AVDTP_PROTOCOL_ID,
|
||||
BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE,
|
||||
name_or_number,
|
||||
)
|
||||
from bumble.rtp import MediaPacket
|
||||
from bumble.sdp import (
|
||||
SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
|
||||
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
SDP_PUBLIC_BROWSE_ROOT,
|
||||
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
|
||||
SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
|
||||
DataElement,
|
||||
ServiceAttribute,
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -60,18 +60,19 @@ logger = logging.getLogger(__name__)
|
||||
# -----------------------------------------------------------------------------
|
||||
# fmt: off
|
||||
|
||||
class CodecType(utils.OpenIntEnum):
|
||||
SBC = 0x00
|
||||
MPEG_1_2_AUDIO = 0x01
|
||||
MPEG_2_4_AAC = 0x02
|
||||
ATRAC_FAMILY = 0x03
|
||||
NON_A2DP = 0xFF
|
||||
A2DP_SBC_CODEC_TYPE = 0x00
|
||||
A2DP_MPEG_1_2_AUDIO_CODEC_TYPE = 0x01
|
||||
A2DP_MPEG_2_4_AAC_CODEC_TYPE = 0x02
|
||||
A2DP_ATRAC_FAMILY_CODEC_TYPE = 0x03
|
||||
A2DP_NON_A2DP_CODEC_TYPE = 0xFF
|
||||
|
||||
A2DP_SBC_CODEC_TYPE = CodecType.SBC
|
||||
A2DP_MPEG_1_2_AUDIO_CODEC_TYPE = CodecType.MPEG_1_2_AUDIO
|
||||
A2DP_MPEG_2_4_AAC_CODEC_TYPE = CodecType.MPEG_2_4_AAC
|
||||
A2DP_ATRAC_FAMILY_CODEC_TYPE = CodecType.ATRAC_FAMILY
|
||||
A2DP_NON_A2DP_CODEC_TYPE = CodecType.NON_A2DP
|
||||
A2DP_CODEC_TYPE_NAMES = {
|
||||
A2DP_SBC_CODEC_TYPE: 'A2DP_SBC_CODEC_TYPE',
|
||||
A2DP_MPEG_1_2_AUDIO_CODEC_TYPE: 'A2DP_MPEG_1_2_AUDIO_CODEC_TYPE',
|
||||
A2DP_MPEG_2_4_AAC_CODEC_TYPE: 'A2DP_MPEG_2_4_AAC_CODEC_TYPE',
|
||||
A2DP_ATRAC_FAMILY_CODEC_TYPE: 'A2DP_ATRAC_FAMILY_CODEC_TYPE',
|
||||
A2DP_NON_A2DP_CODEC_TYPE: 'A2DP_NON_A2DP_CODEC_TYPE'
|
||||
}
|
||||
|
||||
|
||||
SBC_SYNC_WORD = 0x9C
|
||||
@@ -259,48 +260,9 @@ def make_audio_sink_service_sdp_records(service_record_handle, version=(1, 3)):
|
||||
]
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class MediaCodecInformation:
|
||||
'''Base Media Codec Information.'''
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls, media_codec_type: int, data: bytes
|
||||
) -> MediaCodecInformation | bytes:
|
||||
if media_codec_type == CodecType.SBC:
|
||||
return SbcMediaCodecInformation.from_bytes(data)
|
||||
elif media_codec_type == CodecType.MPEG_2_4_AAC:
|
||||
return AacMediaCodecInformation.from_bytes(data)
|
||||
elif media_codec_type == CodecType.NON_A2DP:
|
||||
vendor_media_codec_information = (
|
||||
VendorSpecificMediaCodecInformation.from_bytes(data)
|
||||
)
|
||||
if (
|
||||
vendor_class_map := A2DP_VENDOR_MEDIA_CODEC_INFORMATION_CLASSES.get(
|
||||
vendor_media_codec_information.vendor_id
|
||||
)
|
||||
) and (
|
||||
media_codec_information_class := vendor_class_map.get(
|
||||
vendor_media_codec_information.codec_id
|
||||
)
|
||||
):
|
||||
return media_codec_information_class.from_bytes(
|
||||
vendor_media_codec_information.value
|
||||
)
|
||||
return vendor_media_codec_information
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data: bytes) -> Self:
|
||||
del data # Unused.
|
||||
raise NotImplementedError
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@dataclasses.dataclass
|
||||
class SbcMediaCodecInformation(MediaCodecInformation):
|
||||
class SbcMediaCodecInformation:
|
||||
'''
|
||||
A2DP spec - 4.3.2 Codec Specific Information Elements
|
||||
'''
|
||||
@@ -384,7 +346,7 @@ class SbcMediaCodecInformation(MediaCodecInformation):
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@dataclasses.dataclass
|
||||
class AacMediaCodecInformation(MediaCodecInformation):
|
||||
class AacMediaCodecInformation:
|
||||
'''
|
||||
A2DP spec - 4.5.2 Codec Specific Information Elements
|
||||
'''
|
||||
@@ -466,7 +428,7 @@ class AacMediaCodecInformation(MediaCodecInformation):
|
||||
|
||||
@dataclasses.dataclass
|
||||
# -----------------------------------------------------------------------------
|
||||
class VendorSpecificMediaCodecInformation(MediaCodecInformation):
|
||||
class VendorSpecificMediaCodecInformation:
|
||||
'''
|
||||
A2DP spec - 4.7.2 Codec Specific Information Elements
|
||||
'''
|
||||
@@ -490,7 +452,7 @@ class VendorSpecificMediaCodecInformation(MediaCodecInformation):
|
||||
'VendorSpecificMediaCodecInformation(',
|
||||
f' vendor_id: {self.vendor_id:08X} ({name_or_number(COMPANY_IDENTIFIERS, self.vendor_id & 0xFFFF)})',
|
||||
f' codec_id: {self.codec_id:04X}',
|
||||
f' value: {self.value.hex()})',
|
||||
f' value: {self.value.hex()}' ')',
|
||||
]
|
||||
)
|
||||
|
||||
@@ -517,12 +479,6 @@ class OpusMediaCodecInformation(VendorSpecificMediaCodecInformation):
|
||||
class SamplingFrequency(enum.IntFlag):
|
||||
SF_48000 = 1 << 0
|
||||
|
||||
@classmethod
|
||||
def from_int(cls, sampling_frequency: int) -> Self:
|
||||
if sampling_frequency != 48000:
|
||||
raise ValueError("no such sampling frequency")
|
||||
return cls(1)
|
||||
|
||||
VENDOR_ID: ClassVar[int] = 0x000000E0
|
||||
CODEC_ID: ClassVar[int] = 0x0001
|
||||
|
||||
@@ -686,7 +642,7 @@ class SbcPacketSource:
|
||||
# Prepare for next packets
|
||||
sequence_number += 1
|
||||
sequence_number &= 0xFFFF
|
||||
sample_count += sum(frame.sample_count for frame in frames)
|
||||
sample_count += sum((frame.sample_count for frame in frames))
|
||||
frames = [frame]
|
||||
frames_size = len(frame.payload)
|
||||
else:
|
||||
|
||||
13
bumble/at.py
13
bumble/at.py
@@ -12,6 +12,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from typing import List, Union
|
||||
|
||||
from bumble import core
|
||||
|
||||
@@ -20,7 +21,7 @@ class AtParsingError(core.InvalidPacketError):
|
||||
"""Error raised when parsing AT commands fails."""
|
||||
|
||||
|
||||
def tokenize_parameters(buffer: bytes) -> list[bytes]:
|
||||
def tokenize_parameters(buffer: bytes) -> List[bytes]:
|
||||
"""Split input parameters into tokens.
|
||||
Removes space characters outside of double quote blocks:
|
||||
T-rec-V-25 - 5.2.1 Command line general format: "Space characters (IA5 2/0)
|
||||
@@ -35,7 +36,7 @@ def tokenize_parameters(buffer: bytes) -> list[bytes]:
|
||||
|
||||
if in_quotes:
|
||||
token.extend(char)
|
||||
if char == b'"':
|
||||
if char == b'\"':
|
||||
in_quotes = False
|
||||
tokens.append(token[1:-1])
|
||||
token = bytearray()
|
||||
@@ -62,18 +63,18 @@ def tokenize_parameters(buffer: bytes) -> list[bytes]:
|
||||
return [bytes(token) for token in tokens if len(token) > 0]
|
||||
|
||||
|
||||
def parse_parameters(buffer: bytes) -> list[bytes | list]:
|
||||
def parse_parameters(buffer: bytes) -> List[Union[bytes, list]]:
|
||||
"""Parse the parameters using the comma and parenthesis separators.
|
||||
Raises AtParsingError in case of invalid input string."""
|
||||
|
||||
tokens = tokenize_parameters(buffer)
|
||||
accumulator: list[list] = [[]]
|
||||
current: bytes | list = b''
|
||||
accumulator: List[list] = [[]]
|
||||
current: Union[bytes, list] = bytes()
|
||||
|
||||
for token in tokens:
|
||||
if token == b',':
|
||||
accumulator[-1].append(current)
|
||||
current = b''
|
||||
current = bytes()
|
||||
elif token == b'(':
|
||||
accumulator.append([])
|
||||
elif token == b')':
|
||||
|
||||
603
bumble/att.py
603
bumble/att.py
@@ -24,26 +24,28 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import enum
|
||||
import functools
|
||||
import inspect
|
||||
import struct
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
ClassVar,
|
||||
Awaitable,
|
||||
Callable,
|
||||
Generic,
|
||||
TypeAlias,
|
||||
Dict,
|
||||
List,
|
||||
Optional,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
from typing_extensions import TypeIs
|
||||
|
||||
from bumble import hci, l2cap, utils
|
||||
from bumble import utils
|
||||
from bumble.core import UUID, name_or_number, InvalidOperationError, ProtocolError
|
||||
from bumble.hci import HCI_Object, key_with_value
|
||||
from bumble.colors import color
|
||||
from bumble.core import UUID, InvalidOperationError, ProtocolError
|
||||
from bumble.hci import HCI_Object
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Typing
|
||||
@@ -53,14 +55,6 @@ if TYPE_CHECKING:
|
||||
|
||||
_T = TypeVar('_T')
|
||||
|
||||
Bearer: TypeAlias = "Connection | l2cap.LeCreditBasedChannel"
|
||||
EnhancedBearer: TypeAlias = l2cap.LeCreditBasedChannel
|
||||
|
||||
|
||||
def is_enhanced_bearer(bearer: Bearer) -> TypeIs[EnhancedBearer]:
|
||||
return isinstance(bearer, EnhancedBearer)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -69,68 +63,97 @@ def is_enhanced_bearer(bearer: Bearer) -> TypeIs[EnhancedBearer]:
|
||||
|
||||
ATT_CID = 0x04
|
||||
ATT_PSM = 0x001F
|
||||
EATT_PSM = 0x0027
|
||||
|
||||
class Opcode(hci.SpecableEnum):
|
||||
ATT_ERROR_RESPONSE = 0x01
|
||||
ATT_EXCHANGE_MTU_REQUEST = 0x02
|
||||
ATT_EXCHANGE_MTU_RESPONSE = 0x03
|
||||
ATT_FIND_INFORMATION_REQUEST = 0x04
|
||||
ATT_FIND_INFORMATION_RESPONSE = 0x05
|
||||
ATT_FIND_BY_TYPE_VALUE_REQUEST = 0x06
|
||||
ATT_FIND_BY_TYPE_VALUE_RESPONSE = 0x07
|
||||
ATT_READ_BY_TYPE_REQUEST = 0x08
|
||||
ATT_READ_BY_TYPE_RESPONSE = 0x09
|
||||
ATT_READ_REQUEST = 0x0A
|
||||
ATT_READ_RESPONSE = 0x0B
|
||||
ATT_READ_BLOB_REQUEST = 0x0C
|
||||
ATT_READ_BLOB_RESPONSE = 0x0D
|
||||
ATT_READ_MULTIPLE_REQUEST = 0x0E
|
||||
ATT_READ_MULTIPLE_RESPONSE = 0x0F
|
||||
ATT_READ_BY_GROUP_TYPE_REQUEST = 0x10
|
||||
ATT_READ_BY_GROUP_TYPE_RESPONSE = 0x11
|
||||
ATT_WRITE_REQUEST = 0x12
|
||||
ATT_WRITE_RESPONSE = 0x13
|
||||
ATT_WRITE_COMMAND = 0x52
|
||||
ATT_SIGNED_WRITE_COMMAND = 0xD2
|
||||
ATT_PREPARE_WRITE_REQUEST = 0x16
|
||||
ATT_PREPARE_WRITE_RESPONSE = 0x17
|
||||
ATT_EXECUTE_WRITE_REQUEST = 0x18
|
||||
ATT_EXECUTE_WRITE_RESPONSE = 0x19
|
||||
ATT_HANDLE_VALUE_NOTIFICATION = 0x1B
|
||||
ATT_HANDLE_VALUE_INDICATION = 0x1D
|
||||
ATT_HANDLE_VALUE_CONFIRMATION = 0x1E
|
||||
ATT_ERROR_RESPONSE = 0x01
|
||||
ATT_EXCHANGE_MTU_REQUEST = 0x02
|
||||
ATT_EXCHANGE_MTU_RESPONSE = 0x03
|
||||
ATT_FIND_INFORMATION_REQUEST = 0x04
|
||||
ATT_FIND_INFORMATION_RESPONSE = 0x05
|
||||
ATT_FIND_BY_TYPE_VALUE_REQUEST = 0x06
|
||||
ATT_FIND_BY_TYPE_VALUE_RESPONSE = 0x07
|
||||
ATT_READ_BY_TYPE_REQUEST = 0x08
|
||||
ATT_READ_BY_TYPE_RESPONSE = 0x09
|
||||
ATT_READ_REQUEST = 0x0A
|
||||
ATT_READ_RESPONSE = 0x0B
|
||||
ATT_READ_BLOB_REQUEST = 0x0C
|
||||
ATT_READ_BLOB_RESPONSE = 0x0D
|
||||
ATT_READ_MULTIPLE_REQUEST = 0x0E
|
||||
ATT_READ_MULTIPLE_RESPONSE = 0x0F
|
||||
ATT_READ_BY_GROUP_TYPE_REQUEST = 0x10
|
||||
ATT_READ_BY_GROUP_TYPE_RESPONSE = 0x11
|
||||
ATT_WRITE_REQUEST = 0x12
|
||||
ATT_WRITE_RESPONSE = 0x13
|
||||
ATT_WRITE_COMMAND = 0x52
|
||||
ATT_SIGNED_WRITE_COMMAND = 0xD2
|
||||
ATT_PREPARE_WRITE_REQUEST = 0x16
|
||||
ATT_PREPARE_WRITE_RESPONSE = 0x17
|
||||
ATT_EXECUTE_WRITE_REQUEST = 0x18
|
||||
ATT_EXECUTE_WRITE_RESPONSE = 0x19
|
||||
ATT_HANDLE_VALUE_NOTIFICATION = 0x1B
|
||||
ATT_HANDLE_VALUE_INDICATION = 0x1D
|
||||
ATT_HANDLE_VALUE_CONFIRMATION = 0x1E
|
||||
|
||||
ATT_PDU_NAMES = {
|
||||
ATT_ERROR_RESPONSE: 'ATT_ERROR_RESPONSE',
|
||||
ATT_EXCHANGE_MTU_REQUEST: 'ATT_EXCHANGE_MTU_REQUEST',
|
||||
ATT_EXCHANGE_MTU_RESPONSE: 'ATT_EXCHANGE_MTU_RESPONSE',
|
||||
ATT_FIND_INFORMATION_REQUEST: 'ATT_FIND_INFORMATION_REQUEST',
|
||||
ATT_FIND_INFORMATION_RESPONSE: 'ATT_FIND_INFORMATION_RESPONSE',
|
||||
ATT_FIND_BY_TYPE_VALUE_REQUEST: 'ATT_FIND_BY_TYPE_VALUE_REQUEST',
|
||||
ATT_FIND_BY_TYPE_VALUE_RESPONSE: 'ATT_FIND_BY_TYPE_VALUE_RESPONSE',
|
||||
ATT_READ_BY_TYPE_REQUEST: 'ATT_READ_BY_TYPE_REQUEST',
|
||||
ATT_READ_BY_TYPE_RESPONSE: 'ATT_READ_BY_TYPE_RESPONSE',
|
||||
ATT_READ_REQUEST: 'ATT_READ_REQUEST',
|
||||
ATT_READ_RESPONSE: 'ATT_READ_RESPONSE',
|
||||
ATT_READ_BLOB_REQUEST: 'ATT_READ_BLOB_REQUEST',
|
||||
ATT_READ_BLOB_RESPONSE: 'ATT_READ_BLOB_RESPONSE',
|
||||
ATT_READ_MULTIPLE_REQUEST: 'ATT_READ_MULTIPLE_REQUEST',
|
||||
ATT_READ_MULTIPLE_RESPONSE: 'ATT_READ_MULTIPLE_RESPONSE',
|
||||
ATT_READ_BY_GROUP_TYPE_REQUEST: 'ATT_READ_BY_GROUP_TYPE_REQUEST',
|
||||
ATT_READ_BY_GROUP_TYPE_RESPONSE: 'ATT_READ_BY_GROUP_TYPE_RESPONSE',
|
||||
ATT_WRITE_REQUEST: 'ATT_WRITE_REQUEST',
|
||||
ATT_WRITE_RESPONSE: 'ATT_WRITE_RESPONSE',
|
||||
ATT_WRITE_COMMAND: 'ATT_WRITE_COMMAND',
|
||||
ATT_SIGNED_WRITE_COMMAND: 'ATT_SIGNED_WRITE_COMMAND',
|
||||
ATT_PREPARE_WRITE_REQUEST: 'ATT_PREPARE_WRITE_REQUEST',
|
||||
ATT_PREPARE_WRITE_RESPONSE: 'ATT_PREPARE_WRITE_RESPONSE',
|
||||
ATT_EXECUTE_WRITE_REQUEST: 'ATT_EXECUTE_WRITE_REQUEST',
|
||||
ATT_EXECUTE_WRITE_RESPONSE: 'ATT_EXECUTE_WRITE_RESPONSE',
|
||||
ATT_HANDLE_VALUE_NOTIFICATION: 'ATT_HANDLE_VALUE_NOTIFICATION',
|
||||
ATT_HANDLE_VALUE_INDICATION: 'ATT_HANDLE_VALUE_INDICATION',
|
||||
ATT_HANDLE_VALUE_CONFIRMATION: 'ATT_HANDLE_VALUE_CONFIRMATION'
|
||||
}
|
||||
|
||||
ATT_REQUESTS = [
|
||||
Opcode.ATT_EXCHANGE_MTU_REQUEST,
|
||||
Opcode.ATT_FIND_INFORMATION_REQUEST,
|
||||
Opcode.ATT_FIND_BY_TYPE_VALUE_REQUEST,
|
||||
Opcode.ATT_READ_BY_TYPE_REQUEST,
|
||||
Opcode.ATT_READ_REQUEST,
|
||||
Opcode.ATT_READ_BLOB_REQUEST,
|
||||
Opcode.ATT_READ_MULTIPLE_REQUEST,
|
||||
Opcode.ATT_READ_BY_GROUP_TYPE_REQUEST,
|
||||
Opcode.ATT_WRITE_REQUEST,
|
||||
Opcode.ATT_PREPARE_WRITE_REQUEST,
|
||||
Opcode.ATT_EXECUTE_WRITE_REQUEST
|
||||
ATT_EXCHANGE_MTU_REQUEST,
|
||||
ATT_FIND_INFORMATION_REQUEST,
|
||||
ATT_FIND_BY_TYPE_VALUE_REQUEST,
|
||||
ATT_READ_BY_TYPE_REQUEST,
|
||||
ATT_READ_REQUEST,
|
||||
ATT_READ_BLOB_REQUEST,
|
||||
ATT_READ_MULTIPLE_REQUEST,
|
||||
ATT_READ_BY_GROUP_TYPE_REQUEST,
|
||||
ATT_WRITE_REQUEST,
|
||||
ATT_PREPARE_WRITE_REQUEST,
|
||||
ATT_EXECUTE_WRITE_REQUEST
|
||||
]
|
||||
|
||||
ATT_RESPONSES = [
|
||||
Opcode.ATT_ERROR_RESPONSE,
|
||||
Opcode.ATT_EXCHANGE_MTU_RESPONSE,
|
||||
Opcode.ATT_FIND_INFORMATION_RESPONSE,
|
||||
Opcode.ATT_FIND_BY_TYPE_VALUE_RESPONSE,
|
||||
Opcode.ATT_READ_BY_TYPE_RESPONSE,
|
||||
Opcode.ATT_READ_RESPONSE,
|
||||
Opcode.ATT_READ_BLOB_RESPONSE,
|
||||
Opcode.ATT_READ_MULTIPLE_RESPONSE,
|
||||
Opcode.ATT_READ_BY_GROUP_TYPE_RESPONSE,
|
||||
Opcode.ATT_WRITE_RESPONSE,
|
||||
Opcode.ATT_PREPARE_WRITE_RESPONSE,
|
||||
Opcode.ATT_EXECUTE_WRITE_RESPONSE
|
||||
ATT_ERROR_RESPONSE,
|
||||
ATT_EXCHANGE_MTU_RESPONSE,
|
||||
ATT_FIND_INFORMATION_RESPONSE,
|
||||
ATT_FIND_BY_TYPE_VALUE_RESPONSE,
|
||||
ATT_READ_BY_TYPE_RESPONSE,
|
||||
ATT_READ_RESPONSE,
|
||||
ATT_READ_BLOB_RESPONSE,
|
||||
ATT_READ_MULTIPLE_RESPONSE,
|
||||
ATT_READ_BY_GROUP_TYPE_RESPONSE,
|
||||
ATT_WRITE_RESPONSE,
|
||||
ATT_PREPARE_WRITE_RESPONSE,
|
||||
ATT_EXECUTE_WRITE_RESPONSE
|
||||
]
|
||||
|
||||
class ErrorCode(hci.SpecableEnum):
|
||||
class ErrorCode(utils.OpenIntEnum):
|
||||
'''
|
||||
See
|
||||
|
||||
@@ -185,6 +208,10 @@ ATT_INSUFFICIENT_RESOURCES_ERROR = ErrorCode.INSUFFICIENT_RESOURCES
|
||||
ATT_DEFAULT_MTU = 23
|
||||
|
||||
HANDLE_FIELD_SPEC = {'size': 2, 'mapper': lambda x: f'0x{x:04X}'}
|
||||
# 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
|
||||
|
||||
# fmt: on
|
||||
# pylint: enable=line-too-long
|
||||
@@ -204,7 +231,7 @@ class ATT_Error(ProtocolError):
|
||||
super().__init__(
|
||||
error_code,
|
||||
error_namespace='att',
|
||||
error_name=ErrorCode(error_code).name,
|
||||
error_name=ATT_PDU.error_name(error_code),
|
||||
)
|
||||
self.att_handle = att_handle
|
||||
self.message = message
|
||||
@@ -219,45 +246,61 @@ class ATT_Error(ProtocolError):
|
||||
# -----------------------------------------------------------------------------
|
||||
# Attribute Protocol
|
||||
# -----------------------------------------------------------------------------
|
||||
@dataclasses.dataclass
|
||||
class ATT_PDU:
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.3 ATTRIBUTE PDU
|
||||
'''
|
||||
|
||||
pdu_classes: ClassVar[dict[int, type[ATT_PDU]]] = {}
|
||||
fields: ClassVar[hci.Fields] = ()
|
||||
op_code: int = dataclasses.field(init=False)
|
||||
name: str = dataclasses.field(init=False)
|
||||
_payload: bytes | None = dataclasses.field(default=None, init=False)
|
||||
pdu_classes: Dict[int, Type[ATT_PDU]] = {}
|
||||
op_code = 0
|
||||
name: str
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, pdu: bytes) -> ATT_PDU:
|
||||
@staticmethod
|
||||
def from_bytes(pdu):
|
||||
op_code = pdu[0]
|
||||
|
||||
subclass = ATT_PDU.pdu_classes.get(op_code)
|
||||
if subclass is None:
|
||||
instance = ATT_PDU()
|
||||
cls = ATT_PDU.pdu_classes.get(op_code)
|
||||
if cls is None:
|
||||
instance = ATT_PDU(pdu)
|
||||
instance.name = ATT_PDU.pdu_name(op_code)
|
||||
instance.op_code = op_code
|
||||
instance.payload = pdu[1:]
|
||||
instance.name = Opcode(op_code).name
|
||||
return instance
|
||||
instance = subclass(**HCI_Object.dict_from_bytes(pdu, 1, subclass.fields))
|
||||
instance.payload = pdu[1:]
|
||||
return instance
|
||||
self = cls.__new__(cls)
|
||||
ATT_PDU.__init__(self, pdu)
|
||||
if hasattr(self, 'fields'):
|
||||
self.init_from_bytes(pdu, 1)
|
||||
return self
|
||||
|
||||
_PDU = TypeVar("_PDU", bound="ATT_PDU")
|
||||
@staticmethod
|
||||
def pdu_name(op_code):
|
||||
return name_or_number(ATT_PDU_NAMES, op_code, 2)
|
||||
|
||||
@classmethod
|
||||
def subclass(cls, subclass: type[_PDU]) -> type[_PDU]:
|
||||
subclass.name = subclass.__name__.upper()
|
||||
subclass.op_code = Opcode[subclass.name]
|
||||
subclass.fields = HCI_Object.fields_from_dataclass(subclass)
|
||||
def error_name(cls, error_code: int) -> str:
|
||||
return ErrorCode(error_code).name
|
||||
|
||||
# Register a factory for this class
|
||||
ATT_PDU.pdu_classes[subclass.op_code] = subclass
|
||||
@staticmethod
|
||||
def subclass(fields):
|
||||
def inner(cls):
|
||||
cls.name = cls.__name__.upper()
|
||||
cls.op_code = key_with_value(ATT_PDU_NAMES, cls.name)
|
||||
if cls.op_code is None:
|
||||
raise KeyError(f'PDU name {cls.name} not found in ATT_PDU_NAMES')
|
||||
cls.fields = fields
|
||||
|
||||
return subclass
|
||||
# Register a factory for this class
|
||||
ATT_PDU.pdu_classes[cls.op_code] = cls
|
||||
|
||||
return cls
|
||||
|
||||
return inner
|
||||
|
||||
def __init__(self, pdu=None, **kwargs):
|
||||
if hasattr(self, 'fields') and kwargs:
|
||||
HCI_Object.init_from_fields(self, self.fields, kwargs)
|
||||
if pdu is None:
|
||||
pdu = bytes([self.op_code]) + HCI_Object.dict_to_bytes(kwargs, self.fields)
|
||||
self.pdu = pdu
|
||||
|
||||
def init_from_bytes(self, pdu, offset):
|
||||
return HCI_Object.init_from_bytes(self, pdu, offset, self.fields)
|
||||
@@ -270,91 +313,67 @@ class ATT_PDU:
|
||||
def has_authentication_signature(self):
|
||||
return ((self.op_code >> 7) & 1) == 1
|
||||
|
||||
@property
|
||||
def payload(self) -> bytes:
|
||||
if self._payload is None:
|
||||
self._payload = HCI_Object.dict_to_bytes(self.__dict__, self.fields)
|
||||
return self._payload
|
||||
|
||||
@payload.setter
|
||||
def payload(self, value: bytes):
|
||||
self._payload = value
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
return bytes([self.op_code]) + self.payload
|
||||
def __bytes__(self):
|
||||
return self.pdu
|
||||
|
||||
def __str__(self):
|
||||
result = color(self.name, 'yellow')
|
||||
if fields := getattr(self, 'fields', None):
|
||||
result += ':\n' + HCI_Object.format_fields(self.__dict__, fields, ' ')
|
||||
else:
|
||||
if self.payload:
|
||||
result += f': {self.payload.hex()}'
|
||||
if len(self.pdu) > 1:
|
||||
result += f': {self.pdu.hex()}'
|
||||
return result
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
@ATT_PDU.subclass(
|
||||
[
|
||||
('request_opcode_in_error', {'size': 1, 'mapper': ATT_PDU.pdu_name}),
|
||||
('attribute_handle_in_error', HANDLE_FIELD_SPEC),
|
||||
('error_code', {'size': 1, 'mapper': ATT_PDU.error_name}),
|
||||
]
|
||||
)
|
||||
class ATT_Error_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.1.1 Error Response
|
||||
'''
|
||||
|
||||
request_opcode_in_error: int = dataclasses.field(metadata=Opcode.type_metadata(1))
|
||||
attribute_handle_in_error: int = dataclasses.field(
|
||||
metadata=hci.metadata(HANDLE_FIELD_SPEC)
|
||||
)
|
||||
error_code: int = dataclasses.field(metadata=ErrorCode.type_metadata(1))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
@ATT_PDU.subclass([('client_rx_mtu', 2)])
|
||||
class ATT_Exchange_MTU_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.2.1 Exchange MTU Request
|
||||
'''
|
||||
|
||||
client_rx_mtu: int = dataclasses.field(metadata=hci.metadata(2))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
@ATT_PDU.subclass([('server_rx_mtu', 2)])
|
||||
class ATT_Exchange_MTU_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.2.2 Exchange MTU Response
|
||||
'''
|
||||
|
||||
server_rx_mtu: int = dataclasses.field(metadata=hci.metadata(2))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
@ATT_PDU.subclass(
|
||||
[('starting_handle', HANDLE_FIELD_SPEC), ('ending_handle', HANDLE_FIELD_SPEC)]
|
||||
)
|
||||
class ATT_Find_Information_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.3.1 Find Information Request
|
||||
'''
|
||||
|
||||
starting_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
ending_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
@ATT_PDU.subclass([('format', 1), ('information_data', '*')])
|
||||
class ATT_Find_Information_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.3.2 Find Information Response
|
||||
'''
|
||||
|
||||
format: int = dataclasses.field(metadata=hci.metadata(1))
|
||||
information_data: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
information: list[tuple[int, bytes]] = dataclasses.field(init=False)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
def parse_information_data(self):
|
||||
self.information = []
|
||||
offset = 0
|
||||
uuid_size = 2 if self.format == 1 else 16
|
||||
@@ -364,6 +383,14 @@ class ATT_Find_Information_Response(ATT_PDU):
|
||||
self.information.append((handle, uuid))
|
||||
offset += 2 + uuid_size
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.parse_information_data()
|
||||
|
||||
def init_from_bytes(self, pdu, offset):
|
||||
super().init_from_bytes(pdu, offset)
|
||||
self.parse_information_data()
|
||||
|
||||
def __str__(self):
|
||||
result = color(self.name, 'yellow')
|
||||
result += ':\n' + HCI_Object.format_fields(
|
||||
@@ -385,31 +412,28 @@ class ATT_Find_Information_Response(ATT_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
@ATT_PDU.subclass(
|
||||
[
|
||||
('starting_handle', HANDLE_FIELD_SPEC),
|
||||
('ending_handle', HANDLE_FIELD_SPEC),
|
||||
('attribute_type', UUID_2_FIELD_SPEC),
|
||||
('attribute_value', '*'),
|
||||
]
|
||||
)
|
||||
class ATT_Find_By_Type_Value_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.3.3 Find By Type Value Request
|
||||
'''
|
||||
|
||||
starting_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
ending_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
attribute_type: UUID = dataclasses.field(metadata=hci.metadata(UUID.parse_uuid_2))
|
||||
attribute_value: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
@ATT_PDU.subclass([('handles_information_list', '*')])
|
||||
class ATT_Find_By_Type_Value_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.3.4 Find By Type Value Response
|
||||
'''
|
||||
|
||||
handles_information_list: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
handles_information: list[tuple[int, int]] = dataclasses.field(init=False)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
def parse_handles_information_list(self):
|
||||
self.handles_information = []
|
||||
offset = 0
|
||||
while offset + 4 <= len(self.handles_information_list):
|
||||
@@ -419,6 +443,14 @@ class ATT_Find_By_Type_Value_Response(ATT_PDU):
|
||||
self.handles_information.append((found_attribute_handle, group_end_handle))
|
||||
offset += 4
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.parse_handles_information_list()
|
||||
|
||||
def init_from_bytes(self, pdu, offset):
|
||||
super().init_from_bytes(pdu, offset)
|
||||
self.parse_handles_information_list()
|
||||
|
||||
def __str__(self):
|
||||
result = color(self.name, 'yellow')
|
||||
result += ':\n' + HCI_Object.format_fields(
|
||||
@@ -442,31 +474,27 @@ class ATT_Find_By_Type_Value_Response(ATT_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
@ATT_PDU.subclass(
|
||||
[
|
||||
('starting_handle', HANDLE_FIELD_SPEC),
|
||||
('ending_handle', HANDLE_FIELD_SPEC),
|
||||
('attribute_type', UUID_2_16_FIELD_SPEC),
|
||||
]
|
||||
)
|
||||
class ATT_Read_By_Type_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.1 Read By Type Request
|
||||
'''
|
||||
|
||||
starting_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
ending_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
attribute_type: UUID = dataclasses.field(metadata=hci.metadata(UUID.parse_uuid))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
@ATT_PDU.subclass([('length', 1), ('attribute_data_list', '*')])
|
||||
class ATT_Read_By_Type_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.2 Read By Type Response
|
||||
'''
|
||||
|
||||
length: int = dataclasses.field(metadata=hci.metadata(1))
|
||||
attribute_data_list: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
attributes: list[tuple[int, bytes]] = dataclasses.field(init=False)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
def parse_attribute_data_list(self):
|
||||
self.attributes = []
|
||||
offset = 0
|
||||
while self.length != 0 and offset + self.length <= len(
|
||||
@@ -481,6 +509,14 @@ class ATT_Read_By_Type_Response(ATT_PDU):
|
||||
self.attributes.append((attribute_handle, attribute_value))
|
||||
offset += self.length
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.parse_attribute_data_list()
|
||||
|
||||
def init_from_bytes(self, pdu, offset):
|
||||
super().init_from_bytes(pdu, offset)
|
||||
self.parse_attribute_data_list()
|
||||
|
||||
def __str__(self):
|
||||
result = color(self.name, 'yellow')
|
||||
result += ':\n' + HCI_Object.format_fields(
|
||||
@@ -502,100 +538,75 @@ class ATT_Read_By_Type_Response(ATT_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
@ATT_PDU.subclass([('attribute_handle', HANDLE_FIELD_SPEC)])
|
||||
class ATT_Read_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.3 Read Request
|
||||
'''
|
||||
|
||||
attribute_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
@ATT_PDU.subclass([('attribute_value', '*')])
|
||||
class ATT_Read_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.4 Read Response
|
||||
'''
|
||||
|
||||
attribute_value: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
@ATT_PDU.subclass([('attribute_handle', HANDLE_FIELD_SPEC), ('value_offset', 2)])
|
||||
class ATT_Read_Blob_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.5 Read Blob Request
|
||||
'''
|
||||
|
||||
attribute_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
value_offset: int = dataclasses.field(metadata=hci.metadata(2))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
@ATT_PDU.subclass([('part_attribute_value', '*')])
|
||||
class ATT_Read_Blob_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.6 Read Blob Response
|
||||
'''
|
||||
|
||||
part_attribute_value: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
@ATT_PDU.subclass([('set_of_handles', '*')])
|
||||
class ATT_Read_Multiple_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.7 Read Multiple Request
|
||||
'''
|
||||
|
||||
set_of_handles: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
@ATT_PDU.subclass([('set_of_values', '*')])
|
||||
class ATT_Read_Multiple_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.8 Read Multiple Response
|
||||
'''
|
||||
|
||||
set_of_values: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
@ATT_PDU.subclass(
|
||||
[
|
||||
('starting_handle', HANDLE_FIELD_SPEC),
|
||||
('ending_handle', HANDLE_FIELD_SPEC),
|
||||
('attribute_group_type', UUID_2_16_FIELD_SPEC),
|
||||
]
|
||||
)
|
||||
class ATT_Read_By_Group_Type_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.9 Read by Group Type Request
|
||||
'''
|
||||
|
||||
starting_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
ending_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
attribute_group_type: UUID = dataclasses.field(
|
||||
metadata=hci.metadata(UUID.parse_uuid)
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
@ATT_PDU.subclass([('length', 1), ('attribute_data_list', '*')])
|
||||
class ATT_Read_By_Group_Type_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.10 Read by Group Type Response
|
||||
'''
|
||||
|
||||
length: int = dataclasses.field(metadata=hci.metadata(1))
|
||||
attribute_data_list: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
attributes: list[tuple[int, int, bytes]] = dataclasses.field(init=False)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
def parse_attribute_data_list(self):
|
||||
self.attributes = []
|
||||
offset = 0
|
||||
while self.length != 0 and offset + self.length <= len(
|
||||
@@ -612,6 +623,14 @@ class ATT_Read_By_Group_Type_Response(ATT_PDU):
|
||||
)
|
||||
offset += self.length
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.parse_attribute_data_list()
|
||||
|
||||
def init_from_bytes(self, pdu, offset):
|
||||
super().init_from_bytes(pdu, offset)
|
||||
self.parse_attribute_data_list()
|
||||
|
||||
def __str__(self):
|
||||
result = color(self.name, 'yellow')
|
||||
result += ':\n' + HCI_Object.format_fields(
|
||||
@@ -636,20 +655,15 @@ class ATT_Read_By_Group_Type_Response(ATT_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
@ATT_PDU.subclass([('attribute_handle', HANDLE_FIELD_SPEC), ('attribute_value', '*')])
|
||||
class ATT_Write_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.5.1 Write Request
|
||||
'''
|
||||
|
||||
attribute_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
attribute_value: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
@ATT_PDU.subclass([])
|
||||
class ATT_Write_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.5.2 Write Response
|
||||
@@ -657,70 +671,65 @@ class ATT_Write_Response(ATT_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
@ATT_PDU.subclass([('attribute_handle', HANDLE_FIELD_SPEC), ('attribute_value', '*')])
|
||||
class ATT_Write_Command(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.5.3 Write Command
|
||||
'''
|
||||
|
||||
attribute_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
attribute_value: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
@ATT_PDU.subclass(
|
||||
[
|
||||
('attribute_handle', HANDLE_FIELD_SPEC),
|
||||
('attribute_value', '*'),
|
||||
# ('authentication_signature', 'TODO')
|
||||
]
|
||||
)
|
||||
class ATT_Signed_Write_Command(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.5.4 Signed Write Command
|
||||
'''
|
||||
|
||||
attribute_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
attribute_value: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
# TODO: authentication_signature
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
@ATT_PDU.subclass(
|
||||
[
|
||||
('attribute_handle', HANDLE_FIELD_SPEC),
|
||||
('value_offset', 2),
|
||||
('part_attribute_value', '*'),
|
||||
]
|
||||
)
|
||||
class ATT_Prepare_Write_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.6.1 Prepare Write Request
|
||||
'''
|
||||
|
||||
attribute_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
value_offset: int = dataclasses.field(metadata=hci.metadata(2))
|
||||
part_attribute_value: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
@ATT_PDU.subclass(
|
||||
[
|
||||
('attribute_handle', HANDLE_FIELD_SPEC),
|
||||
('value_offset', 2),
|
||||
('part_attribute_value', '*'),
|
||||
]
|
||||
)
|
||||
class ATT_Prepare_Write_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.6.2 Prepare Write Response
|
||||
'''
|
||||
|
||||
attribute_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
value_offset: int = dataclasses.field(metadata=hci.metadata(2))
|
||||
part_attribute_value: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
@ATT_PDU.subclass([("flags", 1)])
|
||||
class ATT_Execute_Write_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.6.3 Execute Write Request
|
||||
'''
|
||||
|
||||
flags: int = dataclasses.field(metadata=hci.metadata(1))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
@ATT_PDU.subclass([])
|
||||
class ATT_Execute_Write_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.6.4 Execute Write Response
|
||||
@@ -728,32 +737,23 @@ class ATT_Execute_Write_Response(ATT_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
@ATT_PDU.subclass([('attribute_handle', HANDLE_FIELD_SPEC), ('attribute_value', '*')])
|
||||
class ATT_Handle_Value_Notification(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.7.1 Handle Value Notification
|
||||
'''
|
||||
|
||||
attribute_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
attribute_value: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
@ATT_PDU.subclass([('attribute_handle', HANDLE_FIELD_SPEC), ('attribute_value', '*')])
|
||||
class ATT_Handle_Value_Indication(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.7.2 Handle Value Indication
|
||||
'''
|
||||
|
||||
attribute_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
attribute_value: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
@ATT_PDU.subclass([])
|
||||
class ATT_Handle_Value_Confirmation(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.7.3 Handle Value Confirmation
|
||||
@@ -769,66 +769,33 @@ class AttributeValue(Generic[_T]):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
read: (
|
||||
Callable[[Connection], _T] | Callable[[Connection], Awaitable[_T]] | None
|
||||
) = None,
|
||||
write: (
|
||||
Callable[[Connection, _T], None]
|
||||
| Callable[[Connection, _T], Awaitable[None]]
|
||||
| None
|
||||
) = None,
|
||||
read: Union[
|
||||
Callable[[Optional[Connection]], _T],
|
||||
Callable[[Optional[Connection]], Awaitable[_T]],
|
||||
None,
|
||||
] = None,
|
||||
write: Union[
|
||||
Callable[[Optional[Connection], _T], None],
|
||||
Callable[[Optional[Connection], _T], Awaitable[None]],
|
||||
None,
|
||||
] = None,
|
||||
):
|
||||
self._read = read
|
||||
self._write = write
|
||||
|
||||
def read(self, connection: Connection) -> _T | Awaitable[_T]:
|
||||
def read(self, connection: Optional[Connection]) -> Union[_T, Awaitable[_T]]:
|
||||
if self._read is None:
|
||||
raise InvalidOperationError('AttributeValue has no read function')
|
||||
return self._read(connection)
|
||||
|
||||
def write(self, connection: Connection, value: _T) -> Awaitable[None] | None:
|
||||
def write(
|
||||
self, connection: Optional[Connection], value: _T
|
||||
) -> Union[Awaitable[None], None]:
|
||||
if self._write is None:
|
||||
raise InvalidOperationError('AttributeValue has no write function')
|
||||
return self._write(connection, value)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class AttributeValueV2(Generic[_T]):
|
||||
'''
|
||||
Attribute value compatible with enhanced bearers.
|
||||
|
||||
The only difference between AttributeValue and AttributeValueV2 is that the actual
|
||||
bearer (ACL connection for un-enhanced bearer, L2CAP channel for enhanced bearer)
|
||||
will be passed into read and write callbacks in V2, while in V1 it is always
|
||||
the base ACL connection.
|
||||
|
||||
This is only required when attributes must distinguish bearers, otherwise normal
|
||||
`AttributeValue` objects are also applicable in enhanced bearers.
|
||||
'''
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
read: Callable[[Bearer], Awaitable[_T]] | Callable[[Bearer], _T] | None = None,
|
||||
write: (
|
||||
Callable[[Bearer, _T], Awaitable[None]]
|
||||
| Callable[[Bearer, _T], None]
|
||||
| None
|
||||
) = None,
|
||||
):
|
||||
self._read = read
|
||||
self._write = write
|
||||
|
||||
def read(self, bearer: Bearer) -> _T | Awaitable[_T]:
|
||||
if self._read is None:
|
||||
raise InvalidOperationError('AttributeValue has no read function')
|
||||
return self._read(bearer)
|
||||
|
||||
def write(self, bearer: Bearer, value: _T) -> Awaitable[None] | None:
|
||||
if self._write is None:
|
||||
raise InvalidOperationError('AttributeValue has no write function')
|
||||
return self._write(bearer, value)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Attribute(utils.EventEmitter, Generic[_T]):
|
||||
class Permissions(enum.IntFlag):
|
||||
@@ -853,7 +820,7 @@ class Attribute(utils.EventEmitter, Generic[_T]):
|
||||
# The check for `p.name is not None` here is needed because for InFlag
|
||||
# enums, the .name property can be None, when the enum value is 0,
|
||||
# so the type hint for .name is Optional[str].
|
||||
enum_list: list[str] = [p.name for p in cls if p.name is not None]
|
||||
enum_list: List[str] = [p.name for p in cls if p.name is not None]
|
||||
enum_list_str = ",".join(enum_list)
|
||||
raise TypeError(
|
||||
f"Attribute::permissions error:\nExpected a string containing any of the keys, separated by commas: {enum_list_str}\nGot: {permissions_str}"
|
||||
@@ -872,13 +839,13 @@ class Attribute(utils.EventEmitter, Generic[_T]):
|
||||
EVENT_READ = "read"
|
||||
EVENT_WRITE = "write"
|
||||
|
||||
value: AttributeValue[_T] | _T | None
|
||||
value: Union[AttributeValue[_T], _T, None]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
attribute_type: str | bytes | UUID,
|
||||
permissions: str | Attribute.Permissions,
|
||||
value: AttributeValue[_T] | _T | None = None,
|
||||
attribute_type: Union[str, bytes, UUID],
|
||||
permissions: Union[str, Attribute.Permissions],
|
||||
value: Union[AttributeValue[_T], _T, None] = None,
|
||||
) -> None:
|
||||
utils.EventEmitter.__init__(self)
|
||||
self.handle = 0
|
||||
@@ -904,8 +871,7 @@ class Attribute(utils.EventEmitter, Generic[_T]):
|
||||
def decode_value(self, value: bytes) -> _T:
|
||||
return value # type: ignore
|
||||
|
||||
async def read_value(self, bearer: Bearer) -> bytes:
|
||||
connection = bearer.connection if is_enhanced_bearer(bearer) else bearer
|
||||
async def read_value(self, connection: Optional[Connection]) -> bytes:
|
||||
if (
|
||||
(self.permissions & self.READ_REQUIRES_ENCRYPTION)
|
||||
and connection is not None
|
||||
@@ -928,7 +894,7 @@ class Attribute(utils.EventEmitter, Generic[_T]):
|
||||
error_code=ATT_INSUFFICIENT_AUTHORIZATION_ERROR, att_handle=self.handle
|
||||
)
|
||||
|
||||
value: _T | None
|
||||
value: Union[_T, None]
|
||||
if isinstance(self.value, AttributeValue):
|
||||
try:
|
||||
read_value = self.value.read(connection)
|
||||
@@ -940,17 +906,6 @@ class Attribute(utils.EventEmitter, Generic[_T]):
|
||||
raise ATT_Error(
|
||||
error_code=error.error_code, att_handle=self.handle
|
||||
) from error
|
||||
elif isinstance(self.value, AttributeValueV2):
|
||||
try:
|
||||
read_value = self.value.read(bearer)
|
||||
if inspect.isawaitable(read_value):
|
||||
value = await read_value
|
||||
else:
|
||||
value = read_value
|
||||
except ATT_Error as error:
|
||||
raise ATT_Error(
|
||||
error_code=error.error_code, att_handle=self.handle
|
||||
) from error
|
||||
else:
|
||||
value = self.value
|
||||
|
||||
@@ -958,8 +913,7 @@ class Attribute(utils.EventEmitter, Generic[_T]):
|
||||
|
||||
return b'' if value is None else self.encode_value(value)
|
||||
|
||||
async def write_value(self, bearer: Bearer, value: bytes) -> None:
|
||||
connection = bearer.connection if is_enhanced_bearer(bearer) else bearer
|
||||
async def write_value(self, connection: Optional[Connection], value: bytes) -> None:
|
||||
if (
|
||||
(self.permissions & self.WRITE_REQUIRES_ENCRYPTION)
|
||||
and connection is not None
|
||||
@@ -993,15 +947,6 @@ class Attribute(utils.EventEmitter, Generic[_T]):
|
||||
raise ATT_Error(
|
||||
error_code=error.error_code, att_handle=self.handle
|
||||
) from error
|
||||
elif isinstance(self.value, AttributeValueV2):
|
||||
try:
|
||||
result = self.value.write(bearer, decoded_value)
|
||||
if inspect.isawaitable(result):
|
||||
await result
|
||||
except ATT_Error as error:
|
||||
raise ATT_Error(
|
||||
error_code=error.error_code, att_handle=self.handle
|
||||
) from error
|
||||
else:
|
||||
self.value = decoded_value
|
||||
|
||||
|
||||
@@ -17,17 +17,20 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
import asyncio
|
||||
import concurrent.futures
|
||||
import abc
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import dataclasses
|
||||
import enum
|
||||
import logging
|
||||
import pathlib
|
||||
from typing import (
|
||||
AsyncGenerator,
|
||||
BinaryIO,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
import sys
|
||||
import wave
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import TYPE_CHECKING, BinaryIO
|
||||
|
||||
from bumble.colors import color
|
||||
|
||||
@@ -177,7 +180,7 @@ class ThreadedAudioOutput(AudioOutput):
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._thread_pool = concurrent.futures.ThreadPoolExecutor(1)
|
||||
self._thread_pool = ThreadPoolExecutor(1)
|
||||
self._pcm_samples: asyncio.Queue[bytes] = asyncio.Queue()
|
||||
self._write_task = asyncio.create_task(self._write_loop())
|
||||
|
||||
@@ -227,8 +230,8 @@ class SoundDeviceAudioOutput(ThreadedAudioOutput):
|
||||
|
||||
try:
|
||||
self._stream.write(pcm_samples)
|
||||
except Exception:
|
||||
logger.exception('Sound device error')
|
||||
except Exception as error:
|
||||
print(f'Sound device error: {error}')
|
||||
raise
|
||||
|
||||
def _close(self):
|
||||
@@ -406,7 +409,7 @@ class ThreadedAudioInput(AudioInput):
|
||||
"""Base class for AudioInput implementation where reading samples may block."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._thread_pool = concurrent.futures.ThreadPoolExecutor(1)
|
||||
self._thread_pool = ThreadPoolExecutor(1)
|
||||
self._pcm_samples: asyncio.Queue[bytes] = asyncio.Queue()
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -546,6 +549,5 @@ class SoundDeviceAudioInput(ThreadedAudioInput):
|
||||
return bytes(pcm_buffer)
|
||||
|
||||
def _close(self):
|
||||
if self._stream:
|
||||
self._stream.stop()
|
||||
self._stream = None
|
||||
self._stream.stop()
|
||||
self._stream = None
|
||||
|
||||
@@ -16,11 +16,12 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
import struct
|
||||
from typing import Dict, Type, Union, Tuple
|
||||
|
||||
from bumble import core, utils
|
||||
from bumble import core
|
||||
from bumble import utils
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -165,7 +166,7 @@ class Frame:
|
||||
|
||||
def to_bytes(
|
||||
self,
|
||||
ctype_or_response: CommandFrame.CommandType | ResponseFrame.ResponseCode,
|
||||
ctype_or_response: Union[CommandFrame.CommandType, ResponseFrame.ResponseCode],
|
||||
) -> bytes:
|
||||
# TODO: support extended subunit types and ids.
|
||||
return (
|
||||
@@ -212,11 +213,11 @@ class CommandFrame(Frame):
|
||||
NOTIFY = 0x03
|
||||
GENERAL_INQUIRY = 0x04
|
||||
|
||||
subclasses: dict[Frame.OperationCode, type[CommandFrame]] = {}
|
||||
subclasses: Dict[Frame.OperationCode, Type[CommandFrame]] = {}
|
||||
ctype: CommandType
|
||||
|
||||
@staticmethod
|
||||
def parse_operands(operands: bytes) -> tuple:
|
||||
def parse_operands(operands: bytes) -> Tuple:
|
||||
raise NotImplementedError
|
||||
|
||||
def __init__(
|
||||
@@ -250,11 +251,11 @@ class ResponseFrame(Frame):
|
||||
CHANGED = 0x0D
|
||||
INTERIM = 0x0F
|
||||
|
||||
subclasses: dict[Frame.OperationCode, type[ResponseFrame]] = {}
|
||||
subclasses: Dict[Frame.OperationCode, Type[ResponseFrame]] = {}
|
||||
response: ResponseCode
|
||||
|
||||
@staticmethod
|
||||
def parse_operands(operands: bytes) -> tuple:
|
||||
def parse_operands(operands: bytes) -> Tuple:
|
||||
raise NotImplementedError
|
||||
|
||||
def __init__(
|
||||
@@ -281,7 +282,7 @@ class VendorDependentFrame:
|
||||
vendor_dependent_data: bytes
|
||||
|
||||
@staticmethod
|
||||
def parse_operands(operands: bytes) -> tuple:
|
||||
def parse_operands(operands: bytes) -> Tuple:
|
||||
return (
|
||||
struct.unpack(">I", b"\x00" + operands[:3])[0],
|
||||
operands[3:],
|
||||
@@ -431,7 +432,7 @@ class PassThroughFrame:
|
||||
operation_data: bytes
|
||||
|
||||
@staticmethod
|
||||
def parse_operands(operands: bytes) -> tuple:
|
||||
def parse_operands(operands: bytes) -> Tuple:
|
||||
return (
|
||||
PassThroughFrame.StateFlag(operands[0] >> 7),
|
||||
PassThroughFrame.OperationId(operands[0] & 0x7F),
|
||||
|
||||
@@ -16,14 +16,15 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import IntEnum
|
||||
import logging
|
||||
import struct
|
||||
from collections.abc import Callable
|
||||
from enum import IntEnum
|
||||
from typing import Callable, cast, Dict, Optional
|
||||
|
||||
from bumble import core, l2cap
|
||||
from bumble.colors import color
|
||||
from bumble import avc
|
||||
from bumble import core
|
||||
from bumble import l2cap
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -136,18 +137,18 @@ class MessageAssembler:
|
||||
self.pid,
|
||||
self.payload,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception(color("!!! exception in callback", "red"))
|
||||
except Exception as error:
|
||||
logger.exception(color(f"!!! exception in callback: {error}", "red"))
|
||||
|
||||
self.reset()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Protocol:
|
||||
CommandHandler = Callable[[int, bytes], None]
|
||||
command_handlers: dict[int, CommandHandler] # Command handlers, by PID
|
||||
ResponseHandler = Callable[[int, bytes | None], None]
|
||||
response_handlers: dict[int, ResponseHandler] # Response handlers, by PID
|
||||
CommandHandler = Callable[[int, avc.CommandFrame], None]
|
||||
command_handlers: Dict[int, CommandHandler] # Command handlers, by PID
|
||||
ResponseHandler = Callable[[int, Optional[avc.ResponseFrame]], None]
|
||||
response_handlers: Dict[int, ResponseHandler] # Response handlers, by PID
|
||||
next_transaction_label: int
|
||||
message_assembler: MessageAssembler
|
||||
|
||||
@@ -204,15 +205,20 @@ class Protocol:
|
||||
self.send_ipid(transaction_label, pid)
|
||||
return
|
||||
|
||||
self.command_handlers[pid](transaction_label, payload)
|
||||
command_frame = cast(avc.CommandFrame, avc.Frame.from_bytes(payload))
|
||||
self.command_handlers[pid](transaction_label, command_frame)
|
||||
else:
|
||||
if pid not in self.response_handlers:
|
||||
logger.warning(f"no response handler for PID {pid}")
|
||||
return
|
||||
|
||||
# By convention, for an ipid, send a None payload to the response handler.
|
||||
response_payload = None if ipid else payload
|
||||
self.response_handlers[pid](transaction_label, response_payload)
|
||||
if ipid:
|
||||
response_frame = None
|
||||
else:
|
||||
response_frame = cast(avc.ResponseFrame, avc.Frame.from_bytes(payload))
|
||||
|
||||
self.response_handlers[pid](transaction_label, response_frame)
|
||||
|
||||
def send_message(
|
||||
self,
|
||||
@@ -257,7 +263,7 @@ class Protocol:
|
||||
|
||||
def send_ipid(self, transaction_label: int, pid: int) -> None:
|
||||
logger.debug(
|
||||
f">>> AVCTP ipid: transaction_label={transaction_label}, pid={pid}"
|
||||
">>> AVCTP ipid: " f"transaction_label={transaction_label}, " f"pid={pid}"
|
||||
)
|
||||
self.send_message(transaction_label, False, True, pid, b'')
|
||||
|
||||
|
||||
1210
bumble/avdtp.py
1210
bumble/avdtp.py
File diff suppressed because it is too large
Load Diff
1894
bumble/avrcp.py
1894
bumble/avrcp.py
File diff suppressed because it is too large
Load Diff
@@ -16,9 +16,7 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from bumble import core
|
||||
@@ -163,23 +161,23 @@ class AacAudioRtpPacket:
|
||||
cls, reader: BitReader, channel_configuration: int, audio_object_type: int
|
||||
) -> Self:
|
||||
# GASpecificConfig - ISO/EIC 14496-3 Table 4.1
|
||||
reader.read(1) # frame_length_flag
|
||||
frame_length_flag = reader.read(1)
|
||||
depends_on_core_coder = reader.read(1)
|
||||
if depends_on_core_coder:
|
||||
reader.read(14) # core_coder_delay
|
||||
core_coder_delay = reader.read(14)
|
||||
extension_flag = reader.read(1)
|
||||
if not channel_configuration:
|
||||
raise core.InvalidPacketError('program_config_element not supported')
|
||||
if audio_object_type in (6, 20):
|
||||
reader.read(3) # layer_nr
|
||||
layer_nr = reader.read(3)
|
||||
if extension_flag:
|
||||
if audio_object_type == 22:
|
||||
reader.read(5) # num_of_sub_frame
|
||||
reader.read(11) # layer_length
|
||||
num_of_sub_frame = reader.read(5)
|
||||
layer_length = reader.read(11)
|
||||
if audio_object_type in (17, 19, 20, 23):
|
||||
reader.read(1) # aac_section_data_resilience_flags
|
||||
reader.read(1) # aac_scale_factor_data_resilience_flags
|
||||
reader.read(1) # aac_spectral_data_resilience_flags
|
||||
aac_section_data_resilience_flags = reader.read(1)
|
||||
aac_scale_factor_data_resilience_flags = reader.read(1)
|
||||
aac_spectral_data_resilience_flags = reader.read(1)
|
||||
extension_flag_3 = reader.read(1)
|
||||
if extension_flag_3 == 1:
|
||||
raise core.InvalidPacketError('extensionFlag3 == 1 not supported')
|
||||
@@ -364,10 +362,10 @@ class AacAudioRtpPacket:
|
||||
if audio_mux_version_a != 0:
|
||||
raise core.InvalidPacketError('audioMuxVersionA != 0 not supported')
|
||||
if audio_mux_version == 1:
|
||||
AacAudioRtpPacket.read_latm_value(reader) # tara_buffer_fullness
|
||||
# stream_cnt = 0
|
||||
reader.read(1) # all_streams_same_time_framing
|
||||
reader.read(6) # num_sub_frames
|
||||
tara_buffer_fullness = AacAudioRtpPacket.read_latm_value(reader)
|
||||
stream_cnt = 0
|
||||
all_streams_same_time_framing = reader.read(1)
|
||||
num_sub_frames = reader.read(6)
|
||||
num_program = reader.read(4)
|
||||
if num_program != 0:
|
||||
raise core.InvalidPacketError('num_program != 0 not supported')
|
||||
@@ -391,9 +389,9 @@ class AacAudioRtpPacket:
|
||||
reader.skip(asc_len)
|
||||
frame_length_type = reader.read(3)
|
||||
if frame_length_type == 0:
|
||||
reader.read(8) # latm_buffer_fullness
|
||||
latm_buffer_fullness = reader.read(8)
|
||||
elif frame_length_type == 1:
|
||||
reader.read(9) # frame_length
|
||||
frame_length = reader.read(9)
|
||||
else:
|
||||
raise core.InvalidPacketError(
|
||||
f'frame_length_type {frame_length_type} not supported'
|
||||
@@ -413,7 +411,7 @@ class AacAudioRtpPacket:
|
||||
break
|
||||
crc_check_present = reader.read(1)
|
||||
if crc_check_present:
|
||||
reader.read(8) # crc_checksum
|
||||
crc_checksum = reader.read(8)
|
||||
|
||||
return cls(other_data_present, other_data_len_bits, audio_specific_config)
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
from functools import partial
|
||||
from typing import List, Optional, Union
|
||||
|
||||
|
||||
class ColorError(ValueError):
|
||||
@@ -37,7 +38,7 @@ STYLES = (
|
||||
)
|
||||
|
||||
|
||||
ColorSpec = str | int
|
||||
ColorSpec = Union[str, int]
|
||||
|
||||
|
||||
def _join(*values: ColorSpec) -> str:
|
||||
@@ -55,16 +56,16 @@ def _color_code(spec: ColorSpec, base: int) -> str:
|
||||
elif isinstance(spec, int) and 0 <= spec <= 255:
|
||||
return _join(base + 8, 5, spec)
|
||||
else:
|
||||
raise ColorError(f'Invalid color spec "{spec}"')
|
||||
raise ColorError('Invalid color spec "%s"' % spec)
|
||||
|
||||
|
||||
def color(
|
||||
s: str,
|
||||
fg: ColorSpec | None = None,
|
||||
bg: ColorSpec | None = None,
|
||||
style: str | None = None,
|
||||
fg: Optional[ColorSpec] = None,
|
||||
bg: Optional[ColorSpec] = None,
|
||||
style: Optional[str] = None,
|
||||
) -> str:
|
||||
codes: list[ColorSpec] = []
|
||||
codes: List[ColorSpec] = []
|
||||
|
||||
if fg:
|
||||
codes.append(_color_code(fg, 30))
|
||||
@@ -75,10 +76,10 @@ def color(
|
||||
if style_part in STYLES:
|
||||
codes.append(STYLES.index(style_part))
|
||||
else:
|
||||
raise ColorError(f'Invalid style "{style_part}"')
|
||||
raise ColorError('Invalid style "%s"' % style_part)
|
||||
|
||||
if codes:
|
||||
return f'\x1b[{_join(*codes)}m{s}\x1b[0m'
|
||||
return '\x1b[{0}m{1}\x1b[0m'.format(_join(*codes), s)
|
||||
else:
|
||||
return s
|
||||
|
||||
|
||||
2326
bumble/controller.py
2326
bumble/controller.py
File diff suppressed because it is too large
Load Diff
883
bumble/core.py
883
bumble/core.py
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
# Copyright 2021-2025 Google LLC
|
||||
# Copyright 2021-2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License")
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
@@ -12,6 +12,12 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Crypto support
|
||||
#
|
||||
# See Bluetooth spec Vol 3, Part H - 2.2 CRYPTOGRAPHIC TOOLBOX
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -19,17 +25,20 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import operator
|
||||
|
||||
import secrets
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
from cryptography.hazmat.primitives.asymmetric.ec import (
|
||||
generate_private_key,
|
||||
ECDH,
|
||||
EllipticCurvePrivateKey,
|
||||
EllipticCurvePublicNumbers,
|
||||
EllipticCurvePrivateNumbers,
|
||||
SECP256R1,
|
||||
)
|
||||
from cryptography.hazmat.primitives import cmac
|
||||
from typing import Tuple
|
||||
|
||||
try:
|
||||
from bumble.crypto.cryptography import EccKey, aes_cmac, e
|
||||
except ImportError:
|
||||
logging.getLogger(__name__).debug(
|
||||
"Unable to import cryptography, using built-in primitives."
|
||||
)
|
||||
from bumble.crypto.builtin import EccKey, aes_cmac, e # type: ignore[assignment]
|
||||
|
||||
_EccKey = EccKey # For the linter only
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -37,6 +46,55 @@ _EccKey = EccKey # For the linter only
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Classes
|
||||
# -----------------------------------------------------------------------------
|
||||
class EccKey:
|
||||
def __init__(self, private_key: EllipticCurvePrivateKey) -> None:
|
||||
self.private_key = private_key
|
||||
|
||||
@classmethod
|
||||
def generate(cls) -> EccKey:
|
||||
private_key = generate_private_key(SECP256R1())
|
||||
return cls(private_key)
|
||||
|
||||
@classmethod
|
||||
def from_private_key_bytes(
|
||||
cls, d_bytes: bytes, x_bytes: bytes, y_bytes: bytes
|
||||
) -> EccKey:
|
||||
d = int.from_bytes(d_bytes, byteorder='big', signed=False)
|
||||
x = int.from_bytes(x_bytes, byteorder='big', signed=False)
|
||||
y = int.from_bytes(y_bytes, byteorder='big', signed=False)
|
||||
private_key = EllipticCurvePrivateNumbers(
|
||||
d, EllipticCurvePublicNumbers(x, y, SECP256R1())
|
||||
).private_key()
|
||||
return cls(private_key)
|
||||
|
||||
@property
|
||||
def x(self) -> bytes:
|
||||
return (
|
||||
self.private_key.public_key()
|
||||
.public_numbers()
|
||||
.x.to_bytes(32, byteorder='big')
|
||||
)
|
||||
|
||||
@property
|
||||
def y(self) -> bytes:
|
||||
return (
|
||||
self.private_key.public_key()
|
||||
.public_numbers()
|
||||
.y.to_bytes(32, byteorder='big')
|
||||
)
|
||||
|
||||
def dh(self, public_key_x: bytes, public_key_y: bytes) -> bytes:
|
||||
x = int.from_bytes(public_key_x, byteorder='big', signed=False)
|
||||
y = int.from_bytes(public_key_y, byteorder='big', signed=False)
|
||||
public_key = EllipticCurvePublicNumbers(x, y, SECP256R1()).public_key()
|
||||
shared_key = self.private_key.exchange(ECDH(), public_key)
|
||||
|
||||
return shared_key
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Functions
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -74,6 +132,19 @@ def r() -> bytes:
|
||||
return secrets.token_bytes(16)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def e(key: bytes, data: bytes) -> bytes:
|
||||
'''
|
||||
AES-128 ECB, expecting byte-swapped inputs and producing a byte-swapped output.
|
||||
|
||||
See Bluetooth spec Vol 3, Part H - 2.2.1 Security function e
|
||||
'''
|
||||
|
||||
cipher = Cipher(algorithms.AES(reverse(key)), modes.ECB())
|
||||
encryptor = cipher.encryptor()
|
||||
return reverse(encryptor.update(reverse(data)))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def ah(k: bytes, r: bytes) -> bytes: # pylint: disable=redefined-outer-name
|
||||
'''
|
||||
@@ -116,6 +187,18 @@ def s1(k: bytes, r1: bytes, r2: bytes) -> bytes:
|
||||
return e(k, r2[0:8] + r1[0:8])
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def aes_cmac(m: bytes, k: bytes) -> bytes:
|
||||
'''
|
||||
See Bluetooth spec, Vol 3, Part H - 2.2.5 FunctionAES-CMAC
|
||||
|
||||
NOTE: the input and output of this internal function are in big-endian byte order
|
||||
'''
|
||||
mac = cmac.CMAC(algorithms.AES(k))
|
||||
mac.update(m)
|
||||
return mac.finalize()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def f4(u: bytes, v: bytes, x: bytes, z: bytes) -> bytes:
|
||||
'''
|
||||
@@ -126,7 +209,7 @@ def f4(u: bytes, v: bytes, x: bytes, z: bytes) -> bytes:
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def f5(w: bytes, n1: bytes, n2: bytes, a1: bytes, a2: bytes) -> tuple[bytes, bytes]:
|
||||
def f5(w: bytes, n1: bytes, n2: bytes, a1: bytes, a2: bytes) -> Tuple[bytes, bytes]:
|
||||
'''
|
||||
See Bluetooth spec, Vol 3, Part H - 2.2.7 LE Secure Connections Key Generation
|
||||
Function f5
|
||||
@@ -1,646 +0,0 @@
|
||||
# Copyright 2021-2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License")
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# The implementation is modified from:
|
||||
# * AES - https://github.com/ricmoo/pyaes by Richard Moore under MIT License
|
||||
# * CMAC - https://github.com/pycrypto/pycrypto by contributors under pycrypto License.
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Built-in implementation of cryptography primitives.
|
||||
#
|
||||
# Note: It's very dangerous to use this library in the real world.
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import dataclasses
|
||||
import functools
|
||||
import secrets
|
||||
import struct
|
||||
|
||||
from bumble import core
|
||||
|
||||
|
||||
def _compact_word(word: bytes) -> int:
|
||||
return int.from_bytes(word, "big")
|
||||
|
||||
|
||||
def _shift_bytes(bs: bytes, xor_lsb: int = 0) -> bytes:
|
||||
return ((int.from_bytes(bs, "big") << 1) ^ xor_lsb).to_bytes(len(bs) + 1, "big")[1:]
|
||||
|
||||
|
||||
def _xor(a: bytes, b: bytes) -> bytes:
|
||||
return bytes(x ^ y for x, y in zip(a, b))
|
||||
|
||||
|
||||
# Based *largely* on the Rijndael implementation
|
||||
# See: http://csrc.nist.gov/publications/FIPS/FIPS197/FIPS-197.pdf
|
||||
class _AES:
|
||||
'''Encapsulates the AES block cipher.
|
||||
|
||||
You generally should not need this. Use the AESModeOfOperation classes
|
||||
below instead.'''
|
||||
|
||||
# fmt: off
|
||||
# Number of rounds by key size
|
||||
_NUMBER_OF_ROUNDS = {16: 10, 24: 12, 32: 14}
|
||||
|
||||
# Round constant words
|
||||
_RCON = [ 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91 ]
|
||||
|
||||
# S-box and Inverse S-box (S is for Substitution)
|
||||
_S = [ 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16 ]
|
||||
_S_INV =[ 0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb, 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb, 0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e, 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25, 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92, 0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84, 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06, 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b, 0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73, 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e, 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b, 0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4, 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f, 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef, 0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61, 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d ]
|
||||
|
||||
# Transformations for encryption
|
||||
_T1 = [ 0xc66363a5, 0xf87c7c84, 0xee777799, 0xf67b7b8d, 0xfff2f20d, 0xd66b6bbd, 0xde6f6fb1, 0x91c5c554, 0x60303050, 0x02010103, 0xce6767a9, 0x562b2b7d, 0xe7fefe19, 0xb5d7d762, 0x4dababe6, 0xec76769a, 0x8fcaca45, 0x1f82829d, 0x89c9c940, 0xfa7d7d87, 0xeffafa15, 0xb25959eb, 0x8e4747c9, 0xfbf0f00b, 0x41adadec, 0xb3d4d467, 0x5fa2a2fd, 0x45afafea, 0x239c9cbf, 0x53a4a4f7, 0xe4727296, 0x9bc0c05b, 0x75b7b7c2, 0xe1fdfd1c, 0x3d9393ae, 0x4c26266a, 0x6c36365a, 0x7e3f3f41, 0xf5f7f702, 0x83cccc4f, 0x6834345c, 0x51a5a5f4, 0xd1e5e534, 0xf9f1f108, 0xe2717193, 0xabd8d873, 0x62313153, 0x2a15153f, 0x0804040c, 0x95c7c752, 0x46232365, 0x9dc3c35e, 0x30181828, 0x379696a1, 0x0a05050f, 0x2f9a9ab5, 0x0e070709, 0x24121236, 0x1b80809b, 0xdfe2e23d, 0xcdebeb26, 0x4e272769, 0x7fb2b2cd, 0xea75759f, 0x1209091b, 0x1d83839e, 0x582c2c74, 0x341a1a2e, 0x361b1b2d, 0xdc6e6eb2, 0xb45a5aee, 0x5ba0a0fb, 0xa45252f6, 0x763b3b4d, 0xb7d6d661, 0x7db3b3ce, 0x5229297b, 0xdde3e33e, 0x5e2f2f71, 0x13848497, 0xa65353f5, 0xb9d1d168, 0x00000000, 0xc1eded2c, 0x40202060, 0xe3fcfc1f, 0x79b1b1c8, 0xb65b5bed, 0xd46a6abe, 0x8dcbcb46, 0x67bebed9, 0x7239394b, 0x944a4ade, 0x984c4cd4, 0xb05858e8, 0x85cfcf4a, 0xbbd0d06b, 0xc5efef2a, 0x4faaaae5, 0xedfbfb16, 0x864343c5, 0x9a4d4dd7, 0x66333355, 0x11858594, 0x8a4545cf, 0xe9f9f910, 0x04020206, 0xfe7f7f81, 0xa05050f0, 0x783c3c44, 0x259f9fba, 0x4ba8a8e3, 0xa25151f3, 0x5da3a3fe, 0x804040c0, 0x058f8f8a, 0x3f9292ad, 0x219d9dbc, 0x70383848, 0xf1f5f504, 0x63bcbcdf, 0x77b6b6c1, 0xafdada75, 0x42212163, 0x20101030, 0xe5ffff1a, 0xfdf3f30e, 0xbfd2d26d, 0x81cdcd4c, 0x180c0c14, 0x26131335, 0xc3ecec2f, 0xbe5f5fe1, 0x359797a2, 0x884444cc, 0x2e171739, 0x93c4c457, 0x55a7a7f2, 0xfc7e7e82, 0x7a3d3d47, 0xc86464ac, 0xba5d5de7, 0x3219192b, 0xe6737395, 0xc06060a0, 0x19818198, 0x9e4f4fd1, 0xa3dcdc7f, 0x44222266, 0x542a2a7e, 0x3b9090ab, 0x0b888883, 0x8c4646ca, 0xc7eeee29, 0x6bb8b8d3, 0x2814143c, 0xa7dede79, 0xbc5e5ee2, 0x160b0b1d, 0xaddbdb76, 0xdbe0e03b, 0x64323256, 0x743a3a4e, 0x140a0a1e, 0x924949db, 0x0c06060a, 0x4824246c, 0xb85c5ce4, 0x9fc2c25d, 0xbdd3d36e, 0x43acacef, 0xc46262a6, 0x399191a8, 0x319595a4, 0xd3e4e437, 0xf279798b, 0xd5e7e732, 0x8bc8c843, 0x6e373759, 0xda6d6db7, 0x018d8d8c, 0xb1d5d564, 0x9c4e4ed2, 0x49a9a9e0, 0xd86c6cb4, 0xac5656fa, 0xf3f4f407, 0xcfeaea25, 0xca6565af, 0xf47a7a8e, 0x47aeaee9, 0x10080818, 0x6fbabad5, 0xf0787888, 0x4a25256f, 0x5c2e2e72, 0x381c1c24, 0x57a6a6f1, 0x73b4b4c7, 0x97c6c651, 0xcbe8e823, 0xa1dddd7c, 0xe874749c, 0x3e1f1f21, 0x964b4bdd, 0x61bdbddc, 0x0d8b8b86, 0x0f8a8a85, 0xe0707090, 0x7c3e3e42, 0x71b5b5c4, 0xcc6666aa, 0x904848d8, 0x06030305, 0xf7f6f601, 0x1c0e0e12, 0xc26161a3, 0x6a35355f, 0xae5757f9, 0x69b9b9d0, 0x17868691, 0x99c1c158, 0x3a1d1d27, 0x279e9eb9, 0xd9e1e138, 0xebf8f813, 0x2b9898b3, 0x22111133, 0xd26969bb, 0xa9d9d970, 0x078e8e89, 0x339494a7, 0x2d9b9bb6, 0x3c1e1e22, 0x15878792, 0xc9e9e920, 0x87cece49, 0xaa5555ff, 0x50282878, 0xa5dfdf7a, 0x038c8c8f, 0x59a1a1f8, 0x09898980, 0x1a0d0d17, 0x65bfbfda, 0xd7e6e631, 0x844242c6, 0xd06868b8, 0x824141c3, 0x299999b0, 0x5a2d2d77, 0x1e0f0f11, 0x7bb0b0cb, 0xa85454fc, 0x6dbbbbd6, 0x2c16163a ]
|
||||
_T2 = [ 0xa5c66363, 0x84f87c7c, 0x99ee7777, 0x8df67b7b, 0x0dfff2f2, 0xbdd66b6b, 0xb1de6f6f, 0x5491c5c5, 0x50603030, 0x03020101, 0xa9ce6767, 0x7d562b2b, 0x19e7fefe, 0x62b5d7d7, 0xe64dabab, 0x9aec7676, 0x458fcaca, 0x9d1f8282, 0x4089c9c9, 0x87fa7d7d, 0x15effafa, 0xebb25959, 0xc98e4747, 0x0bfbf0f0, 0xec41adad, 0x67b3d4d4, 0xfd5fa2a2, 0xea45afaf, 0xbf239c9c, 0xf753a4a4, 0x96e47272, 0x5b9bc0c0, 0xc275b7b7, 0x1ce1fdfd, 0xae3d9393, 0x6a4c2626, 0x5a6c3636, 0x417e3f3f, 0x02f5f7f7, 0x4f83cccc, 0x5c683434, 0xf451a5a5, 0x34d1e5e5, 0x08f9f1f1, 0x93e27171, 0x73abd8d8, 0x53623131, 0x3f2a1515, 0x0c080404, 0x5295c7c7, 0x65462323, 0x5e9dc3c3, 0x28301818, 0xa1379696, 0x0f0a0505, 0xb52f9a9a, 0x090e0707, 0x36241212, 0x9b1b8080, 0x3ddfe2e2, 0x26cdebeb, 0x694e2727, 0xcd7fb2b2, 0x9fea7575, 0x1b120909, 0x9e1d8383, 0x74582c2c, 0x2e341a1a, 0x2d361b1b, 0xb2dc6e6e, 0xeeb45a5a, 0xfb5ba0a0, 0xf6a45252, 0x4d763b3b, 0x61b7d6d6, 0xce7db3b3, 0x7b522929, 0x3edde3e3, 0x715e2f2f, 0x97138484, 0xf5a65353, 0x68b9d1d1, 0x00000000, 0x2cc1eded, 0x60402020, 0x1fe3fcfc, 0xc879b1b1, 0xedb65b5b, 0xbed46a6a, 0x468dcbcb, 0xd967bebe, 0x4b723939, 0xde944a4a, 0xd4984c4c, 0xe8b05858, 0x4a85cfcf, 0x6bbbd0d0, 0x2ac5efef, 0xe54faaaa, 0x16edfbfb, 0xc5864343, 0xd79a4d4d, 0x55663333, 0x94118585, 0xcf8a4545, 0x10e9f9f9, 0x06040202, 0x81fe7f7f, 0xf0a05050, 0x44783c3c, 0xba259f9f, 0xe34ba8a8, 0xf3a25151, 0xfe5da3a3, 0xc0804040, 0x8a058f8f, 0xad3f9292, 0xbc219d9d, 0x48703838, 0x04f1f5f5, 0xdf63bcbc, 0xc177b6b6, 0x75afdada, 0x63422121, 0x30201010, 0x1ae5ffff, 0x0efdf3f3, 0x6dbfd2d2, 0x4c81cdcd, 0x14180c0c, 0x35261313, 0x2fc3ecec, 0xe1be5f5f, 0xa2359797, 0xcc884444, 0x392e1717, 0x5793c4c4, 0xf255a7a7, 0x82fc7e7e, 0x477a3d3d, 0xacc86464, 0xe7ba5d5d, 0x2b321919, 0x95e67373, 0xa0c06060, 0x98198181, 0xd19e4f4f, 0x7fa3dcdc, 0x66442222, 0x7e542a2a, 0xab3b9090, 0x830b8888, 0xca8c4646, 0x29c7eeee, 0xd36bb8b8, 0x3c281414, 0x79a7dede, 0xe2bc5e5e, 0x1d160b0b, 0x76addbdb, 0x3bdbe0e0, 0x56643232, 0x4e743a3a, 0x1e140a0a, 0xdb924949, 0x0a0c0606, 0x6c482424, 0xe4b85c5c, 0x5d9fc2c2, 0x6ebdd3d3, 0xef43acac, 0xa6c46262, 0xa8399191, 0xa4319595, 0x37d3e4e4, 0x8bf27979, 0x32d5e7e7, 0x438bc8c8, 0x596e3737, 0xb7da6d6d, 0x8c018d8d, 0x64b1d5d5, 0xd29c4e4e, 0xe049a9a9, 0xb4d86c6c, 0xfaac5656, 0x07f3f4f4, 0x25cfeaea, 0xafca6565, 0x8ef47a7a, 0xe947aeae, 0x18100808, 0xd56fbaba, 0x88f07878, 0x6f4a2525, 0x725c2e2e, 0x24381c1c, 0xf157a6a6, 0xc773b4b4, 0x5197c6c6, 0x23cbe8e8, 0x7ca1dddd, 0x9ce87474, 0x213e1f1f, 0xdd964b4b, 0xdc61bdbd, 0x860d8b8b, 0x850f8a8a, 0x90e07070, 0x427c3e3e, 0xc471b5b5, 0xaacc6666, 0xd8904848, 0x05060303, 0x01f7f6f6, 0x121c0e0e, 0xa3c26161, 0x5f6a3535, 0xf9ae5757, 0xd069b9b9, 0x91178686, 0x5899c1c1, 0x273a1d1d, 0xb9279e9e, 0x38d9e1e1, 0x13ebf8f8, 0xb32b9898, 0x33221111, 0xbbd26969, 0x70a9d9d9, 0x89078e8e, 0xa7339494, 0xb62d9b9b, 0x223c1e1e, 0x92158787, 0x20c9e9e9, 0x4987cece, 0xffaa5555, 0x78502828, 0x7aa5dfdf, 0x8f038c8c, 0xf859a1a1, 0x80098989, 0x171a0d0d, 0xda65bfbf, 0x31d7e6e6, 0xc6844242, 0xb8d06868, 0xc3824141, 0xb0299999, 0x775a2d2d, 0x111e0f0f, 0xcb7bb0b0, 0xfca85454, 0xd66dbbbb, 0x3a2c1616 ]
|
||||
_T3 = [ 0x63a5c663, 0x7c84f87c, 0x7799ee77, 0x7b8df67b, 0xf20dfff2, 0x6bbdd66b, 0x6fb1de6f, 0xc55491c5, 0x30506030, 0x01030201, 0x67a9ce67, 0x2b7d562b, 0xfe19e7fe, 0xd762b5d7, 0xabe64dab, 0x769aec76, 0xca458fca, 0x829d1f82, 0xc94089c9, 0x7d87fa7d, 0xfa15effa, 0x59ebb259, 0x47c98e47, 0xf00bfbf0, 0xadec41ad, 0xd467b3d4, 0xa2fd5fa2, 0xafea45af, 0x9cbf239c, 0xa4f753a4, 0x7296e472, 0xc05b9bc0, 0xb7c275b7, 0xfd1ce1fd, 0x93ae3d93, 0x266a4c26, 0x365a6c36, 0x3f417e3f, 0xf702f5f7, 0xcc4f83cc, 0x345c6834, 0xa5f451a5, 0xe534d1e5, 0xf108f9f1, 0x7193e271, 0xd873abd8, 0x31536231, 0x153f2a15, 0x040c0804, 0xc75295c7, 0x23654623, 0xc35e9dc3, 0x18283018, 0x96a13796, 0x050f0a05, 0x9ab52f9a, 0x07090e07, 0x12362412, 0x809b1b80, 0xe23ddfe2, 0xeb26cdeb, 0x27694e27, 0xb2cd7fb2, 0x759fea75, 0x091b1209, 0x839e1d83, 0x2c74582c, 0x1a2e341a, 0x1b2d361b, 0x6eb2dc6e, 0x5aeeb45a, 0xa0fb5ba0, 0x52f6a452, 0x3b4d763b, 0xd661b7d6, 0xb3ce7db3, 0x297b5229, 0xe33edde3, 0x2f715e2f, 0x84971384, 0x53f5a653, 0xd168b9d1, 0x00000000, 0xed2cc1ed, 0x20604020, 0xfc1fe3fc, 0xb1c879b1, 0x5bedb65b, 0x6abed46a, 0xcb468dcb, 0xbed967be, 0x394b7239, 0x4ade944a, 0x4cd4984c, 0x58e8b058, 0xcf4a85cf, 0xd06bbbd0, 0xef2ac5ef, 0xaae54faa, 0xfb16edfb, 0x43c58643, 0x4dd79a4d, 0x33556633, 0x85941185, 0x45cf8a45, 0xf910e9f9, 0x02060402, 0x7f81fe7f, 0x50f0a050, 0x3c44783c, 0x9fba259f, 0xa8e34ba8, 0x51f3a251, 0xa3fe5da3, 0x40c08040, 0x8f8a058f, 0x92ad3f92, 0x9dbc219d, 0x38487038, 0xf504f1f5, 0xbcdf63bc, 0xb6c177b6, 0xda75afda, 0x21634221, 0x10302010, 0xff1ae5ff, 0xf30efdf3, 0xd26dbfd2, 0xcd4c81cd, 0x0c14180c, 0x13352613, 0xec2fc3ec, 0x5fe1be5f, 0x97a23597, 0x44cc8844, 0x17392e17, 0xc45793c4, 0xa7f255a7, 0x7e82fc7e, 0x3d477a3d, 0x64acc864, 0x5de7ba5d, 0x192b3219, 0x7395e673, 0x60a0c060, 0x81981981, 0x4fd19e4f, 0xdc7fa3dc, 0x22664422, 0x2a7e542a, 0x90ab3b90, 0x88830b88, 0x46ca8c46, 0xee29c7ee, 0xb8d36bb8, 0x143c2814, 0xde79a7de, 0x5ee2bc5e, 0x0b1d160b, 0xdb76addb, 0xe03bdbe0, 0x32566432, 0x3a4e743a, 0x0a1e140a, 0x49db9249, 0x060a0c06, 0x246c4824, 0x5ce4b85c, 0xc25d9fc2, 0xd36ebdd3, 0xacef43ac, 0x62a6c462, 0x91a83991, 0x95a43195, 0xe437d3e4, 0x798bf279, 0xe732d5e7, 0xc8438bc8, 0x37596e37, 0x6db7da6d, 0x8d8c018d, 0xd564b1d5, 0x4ed29c4e, 0xa9e049a9, 0x6cb4d86c, 0x56faac56, 0xf407f3f4, 0xea25cfea, 0x65afca65, 0x7a8ef47a, 0xaee947ae, 0x08181008, 0xbad56fba, 0x7888f078, 0x256f4a25, 0x2e725c2e, 0x1c24381c, 0xa6f157a6, 0xb4c773b4, 0xc65197c6, 0xe823cbe8, 0xdd7ca1dd, 0x749ce874, 0x1f213e1f, 0x4bdd964b, 0xbddc61bd, 0x8b860d8b, 0x8a850f8a, 0x7090e070, 0x3e427c3e, 0xb5c471b5, 0x66aacc66, 0x48d89048, 0x03050603, 0xf601f7f6, 0x0e121c0e, 0x61a3c261, 0x355f6a35, 0x57f9ae57, 0xb9d069b9, 0x86911786, 0xc15899c1, 0x1d273a1d, 0x9eb9279e, 0xe138d9e1, 0xf813ebf8, 0x98b32b98, 0x11332211, 0x69bbd269, 0xd970a9d9, 0x8e89078e, 0x94a73394, 0x9bb62d9b, 0x1e223c1e, 0x87921587, 0xe920c9e9, 0xce4987ce, 0x55ffaa55, 0x28785028, 0xdf7aa5df, 0x8c8f038c, 0xa1f859a1, 0x89800989, 0x0d171a0d, 0xbfda65bf, 0xe631d7e6, 0x42c68442, 0x68b8d068, 0x41c38241, 0x99b02999, 0x2d775a2d, 0x0f111e0f, 0xb0cb7bb0, 0x54fca854, 0xbbd66dbb, 0x163a2c16 ]
|
||||
_T4 = [ 0x6363a5c6, 0x7c7c84f8, 0x777799ee, 0x7b7b8df6, 0xf2f20dff, 0x6b6bbdd6, 0x6f6fb1de, 0xc5c55491, 0x30305060, 0x01010302, 0x6767a9ce, 0x2b2b7d56, 0xfefe19e7, 0xd7d762b5, 0xababe64d, 0x76769aec, 0xcaca458f, 0x82829d1f, 0xc9c94089, 0x7d7d87fa, 0xfafa15ef, 0x5959ebb2, 0x4747c98e, 0xf0f00bfb, 0xadadec41, 0xd4d467b3, 0xa2a2fd5f, 0xafafea45, 0x9c9cbf23, 0xa4a4f753, 0x727296e4, 0xc0c05b9b, 0xb7b7c275, 0xfdfd1ce1, 0x9393ae3d, 0x26266a4c, 0x36365a6c, 0x3f3f417e, 0xf7f702f5, 0xcccc4f83, 0x34345c68, 0xa5a5f451, 0xe5e534d1, 0xf1f108f9, 0x717193e2, 0xd8d873ab, 0x31315362, 0x15153f2a, 0x04040c08, 0xc7c75295, 0x23236546, 0xc3c35e9d, 0x18182830, 0x9696a137, 0x05050f0a, 0x9a9ab52f, 0x0707090e, 0x12123624, 0x80809b1b, 0xe2e23ddf, 0xebeb26cd, 0x2727694e, 0xb2b2cd7f, 0x75759fea, 0x09091b12, 0x83839e1d, 0x2c2c7458, 0x1a1a2e34, 0x1b1b2d36, 0x6e6eb2dc, 0x5a5aeeb4, 0xa0a0fb5b, 0x5252f6a4, 0x3b3b4d76, 0xd6d661b7, 0xb3b3ce7d, 0x29297b52, 0xe3e33edd, 0x2f2f715e, 0x84849713, 0x5353f5a6, 0xd1d168b9, 0x00000000, 0xeded2cc1, 0x20206040, 0xfcfc1fe3, 0xb1b1c879, 0x5b5bedb6, 0x6a6abed4, 0xcbcb468d, 0xbebed967, 0x39394b72, 0x4a4ade94, 0x4c4cd498, 0x5858e8b0, 0xcfcf4a85, 0xd0d06bbb, 0xefef2ac5, 0xaaaae54f, 0xfbfb16ed, 0x4343c586, 0x4d4dd79a, 0x33335566, 0x85859411, 0x4545cf8a, 0xf9f910e9, 0x02020604, 0x7f7f81fe, 0x5050f0a0, 0x3c3c4478, 0x9f9fba25, 0xa8a8e34b, 0x5151f3a2, 0xa3a3fe5d, 0x4040c080, 0x8f8f8a05, 0x9292ad3f, 0x9d9dbc21, 0x38384870, 0xf5f504f1, 0xbcbcdf63, 0xb6b6c177, 0xdada75af, 0x21216342, 0x10103020, 0xffff1ae5, 0xf3f30efd, 0xd2d26dbf, 0xcdcd4c81, 0x0c0c1418, 0x13133526, 0xecec2fc3, 0x5f5fe1be, 0x9797a235, 0x4444cc88, 0x1717392e, 0xc4c45793, 0xa7a7f255, 0x7e7e82fc, 0x3d3d477a, 0x6464acc8, 0x5d5de7ba, 0x19192b32, 0x737395e6, 0x6060a0c0, 0x81819819, 0x4f4fd19e, 0xdcdc7fa3, 0x22226644, 0x2a2a7e54, 0x9090ab3b, 0x8888830b, 0x4646ca8c, 0xeeee29c7, 0xb8b8d36b, 0x14143c28, 0xdede79a7, 0x5e5ee2bc, 0x0b0b1d16, 0xdbdb76ad, 0xe0e03bdb, 0x32325664, 0x3a3a4e74, 0x0a0a1e14, 0x4949db92, 0x06060a0c, 0x24246c48, 0x5c5ce4b8, 0xc2c25d9f, 0xd3d36ebd, 0xacacef43, 0x6262a6c4, 0x9191a839, 0x9595a431, 0xe4e437d3, 0x79798bf2, 0xe7e732d5, 0xc8c8438b, 0x3737596e, 0x6d6db7da, 0x8d8d8c01, 0xd5d564b1, 0x4e4ed29c, 0xa9a9e049, 0x6c6cb4d8, 0x5656faac, 0xf4f407f3, 0xeaea25cf, 0x6565afca, 0x7a7a8ef4, 0xaeaee947, 0x08081810, 0xbabad56f, 0x787888f0, 0x25256f4a, 0x2e2e725c, 0x1c1c2438, 0xa6a6f157, 0xb4b4c773, 0xc6c65197, 0xe8e823cb, 0xdddd7ca1, 0x74749ce8, 0x1f1f213e, 0x4b4bdd96, 0xbdbddc61, 0x8b8b860d, 0x8a8a850f, 0x707090e0, 0x3e3e427c, 0xb5b5c471, 0x6666aacc, 0x4848d890, 0x03030506, 0xf6f601f7, 0x0e0e121c, 0x6161a3c2, 0x35355f6a, 0x5757f9ae, 0xb9b9d069, 0x86869117, 0xc1c15899, 0x1d1d273a, 0x9e9eb927, 0xe1e138d9, 0xf8f813eb, 0x9898b32b, 0x11113322, 0x6969bbd2, 0xd9d970a9, 0x8e8e8907, 0x9494a733, 0x9b9bb62d, 0x1e1e223c, 0x87879215, 0xe9e920c9, 0xcece4987, 0x5555ffaa, 0x28287850, 0xdfdf7aa5, 0x8c8c8f03, 0xa1a1f859, 0x89898009, 0x0d0d171a, 0xbfbfda65, 0xe6e631d7, 0x4242c684, 0x6868b8d0, 0x4141c382, 0x9999b029, 0x2d2d775a, 0x0f0f111e, 0xb0b0cb7b, 0x5454fca8, 0xbbbbd66d, 0x16163a2c ]
|
||||
|
||||
# Transformations for decryption
|
||||
_T5 = [ 0x51f4a750, 0x7e416553, 0x1a17a4c3, 0x3a275e96, 0x3bab6bcb, 0x1f9d45f1, 0xacfa58ab, 0x4be30393, 0x2030fa55, 0xad766df6, 0x88cc7691, 0xf5024c25, 0x4fe5d7fc, 0xc52acbd7, 0x26354480, 0xb562a38f, 0xdeb15a49, 0x25ba1b67, 0x45ea0e98, 0x5dfec0e1, 0xc32f7502, 0x814cf012, 0x8d4697a3, 0x6bd3f9c6, 0x038f5fe7, 0x15929c95, 0xbf6d7aeb, 0x955259da, 0xd4be832d, 0x587421d3, 0x49e06929, 0x8ec9c844, 0x75c2896a, 0xf48e7978, 0x99583e6b, 0x27b971dd, 0xbee14fb6, 0xf088ad17, 0xc920ac66, 0x7dce3ab4, 0x63df4a18, 0xe51a3182, 0x97513360, 0x62537f45, 0xb16477e0, 0xbb6bae84, 0xfe81a01c, 0xf9082b94, 0x70486858, 0x8f45fd19, 0x94de6c87, 0x527bf8b7, 0xab73d323, 0x724b02e2, 0xe31f8f57, 0x6655ab2a, 0xb2eb2807, 0x2fb5c203, 0x86c57b9a, 0xd33708a5, 0x302887f2, 0x23bfa5b2, 0x02036aba, 0xed16825c, 0x8acf1c2b, 0xa779b492, 0xf307f2f0, 0x4e69e2a1, 0x65daf4cd, 0x0605bed5, 0xd134621f, 0xc4a6fe8a, 0x342e539d, 0xa2f355a0, 0x058ae132, 0xa4f6eb75, 0x0b83ec39, 0x4060efaa, 0x5e719f06, 0xbd6e1051, 0x3e218af9, 0x96dd063d, 0xdd3e05ae, 0x4de6bd46, 0x91548db5, 0x71c45d05, 0x0406d46f, 0x605015ff, 0x1998fb24, 0xd6bde997, 0x894043cc, 0x67d99e77, 0xb0e842bd, 0x07898b88, 0xe7195b38, 0x79c8eedb, 0xa17c0a47, 0x7c420fe9, 0xf8841ec9, 0x00000000, 0x09808683, 0x322bed48, 0x1e1170ac, 0x6c5a724e, 0xfd0efffb, 0x0f853856, 0x3daed51e, 0x362d3927, 0x0a0fd964, 0x685ca621, 0x9b5b54d1, 0x24362e3a, 0x0c0a67b1, 0x9357e70f, 0xb4ee96d2, 0x1b9b919e, 0x80c0c54f, 0x61dc20a2, 0x5a774b69, 0x1c121a16, 0xe293ba0a, 0xc0a02ae5, 0x3c22e043, 0x121b171d, 0x0e090d0b, 0xf28bc7ad, 0x2db6a8b9, 0x141ea9c8, 0x57f11985, 0xaf75074c, 0xee99ddbb, 0xa37f60fd, 0xf701269f, 0x5c72f5bc, 0x44663bc5, 0x5bfb7e34, 0x8b432976, 0xcb23c6dc, 0xb6edfc68, 0xb8e4f163, 0xd731dcca, 0x42638510, 0x13972240, 0x84c61120, 0x854a247d, 0xd2bb3df8, 0xaef93211, 0xc729a16d, 0x1d9e2f4b, 0xdcb230f3, 0x0d8652ec, 0x77c1e3d0, 0x2bb3166c, 0xa970b999, 0x119448fa, 0x47e96422, 0xa8fc8cc4, 0xa0f03f1a, 0x567d2cd8, 0x223390ef, 0x87494ec7, 0xd938d1c1, 0x8ccaa2fe, 0x98d40b36, 0xa6f581cf, 0xa57ade28, 0xdab78e26, 0x3fadbfa4, 0x2c3a9de4, 0x5078920d, 0x6a5fcc9b, 0x547e4662, 0xf68d13c2, 0x90d8b8e8, 0x2e39f75e, 0x82c3aff5, 0x9f5d80be, 0x69d0937c, 0x6fd52da9, 0xcf2512b3, 0xc8ac993b, 0x10187da7, 0xe89c636e, 0xdb3bbb7b, 0xcd267809, 0x6e5918f4, 0xec9ab701, 0x834f9aa8, 0xe6956e65, 0xaaffe67e, 0x21bccf08, 0xef15e8e6, 0xbae79bd9, 0x4a6f36ce, 0xea9f09d4, 0x29b07cd6, 0x31a4b2af, 0x2a3f2331, 0xc6a59430, 0x35a266c0, 0x744ebc37, 0xfc82caa6, 0xe090d0b0, 0x33a7d815, 0xf104984a, 0x41ecdaf7, 0x7fcd500e, 0x1791f62f, 0x764dd68d, 0x43efb04d, 0xccaa4d54, 0xe49604df, 0x9ed1b5e3, 0x4c6a881b, 0xc12c1fb8, 0x4665517f, 0x9d5eea04, 0x018c355d, 0xfa877473, 0xfb0b412e, 0xb3671d5a, 0x92dbd252, 0xe9105633, 0x6dd64713, 0x9ad7618c, 0x37a10c7a, 0x59f8148e, 0xeb133c89, 0xcea927ee, 0xb761c935, 0xe11ce5ed, 0x7a47b13c, 0x9cd2df59, 0x55f2733f, 0x1814ce79, 0x73c737bf, 0x53f7cdea, 0x5ffdaa5b, 0xdf3d6f14, 0x7844db86, 0xcaaff381, 0xb968c43e, 0x3824342c, 0xc2a3405f, 0x161dc372, 0xbce2250c, 0x283c498b, 0xff0d9541, 0x39a80171, 0x080cb3de, 0xd8b4e49c, 0x6456c190, 0x7bcb8461, 0xd532b670, 0x486c5c74, 0xd0b85742 ]
|
||||
_T6 = [ 0x5051f4a7, 0x537e4165, 0xc31a17a4, 0x963a275e, 0xcb3bab6b, 0xf11f9d45, 0xabacfa58, 0x934be303, 0x552030fa, 0xf6ad766d, 0x9188cc76, 0x25f5024c, 0xfc4fe5d7, 0xd7c52acb, 0x80263544, 0x8fb562a3, 0x49deb15a, 0x6725ba1b, 0x9845ea0e, 0xe15dfec0, 0x02c32f75, 0x12814cf0, 0xa38d4697, 0xc66bd3f9, 0xe7038f5f, 0x9515929c, 0xebbf6d7a, 0xda955259, 0x2dd4be83, 0xd3587421, 0x2949e069, 0x448ec9c8, 0x6a75c289, 0x78f48e79, 0x6b99583e, 0xdd27b971, 0xb6bee14f, 0x17f088ad, 0x66c920ac, 0xb47dce3a, 0x1863df4a, 0x82e51a31, 0x60975133, 0x4562537f, 0xe0b16477, 0x84bb6bae, 0x1cfe81a0, 0x94f9082b, 0x58704868, 0x198f45fd, 0x8794de6c, 0xb7527bf8, 0x23ab73d3, 0xe2724b02, 0x57e31f8f, 0x2a6655ab, 0x07b2eb28, 0x032fb5c2, 0x9a86c57b, 0xa5d33708, 0xf2302887, 0xb223bfa5, 0xba02036a, 0x5ced1682, 0x2b8acf1c, 0x92a779b4, 0xf0f307f2, 0xa14e69e2, 0xcd65daf4, 0xd50605be, 0x1fd13462, 0x8ac4a6fe, 0x9d342e53, 0xa0a2f355, 0x32058ae1, 0x75a4f6eb, 0x390b83ec, 0xaa4060ef, 0x065e719f, 0x51bd6e10, 0xf93e218a, 0x3d96dd06, 0xaedd3e05, 0x464de6bd, 0xb591548d, 0x0571c45d, 0x6f0406d4, 0xff605015, 0x241998fb, 0x97d6bde9, 0xcc894043, 0x7767d99e, 0xbdb0e842, 0x8807898b, 0x38e7195b, 0xdb79c8ee, 0x47a17c0a, 0xe97c420f, 0xc9f8841e, 0x00000000, 0x83098086, 0x48322bed, 0xac1e1170, 0x4e6c5a72, 0xfbfd0eff, 0x560f8538, 0x1e3daed5, 0x27362d39, 0x640a0fd9, 0x21685ca6, 0xd19b5b54, 0x3a24362e, 0xb10c0a67, 0x0f9357e7, 0xd2b4ee96, 0x9e1b9b91, 0x4f80c0c5, 0xa261dc20, 0x695a774b, 0x161c121a, 0x0ae293ba, 0xe5c0a02a, 0x433c22e0, 0x1d121b17, 0x0b0e090d, 0xadf28bc7, 0xb92db6a8, 0xc8141ea9, 0x8557f119, 0x4caf7507, 0xbbee99dd, 0xfda37f60, 0x9ff70126, 0xbc5c72f5, 0xc544663b, 0x345bfb7e, 0x768b4329, 0xdccb23c6, 0x68b6edfc, 0x63b8e4f1, 0xcad731dc, 0x10426385, 0x40139722, 0x2084c611, 0x7d854a24, 0xf8d2bb3d, 0x11aef932, 0x6dc729a1, 0x4b1d9e2f, 0xf3dcb230, 0xec0d8652, 0xd077c1e3, 0x6c2bb316, 0x99a970b9, 0xfa119448, 0x2247e964, 0xc4a8fc8c, 0x1aa0f03f, 0xd8567d2c, 0xef223390, 0xc787494e, 0xc1d938d1, 0xfe8ccaa2, 0x3698d40b, 0xcfa6f581, 0x28a57ade, 0x26dab78e, 0xa43fadbf, 0xe42c3a9d, 0x0d507892, 0x9b6a5fcc, 0x62547e46, 0xc2f68d13, 0xe890d8b8, 0x5e2e39f7, 0xf582c3af, 0xbe9f5d80, 0x7c69d093, 0xa96fd52d, 0xb3cf2512, 0x3bc8ac99, 0xa710187d, 0x6ee89c63, 0x7bdb3bbb, 0x09cd2678, 0xf46e5918, 0x01ec9ab7, 0xa8834f9a, 0x65e6956e, 0x7eaaffe6, 0x0821bccf, 0xe6ef15e8, 0xd9bae79b, 0xce4a6f36, 0xd4ea9f09, 0xd629b07c, 0xaf31a4b2, 0x312a3f23, 0x30c6a594, 0xc035a266, 0x37744ebc, 0xa6fc82ca, 0xb0e090d0, 0x1533a7d8, 0x4af10498, 0xf741ecda, 0x0e7fcd50, 0x2f1791f6, 0x8d764dd6, 0x4d43efb0, 0x54ccaa4d, 0xdfe49604, 0xe39ed1b5, 0x1b4c6a88, 0xb8c12c1f, 0x7f466551, 0x049d5eea, 0x5d018c35, 0x73fa8774, 0x2efb0b41, 0x5ab3671d, 0x5292dbd2, 0x33e91056, 0x136dd647, 0x8c9ad761, 0x7a37a10c, 0x8e59f814, 0x89eb133c, 0xeecea927, 0x35b761c9, 0xede11ce5, 0x3c7a47b1, 0x599cd2df, 0x3f55f273, 0x791814ce, 0xbf73c737, 0xea53f7cd, 0x5b5ffdaa, 0x14df3d6f, 0x867844db, 0x81caaff3, 0x3eb968c4, 0x2c382434, 0x5fc2a340, 0x72161dc3, 0x0cbce225, 0x8b283c49, 0x41ff0d95, 0x7139a801, 0xde080cb3, 0x9cd8b4e4, 0x906456c1, 0x617bcb84, 0x70d532b6, 0x74486c5c, 0x42d0b857 ]
|
||||
_T7 = [ 0xa75051f4, 0x65537e41, 0xa4c31a17, 0x5e963a27, 0x6bcb3bab, 0x45f11f9d, 0x58abacfa, 0x03934be3, 0xfa552030, 0x6df6ad76, 0x769188cc, 0x4c25f502, 0xd7fc4fe5, 0xcbd7c52a, 0x44802635, 0xa38fb562, 0x5a49deb1, 0x1b6725ba, 0x0e9845ea, 0xc0e15dfe, 0x7502c32f, 0xf012814c, 0x97a38d46, 0xf9c66bd3, 0x5fe7038f, 0x9c951592, 0x7aebbf6d, 0x59da9552, 0x832dd4be, 0x21d35874, 0x692949e0, 0xc8448ec9, 0x896a75c2, 0x7978f48e, 0x3e6b9958, 0x71dd27b9, 0x4fb6bee1, 0xad17f088, 0xac66c920, 0x3ab47dce, 0x4a1863df, 0x3182e51a, 0x33609751, 0x7f456253, 0x77e0b164, 0xae84bb6b, 0xa01cfe81, 0x2b94f908, 0x68587048, 0xfd198f45, 0x6c8794de, 0xf8b7527b, 0xd323ab73, 0x02e2724b, 0x8f57e31f, 0xab2a6655, 0x2807b2eb, 0xc2032fb5, 0x7b9a86c5, 0x08a5d337, 0x87f23028, 0xa5b223bf, 0x6aba0203, 0x825ced16, 0x1c2b8acf, 0xb492a779, 0xf2f0f307, 0xe2a14e69, 0xf4cd65da, 0xbed50605, 0x621fd134, 0xfe8ac4a6, 0x539d342e, 0x55a0a2f3, 0xe132058a, 0xeb75a4f6, 0xec390b83, 0xefaa4060, 0x9f065e71, 0x1051bd6e, 0x8af93e21, 0x063d96dd, 0x05aedd3e, 0xbd464de6, 0x8db59154, 0x5d0571c4, 0xd46f0406, 0x15ff6050, 0xfb241998, 0xe997d6bd, 0x43cc8940, 0x9e7767d9, 0x42bdb0e8, 0x8b880789, 0x5b38e719, 0xeedb79c8, 0x0a47a17c, 0x0fe97c42, 0x1ec9f884, 0x00000000, 0x86830980, 0xed48322b, 0x70ac1e11, 0x724e6c5a, 0xfffbfd0e, 0x38560f85, 0xd51e3dae, 0x3927362d, 0xd9640a0f, 0xa621685c, 0x54d19b5b, 0x2e3a2436, 0x67b10c0a, 0xe70f9357, 0x96d2b4ee, 0x919e1b9b, 0xc54f80c0, 0x20a261dc, 0x4b695a77, 0x1a161c12, 0xba0ae293, 0x2ae5c0a0, 0xe0433c22, 0x171d121b, 0x0d0b0e09, 0xc7adf28b, 0xa8b92db6, 0xa9c8141e, 0x198557f1, 0x074caf75, 0xddbbee99, 0x60fda37f, 0x269ff701, 0xf5bc5c72, 0x3bc54466, 0x7e345bfb, 0x29768b43, 0xc6dccb23, 0xfc68b6ed, 0xf163b8e4, 0xdccad731, 0x85104263, 0x22401397, 0x112084c6, 0x247d854a, 0x3df8d2bb, 0x3211aef9, 0xa16dc729, 0x2f4b1d9e, 0x30f3dcb2, 0x52ec0d86, 0xe3d077c1, 0x166c2bb3, 0xb999a970, 0x48fa1194, 0x642247e9, 0x8cc4a8fc, 0x3f1aa0f0, 0x2cd8567d, 0x90ef2233, 0x4ec78749, 0xd1c1d938, 0xa2fe8cca, 0x0b3698d4, 0x81cfa6f5, 0xde28a57a, 0x8e26dab7, 0xbfa43fad, 0x9de42c3a, 0x920d5078, 0xcc9b6a5f, 0x4662547e, 0x13c2f68d, 0xb8e890d8, 0xf75e2e39, 0xaff582c3, 0x80be9f5d, 0x937c69d0, 0x2da96fd5, 0x12b3cf25, 0x993bc8ac, 0x7da71018, 0x636ee89c, 0xbb7bdb3b, 0x7809cd26, 0x18f46e59, 0xb701ec9a, 0x9aa8834f, 0x6e65e695, 0xe67eaaff, 0xcf0821bc, 0xe8e6ef15, 0x9bd9bae7, 0x36ce4a6f, 0x09d4ea9f, 0x7cd629b0, 0xb2af31a4, 0x23312a3f, 0x9430c6a5, 0x66c035a2, 0xbc37744e, 0xcaa6fc82, 0xd0b0e090, 0xd81533a7, 0x984af104, 0xdaf741ec, 0x500e7fcd, 0xf62f1791, 0xd68d764d, 0xb04d43ef, 0x4d54ccaa, 0x04dfe496, 0xb5e39ed1, 0x881b4c6a, 0x1fb8c12c, 0x517f4665, 0xea049d5e, 0x355d018c, 0x7473fa87, 0x412efb0b, 0x1d5ab367, 0xd25292db, 0x5633e910, 0x47136dd6, 0x618c9ad7, 0x0c7a37a1, 0x148e59f8, 0x3c89eb13, 0x27eecea9, 0xc935b761, 0xe5ede11c, 0xb13c7a47, 0xdf599cd2, 0x733f55f2, 0xce791814, 0x37bf73c7, 0xcdea53f7, 0xaa5b5ffd, 0x6f14df3d, 0xdb867844, 0xf381caaf, 0xc43eb968, 0x342c3824, 0x405fc2a3, 0xc372161d, 0x250cbce2, 0x498b283c, 0x9541ff0d, 0x017139a8, 0xb3de080c, 0xe49cd8b4, 0xc1906456, 0x84617bcb, 0xb670d532, 0x5c74486c, 0x5742d0b8 ]
|
||||
_T8 = [ 0xf4a75051, 0x4165537e, 0x17a4c31a, 0x275e963a, 0xab6bcb3b, 0x9d45f11f, 0xfa58abac, 0xe303934b, 0x30fa5520, 0x766df6ad, 0xcc769188, 0x024c25f5, 0xe5d7fc4f, 0x2acbd7c5, 0x35448026, 0x62a38fb5, 0xb15a49de, 0xba1b6725, 0xea0e9845, 0xfec0e15d, 0x2f7502c3, 0x4cf01281, 0x4697a38d, 0xd3f9c66b, 0x8f5fe703, 0x929c9515, 0x6d7aebbf, 0x5259da95, 0xbe832dd4, 0x7421d358, 0xe0692949, 0xc9c8448e, 0xc2896a75, 0x8e7978f4, 0x583e6b99, 0xb971dd27, 0xe14fb6be, 0x88ad17f0, 0x20ac66c9, 0xce3ab47d, 0xdf4a1863, 0x1a3182e5, 0x51336097, 0x537f4562, 0x6477e0b1, 0x6bae84bb, 0x81a01cfe, 0x082b94f9, 0x48685870, 0x45fd198f, 0xde6c8794, 0x7bf8b752, 0x73d323ab, 0x4b02e272, 0x1f8f57e3, 0x55ab2a66, 0xeb2807b2, 0xb5c2032f, 0xc57b9a86, 0x3708a5d3, 0x2887f230, 0xbfa5b223, 0x036aba02, 0x16825ced, 0xcf1c2b8a, 0x79b492a7, 0x07f2f0f3, 0x69e2a14e, 0xdaf4cd65, 0x05bed506, 0x34621fd1, 0xa6fe8ac4, 0x2e539d34, 0xf355a0a2, 0x8ae13205, 0xf6eb75a4, 0x83ec390b, 0x60efaa40, 0x719f065e, 0x6e1051bd, 0x218af93e, 0xdd063d96, 0x3e05aedd, 0xe6bd464d, 0x548db591, 0xc45d0571, 0x06d46f04, 0x5015ff60, 0x98fb2419, 0xbde997d6, 0x4043cc89, 0xd99e7767, 0xe842bdb0, 0x898b8807, 0x195b38e7, 0xc8eedb79, 0x7c0a47a1, 0x420fe97c, 0x841ec9f8, 0x00000000, 0x80868309, 0x2bed4832, 0x1170ac1e, 0x5a724e6c, 0x0efffbfd, 0x8538560f, 0xaed51e3d, 0x2d392736, 0x0fd9640a, 0x5ca62168, 0x5b54d19b, 0x362e3a24, 0x0a67b10c, 0x57e70f93, 0xee96d2b4, 0x9b919e1b, 0xc0c54f80, 0xdc20a261, 0x774b695a, 0x121a161c, 0x93ba0ae2, 0xa02ae5c0, 0x22e0433c, 0x1b171d12, 0x090d0b0e, 0x8bc7adf2, 0xb6a8b92d, 0x1ea9c814, 0xf1198557, 0x75074caf, 0x99ddbbee, 0x7f60fda3, 0x01269ff7, 0x72f5bc5c, 0x663bc544, 0xfb7e345b, 0x4329768b, 0x23c6dccb, 0xedfc68b6, 0xe4f163b8, 0x31dccad7, 0x63851042, 0x97224013, 0xc6112084, 0x4a247d85, 0xbb3df8d2, 0xf93211ae, 0x29a16dc7, 0x9e2f4b1d, 0xb230f3dc, 0x8652ec0d, 0xc1e3d077, 0xb3166c2b, 0x70b999a9, 0x9448fa11, 0xe9642247, 0xfc8cc4a8, 0xf03f1aa0, 0x7d2cd856, 0x3390ef22, 0x494ec787, 0x38d1c1d9, 0xcaa2fe8c, 0xd40b3698, 0xf581cfa6, 0x7ade28a5, 0xb78e26da, 0xadbfa43f, 0x3a9de42c, 0x78920d50, 0x5fcc9b6a, 0x7e466254, 0x8d13c2f6, 0xd8b8e890, 0x39f75e2e, 0xc3aff582, 0x5d80be9f, 0xd0937c69, 0xd52da96f, 0x2512b3cf, 0xac993bc8, 0x187da710, 0x9c636ee8, 0x3bbb7bdb, 0x267809cd, 0x5918f46e, 0x9ab701ec, 0x4f9aa883, 0x956e65e6, 0xffe67eaa, 0xbccf0821, 0x15e8e6ef, 0xe79bd9ba, 0x6f36ce4a, 0x9f09d4ea, 0xb07cd629, 0xa4b2af31, 0x3f23312a, 0xa59430c6, 0xa266c035, 0x4ebc3774, 0x82caa6fc, 0x90d0b0e0, 0xa7d81533, 0x04984af1, 0xecdaf741, 0xcd500e7f, 0x91f62f17, 0x4dd68d76, 0xefb04d43, 0xaa4d54cc, 0x9604dfe4, 0xd1b5e39e, 0x6a881b4c, 0x2c1fb8c1, 0x65517f46, 0x5eea049d, 0x8c355d01, 0x877473fa, 0x0b412efb, 0x671d5ab3, 0xdbd25292, 0x105633e9, 0xd647136d, 0xd7618c9a, 0xa10c7a37, 0xf8148e59, 0x133c89eb, 0xa927eece, 0x61c935b7, 0x1ce5ede1, 0x47b13c7a, 0xd2df599c, 0xf2733f55, 0x14ce7918, 0xc737bf73, 0xf7cdea53, 0xfdaa5b5f, 0x3d6f14df, 0x44db8678, 0xaff381ca, 0x68c43eb9, 0x24342c38, 0xa3405fc2, 0x1dc37216, 0xe2250cbc, 0x3c498b28, 0x0d9541ff, 0xa8017139, 0x0cb3de08, 0xb4e49cd8, 0x56c19064, 0xcb84617b, 0x32b670d5, 0x6c5c7448, 0xb85742d0 ]
|
||||
|
||||
# Transformations for decryption key expansion
|
||||
_U1 = [ 0x00000000, 0x0e090d0b, 0x1c121a16, 0x121b171d, 0x3824342c, 0x362d3927, 0x24362e3a, 0x2a3f2331, 0x70486858, 0x7e416553, 0x6c5a724e, 0x62537f45, 0x486c5c74, 0x4665517f, 0x547e4662, 0x5a774b69, 0xe090d0b0, 0xee99ddbb, 0xfc82caa6, 0xf28bc7ad, 0xd8b4e49c, 0xd6bde997, 0xc4a6fe8a, 0xcaaff381, 0x90d8b8e8, 0x9ed1b5e3, 0x8ccaa2fe, 0x82c3aff5, 0xa8fc8cc4, 0xa6f581cf, 0xb4ee96d2, 0xbae79bd9, 0xdb3bbb7b, 0xd532b670, 0xc729a16d, 0xc920ac66, 0xe31f8f57, 0xed16825c, 0xff0d9541, 0xf104984a, 0xab73d323, 0xa57ade28, 0xb761c935, 0xb968c43e, 0x9357e70f, 0x9d5eea04, 0x8f45fd19, 0x814cf012, 0x3bab6bcb, 0x35a266c0, 0x27b971dd, 0x29b07cd6, 0x038f5fe7, 0x0d8652ec, 0x1f9d45f1, 0x119448fa, 0x4be30393, 0x45ea0e98, 0x57f11985, 0x59f8148e, 0x73c737bf, 0x7dce3ab4, 0x6fd52da9, 0x61dc20a2, 0xad766df6, 0xa37f60fd, 0xb16477e0, 0xbf6d7aeb, 0x955259da, 0x9b5b54d1, 0x894043cc, 0x87494ec7, 0xdd3e05ae, 0xd33708a5, 0xc12c1fb8, 0xcf2512b3, 0xe51a3182, 0xeb133c89, 0xf9082b94, 0xf701269f, 0x4de6bd46, 0x43efb04d, 0x51f4a750, 0x5ffdaa5b, 0x75c2896a, 0x7bcb8461, 0x69d0937c, 0x67d99e77, 0x3daed51e, 0x33a7d815, 0x21bccf08, 0x2fb5c203, 0x058ae132, 0x0b83ec39, 0x1998fb24, 0x1791f62f, 0x764dd68d, 0x7844db86, 0x6a5fcc9b, 0x6456c190, 0x4e69e2a1, 0x4060efaa, 0x527bf8b7, 0x5c72f5bc, 0x0605bed5, 0x080cb3de, 0x1a17a4c3, 0x141ea9c8, 0x3e218af9, 0x302887f2, 0x223390ef, 0x2c3a9de4, 0x96dd063d, 0x98d40b36, 0x8acf1c2b, 0x84c61120, 0xaef93211, 0xa0f03f1a, 0xb2eb2807, 0xbce2250c, 0xe6956e65, 0xe89c636e, 0xfa877473, 0xf48e7978, 0xdeb15a49, 0xd0b85742, 0xc2a3405f, 0xccaa4d54, 0x41ecdaf7, 0x4fe5d7fc, 0x5dfec0e1, 0x53f7cdea, 0x79c8eedb, 0x77c1e3d0, 0x65daf4cd, 0x6bd3f9c6, 0x31a4b2af, 0x3fadbfa4, 0x2db6a8b9, 0x23bfa5b2, 0x09808683, 0x07898b88, 0x15929c95, 0x1b9b919e, 0xa17c0a47, 0xaf75074c, 0xbd6e1051, 0xb3671d5a, 0x99583e6b, 0x97513360, 0x854a247d, 0x8b432976, 0xd134621f, 0xdf3d6f14, 0xcd267809, 0xc32f7502, 0xe9105633, 0xe7195b38, 0xf5024c25, 0xfb0b412e, 0x9ad7618c, 0x94de6c87, 0x86c57b9a, 0x88cc7691, 0xa2f355a0, 0xacfa58ab, 0xbee14fb6, 0xb0e842bd, 0xea9f09d4, 0xe49604df, 0xf68d13c2, 0xf8841ec9, 0xd2bb3df8, 0xdcb230f3, 0xcea927ee, 0xc0a02ae5, 0x7a47b13c, 0x744ebc37, 0x6655ab2a, 0x685ca621, 0x42638510, 0x4c6a881b, 0x5e719f06, 0x5078920d, 0x0a0fd964, 0x0406d46f, 0x161dc372, 0x1814ce79, 0x322bed48, 0x3c22e043, 0x2e39f75e, 0x2030fa55, 0xec9ab701, 0xe293ba0a, 0xf088ad17, 0xfe81a01c, 0xd4be832d, 0xdab78e26, 0xc8ac993b, 0xc6a59430, 0x9cd2df59, 0x92dbd252, 0x80c0c54f, 0x8ec9c844, 0xa4f6eb75, 0xaaffe67e, 0xb8e4f163, 0xb6edfc68, 0x0c0a67b1, 0x02036aba, 0x10187da7, 0x1e1170ac, 0x342e539d, 0x3a275e96, 0x283c498b, 0x26354480, 0x7c420fe9, 0x724b02e2, 0x605015ff, 0x6e5918f4, 0x44663bc5, 0x4a6f36ce, 0x587421d3, 0x567d2cd8, 0x37a10c7a, 0x39a80171, 0x2bb3166c, 0x25ba1b67, 0x0f853856, 0x018c355d, 0x13972240, 0x1d9e2f4b, 0x47e96422, 0x49e06929, 0x5bfb7e34, 0x55f2733f, 0x7fcd500e, 0x71c45d05, 0x63df4a18, 0x6dd64713, 0xd731dcca, 0xd938d1c1, 0xcb23c6dc, 0xc52acbd7, 0xef15e8e6, 0xe11ce5ed, 0xf307f2f0, 0xfd0efffb, 0xa779b492, 0xa970b999, 0xbb6bae84, 0xb562a38f, 0x9f5d80be, 0x91548db5, 0x834f9aa8, 0x8d4697a3 ]
|
||||
_U2 = [ 0x00000000, 0x0b0e090d, 0x161c121a, 0x1d121b17, 0x2c382434, 0x27362d39, 0x3a24362e, 0x312a3f23, 0x58704868, 0x537e4165, 0x4e6c5a72, 0x4562537f, 0x74486c5c, 0x7f466551, 0x62547e46, 0x695a774b, 0xb0e090d0, 0xbbee99dd, 0xa6fc82ca, 0xadf28bc7, 0x9cd8b4e4, 0x97d6bde9, 0x8ac4a6fe, 0x81caaff3, 0xe890d8b8, 0xe39ed1b5, 0xfe8ccaa2, 0xf582c3af, 0xc4a8fc8c, 0xcfa6f581, 0xd2b4ee96, 0xd9bae79b, 0x7bdb3bbb, 0x70d532b6, 0x6dc729a1, 0x66c920ac, 0x57e31f8f, 0x5ced1682, 0x41ff0d95, 0x4af10498, 0x23ab73d3, 0x28a57ade, 0x35b761c9, 0x3eb968c4, 0x0f9357e7, 0x049d5eea, 0x198f45fd, 0x12814cf0, 0xcb3bab6b, 0xc035a266, 0xdd27b971, 0xd629b07c, 0xe7038f5f, 0xec0d8652, 0xf11f9d45, 0xfa119448, 0x934be303, 0x9845ea0e, 0x8557f119, 0x8e59f814, 0xbf73c737, 0xb47dce3a, 0xa96fd52d, 0xa261dc20, 0xf6ad766d, 0xfda37f60, 0xe0b16477, 0xebbf6d7a, 0xda955259, 0xd19b5b54, 0xcc894043, 0xc787494e, 0xaedd3e05, 0xa5d33708, 0xb8c12c1f, 0xb3cf2512, 0x82e51a31, 0x89eb133c, 0x94f9082b, 0x9ff70126, 0x464de6bd, 0x4d43efb0, 0x5051f4a7, 0x5b5ffdaa, 0x6a75c289, 0x617bcb84, 0x7c69d093, 0x7767d99e, 0x1e3daed5, 0x1533a7d8, 0x0821bccf, 0x032fb5c2, 0x32058ae1, 0x390b83ec, 0x241998fb, 0x2f1791f6, 0x8d764dd6, 0x867844db, 0x9b6a5fcc, 0x906456c1, 0xa14e69e2, 0xaa4060ef, 0xb7527bf8, 0xbc5c72f5, 0xd50605be, 0xde080cb3, 0xc31a17a4, 0xc8141ea9, 0xf93e218a, 0xf2302887, 0xef223390, 0xe42c3a9d, 0x3d96dd06, 0x3698d40b, 0x2b8acf1c, 0x2084c611, 0x11aef932, 0x1aa0f03f, 0x07b2eb28, 0x0cbce225, 0x65e6956e, 0x6ee89c63, 0x73fa8774, 0x78f48e79, 0x49deb15a, 0x42d0b857, 0x5fc2a340, 0x54ccaa4d, 0xf741ecda, 0xfc4fe5d7, 0xe15dfec0, 0xea53f7cd, 0xdb79c8ee, 0xd077c1e3, 0xcd65daf4, 0xc66bd3f9, 0xaf31a4b2, 0xa43fadbf, 0xb92db6a8, 0xb223bfa5, 0x83098086, 0x8807898b, 0x9515929c, 0x9e1b9b91, 0x47a17c0a, 0x4caf7507, 0x51bd6e10, 0x5ab3671d, 0x6b99583e, 0x60975133, 0x7d854a24, 0x768b4329, 0x1fd13462, 0x14df3d6f, 0x09cd2678, 0x02c32f75, 0x33e91056, 0x38e7195b, 0x25f5024c, 0x2efb0b41, 0x8c9ad761, 0x8794de6c, 0x9a86c57b, 0x9188cc76, 0xa0a2f355, 0xabacfa58, 0xb6bee14f, 0xbdb0e842, 0xd4ea9f09, 0xdfe49604, 0xc2f68d13, 0xc9f8841e, 0xf8d2bb3d, 0xf3dcb230, 0xeecea927, 0xe5c0a02a, 0x3c7a47b1, 0x37744ebc, 0x2a6655ab, 0x21685ca6, 0x10426385, 0x1b4c6a88, 0x065e719f, 0x0d507892, 0x640a0fd9, 0x6f0406d4, 0x72161dc3, 0x791814ce, 0x48322bed, 0x433c22e0, 0x5e2e39f7, 0x552030fa, 0x01ec9ab7, 0x0ae293ba, 0x17f088ad, 0x1cfe81a0, 0x2dd4be83, 0x26dab78e, 0x3bc8ac99, 0x30c6a594, 0x599cd2df, 0x5292dbd2, 0x4f80c0c5, 0x448ec9c8, 0x75a4f6eb, 0x7eaaffe6, 0x63b8e4f1, 0x68b6edfc, 0xb10c0a67, 0xba02036a, 0xa710187d, 0xac1e1170, 0x9d342e53, 0x963a275e, 0x8b283c49, 0x80263544, 0xe97c420f, 0xe2724b02, 0xff605015, 0xf46e5918, 0xc544663b, 0xce4a6f36, 0xd3587421, 0xd8567d2c, 0x7a37a10c, 0x7139a801, 0x6c2bb316, 0x6725ba1b, 0x560f8538, 0x5d018c35, 0x40139722, 0x4b1d9e2f, 0x2247e964, 0x2949e069, 0x345bfb7e, 0x3f55f273, 0x0e7fcd50, 0x0571c45d, 0x1863df4a, 0x136dd647, 0xcad731dc, 0xc1d938d1, 0xdccb23c6, 0xd7c52acb, 0xe6ef15e8, 0xede11ce5, 0xf0f307f2, 0xfbfd0eff, 0x92a779b4, 0x99a970b9, 0x84bb6bae, 0x8fb562a3, 0xbe9f5d80, 0xb591548d, 0xa8834f9a, 0xa38d4697 ]
|
||||
_U3 = [ 0x00000000, 0x0d0b0e09, 0x1a161c12, 0x171d121b, 0x342c3824, 0x3927362d, 0x2e3a2436, 0x23312a3f, 0x68587048, 0x65537e41, 0x724e6c5a, 0x7f456253, 0x5c74486c, 0x517f4665, 0x4662547e, 0x4b695a77, 0xd0b0e090, 0xddbbee99, 0xcaa6fc82, 0xc7adf28b, 0xe49cd8b4, 0xe997d6bd, 0xfe8ac4a6, 0xf381caaf, 0xb8e890d8, 0xb5e39ed1, 0xa2fe8cca, 0xaff582c3, 0x8cc4a8fc, 0x81cfa6f5, 0x96d2b4ee, 0x9bd9bae7, 0xbb7bdb3b, 0xb670d532, 0xa16dc729, 0xac66c920, 0x8f57e31f, 0x825ced16, 0x9541ff0d, 0x984af104, 0xd323ab73, 0xde28a57a, 0xc935b761, 0xc43eb968, 0xe70f9357, 0xea049d5e, 0xfd198f45, 0xf012814c, 0x6bcb3bab, 0x66c035a2, 0x71dd27b9, 0x7cd629b0, 0x5fe7038f, 0x52ec0d86, 0x45f11f9d, 0x48fa1194, 0x03934be3, 0x0e9845ea, 0x198557f1, 0x148e59f8, 0x37bf73c7, 0x3ab47dce, 0x2da96fd5, 0x20a261dc, 0x6df6ad76, 0x60fda37f, 0x77e0b164, 0x7aebbf6d, 0x59da9552, 0x54d19b5b, 0x43cc8940, 0x4ec78749, 0x05aedd3e, 0x08a5d337, 0x1fb8c12c, 0x12b3cf25, 0x3182e51a, 0x3c89eb13, 0x2b94f908, 0x269ff701, 0xbd464de6, 0xb04d43ef, 0xa75051f4, 0xaa5b5ffd, 0x896a75c2, 0x84617bcb, 0x937c69d0, 0x9e7767d9, 0xd51e3dae, 0xd81533a7, 0xcf0821bc, 0xc2032fb5, 0xe132058a, 0xec390b83, 0xfb241998, 0xf62f1791, 0xd68d764d, 0xdb867844, 0xcc9b6a5f, 0xc1906456, 0xe2a14e69, 0xefaa4060, 0xf8b7527b, 0xf5bc5c72, 0xbed50605, 0xb3de080c, 0xa4c31a17, 0xa9c8141e, 0x8af93e21, 0x87f23028, 0x90ef2233, 0x9de42c3a, 0x063d96dd, 0x0b3698d4, 0x1c2b8acf, 0x112084c6, 0x3211aef9, 0x3f1aa0f0, 0x2807b2eb, 0x250cbce2, 0x6e65e695, 0x636ee89c, 0x7473fa87, 0x7978f48e, 0x5a49deb1, 0x5742d0b8, 0x405fc2a3, 0x4d54ccaa, 0xdaf741ec, 0xd7fc4fe5, 0xc0e15dfe, 0xcdea53f7, 0xeedb79c8, 0xe3d077c1, 0xf4cd65da, 0xf9c66bd3, 0xb2af31a4, 0xbfa43fad, 0xa8b92db6, 0xa5b223bf, 0x86830980, 0x8b880789, 0x9c951592, 0x919e1b9b, 0x0a47a17c, 0x074caf75, 0x1051bd6e, 0x1d5ab367, 0x3e6b9958, 0x33609751, 0x247d854a, 0x29768b43, 0x621fd134, 0x6f14df3d, 0x7809cd26, 0x7502c32f, 0x5633e910, 0x5b38e719, 0x4c25f502, 0x412efb0b, 0x618c9ad7, 0x6c8794de, 0x7b9a86c5, 0x769188cc, 0x55a0a2f3, 0x58abacfa, 0x4fb6bee1, 0x42bdb0e8, 0x09d4ea9f, 0x04dfe496, 0x13c2f68d, 0x1ec9f884, 0x3df8d2bb, 0x30f3dcb2, 0x27eecea9, 0x2ae5c0a0, 0xb13c7a47, 0xbc37744e, 0xab2a6655, 0xa621685c, 0x85104263, 0x881b4c6a, 0x9f065e71, 0x920d5078, 0xd9640a0f, 0xd46f0406, 0xc372161d, 0xce791814, 0xed48322b, 0xe0433c22, 0xf75e2e39, 0xfa552030, 0xb701ec9a, 0xba0ae293, 0xad17f088, 0xa01cfe81, 0x832dd4be, 0x8e26dab7, 0x993bc8ac, 0x9430c6a5, 0xdf599cd2, 0xd25292db, 0xc54f80c0, 0xc8448ec9, 0xeb75a4f6, 0xe67eaaff, 0xf163b8e4, 0xfc68b6ed, 0x67b10c0a, 0x6aba0203, 0x7da71018, 0x70ac1e11, 0x539d342e, 0x5e963a27, 0x498b283c, 0x44802635, 0x0fe97c42, 0x02e2724b, 0x15ff6050, 0x18f46e59, 0x3bc54466, 0x36ce4a6f, 0x21d35874, 0x2cd8567d, 0x0c7a37a1, 0x017139a8, 0x166c2bb3, 0x1b6725ba, 0x38560f85, 0x355d018c, 0x22401397, 0x2f4b1d9e, 0x642247e9, 0x692949e0, 0x7e345bfb, 0x733f55f2, 0x500e7fcd, 0x5d0571c4, 0x4a1863df, 0x47136dd6, 0xdccad731, 0xd1c1d938, 0xc6dccb23, 0xcbd7c52a, 0xe8e6ef15, 0xe5ede11c, 0xf2f0f307, 0xfffbfd0e, 0xb492a779, 0xb999a970, 0xae84bb6b, 0xa38fb562, 0x80be9f5d, 0x8db59154, 0x9aa8834f, 0x97a38d46 ]
|
||||
_U4 = [ 0x00000000, 0x090d0b0e, 0x121a161c, 0x1b171d12, 0x24342c38, 0x2d392736, 0x362e3a24, 0x3f23312a, 0x48685870, 0x4165537e, 0x5a724e6c, 0x537f4562, 0x6c5c7448, 0x65517f46, 0x7e466254, 0x774b695a, 0x90d0b0e0, 0x99ddbbee, 0x82caa6fc, 0x8bc7adf2, 0xb4e49cd8, 0xbde997d6, 0xa6fe8ac4, 0xaff381ca, 0xd8b8e890, 0xd1b5e39e, 0xcaa2fe8c, 0xc3aff582, 0xfc8cc4a8, 0xf581cfa6, 0xee96d2b4, 0xe79bd9ba, 0x3bbb7bdb, 0x32b670d5, 0x29a16dc7, 0x20ac66c9, 0x1f8f57e3, 0x16825ced, 0x0d9541ff, 0x04984af1, 0x73d323ab, 0x7ade28a5, 0x61c935b7, 0x68c43eb9, 0x57e70f93, 0x5eea049d, 0x45fd198f, 0x4cf01281, 0xab6bcb3b, 0xa266c035, 0xb971dd27, 0xb07cd629, 0x8f5fe703, 0x8652ec0d, 0x9d45f11f, 0x9448fa11, 0xe303934b, 0xea0e9845, 0xf1198557, 0xf8148e59, 0xc737bf73, 0xce3ab47d, 0xd52da96f, 0xdc20a261, 0x766df6ad, 0x7f60fda3, 0x6477e0b1, 0x6d7aebbf, 0x5259da95, 0x5b54d19b, 0x4043cc89, 0x494ec787, 0x3e05aedd, 0x3708a5d3, 0x2c1fb8c1, 0x2512b3cf, 0x1a3182e5, 0x133c89eb, 0x082b94f9, 0x01269ff7, 0xe6bd464d, 0xefb04d43, 0xf4a75051, 0xfdaa5b5f, 0xc2896a75, 0xcb84617b, 0xd0937c69, 0xd99e7767, 0xaed51e3d, 0xa7d81533, 0xbccf0821, 0xb5c2032f, 0x8ae13205, 0x83ec390b, 0x98fb2419, 0x91f62f17, 0x4dd68d76, 0x44db8678, 0x5fcc9b6a, 0x56c19064, 0x69e2a14e, 0x60efaa40, 0x7bf8b752, 0x72f5bc5c, 0x05bed506, 0x0cb3de08, 0x17a4c31a, 0x1ea9c814, 0x218af93e, 0x2887f230, 0x3390ef22, 0x3a9de42c, 0xdd063d96, 0xd40b3698, 0xcf1c2b8a, 0xc6112084, 0xf93211ae, 0xf03f1aa0, 0xeb2807b2, 0xe2250cbc, 0x956e65e6, 0x9c636ee8, 0x877473fa, 0x8e7978f4, 0xb15a49de, 0xb85742d0, 0xa3405fc2, 0xaa4d54cc, 0xecdaf741, 0xe5d7fc4f, 0xfec0e15d, 0xf7cdea53, 0xc8eedb79, 0xc1e3d077, 0xdaf4cd65, 0xd3f9c66b, 0xa4b2af31, 0xadbfa43f, 0xb6a8b92d, 0xbfa5b223, 0x80868309, 0x898b8807, 0x929c9515, 0x9b919e1b, 0x7c0a47a1, 0x75074caf, 0x6e1051bd, 0x671d5ab3, 0x583e6b99, 0x51336097, 0x4a247d85, 0x4329768b, 0x34621fd1, 0x3d6f14df, 0x267809cd, 0x2f7502c3, 0x105633e9, 0x195b38e7, 0x024c25f5, 0x0b412efb, 0xd7618c9a, 0xde6c8794, 0xc57b9a86, 0xcc769188, 0xf355a0a2, 0xfa58abac, 0xe14fb6be, 0xe842bdb0, 0x9f09d4ea, 0x9604dfe4, 0x8d13c2f6, 0x841ec9f8, 0xbb3df8d2, 0xb230f3dc, 0xa927eece, 0xa02ae5c0, 0x47b13c7a, 0x4ebc3774, 0x55ab2a66, 0x5ca62168, 0x63851042, 0x6a881b4c, 0x719f065e, 0x78920d50, 0x0fd9640a, 0x06d46f04, 0x1dc37216, 0x14ce7918, 0x2bed4832, 0x22e0433c, 0x39f75e2e, 0x30fa5520, 0x9ab701ec, 0x93ba0ae2, 0x88ad17f0, 0x81a01cfe, 0xbe832dd4, 0xb78e26da, 0xac993bc8, 0xa59430c6, 0xd2df599c, 0xdbd25292, 0xc0c54f80, 0xc9c8448e, 0xf6eb75a4, 0xffe67eaa, 0xe4f163b8, 0xedfc68b6, 0x0a67b10c, 0x036aba02, 0x187da710, 0x1170ac1e, 0x2e539d34, 0x275e963a, 0x3c498b28, 0x35448026, 0x420fe97c, 0x4b02e272, 0x5015ff60, 0x5918f46e, 0x663bc544, 0x6f36ce4a, 0x7421d358, 0x7d2cd856, 0xa10c7a37, 0xa8017139, 0xb3166c2b, 0xba1b6725, 0x8538560f, 0x8c355d01, 0x97224013, 0x9e2f4b1d, 0xe9642247, 0xe0692949, 0xfb7e345b, 0xf2733f55, 0xcd500e7f, 0xc45d0571, 0xdf4a1863, 0xd647136d, 0x31dccad7, 0x38d1c1d9, 0x23c6dccb, 0x2acbd7c5, 0x15e8e6ef, 0x1ce5ede1, 0x07f2f0f3, 0x0efffbfd, 0x79b492a7, 0x70b999a9, 0x6bae84bb, 0x62a38fb5, 0x5d80be9f, 0x548db591, 0x4f9aa883, 0x4697a38d ]
|
||||
# fmt: on
|
||||
|
||||
def __init__(self, key: bytes) -> None:
|
||||
if len(key) not in (16, 24, 32):
|
||||
raise core.InvalidArgumentError(f'Invalid key size {len(key)}')
|
||||
|
||||
rounds = self._NUMBER_OF_ROUNDS[len(key)]
|
||||
|
||||
# Encryption round keys
|
||||
self._ke = [[0] * 4 for i in range(rounds + 1)]
|
||||
|
||||
# Decryption round keys
|
||||
self._kd = [[0] * 4 for i in range(rounds + 1)]
|
||||
|
||||
round_key_count = (rounds + 1) * 4
|
||||
kc = len(key) // 4
|
||||
|
||||
# Convert the key into ints
|
||||
tk = [struct.unpack('>i', key[i : i + 4])[0] for i in range(0, len(key), 4)]
|
||||
|
||||
# Copy values into round key arrays
|
||||
for i in range(0, kc):
|
||||
self._ke[i // 4][i % 4] = tk[i]
|
||||
self._kd[rounds - (i // 4)][i % 4] = tk[i]
|
||||
|
||||
# Key expansion (FIPS-197 section 5.2)
|
||||
r_con_pointer = 0
|
||||
t = kc
|
||||
while t < round_key_count:
|
||||
tt = tk[kc - 1]
|
||||
tk[0] ^= (
|
||||
(self._S[(tt >> 16) & 0xFF] << 24)
|
||||
^ (self._S[(tt >> 8) & 0xFF] << 16)
|
||||
^ (self._S[tt & 0xFF] << 8)
|
||||
^ self._S[(tt >> 24) & 0xFF]
|
||||
^ (self._RCON[r_con_pointer] << 24)
|
||||
)
|
||||
r_con_pointer += 1
|
||||
|
||||
if kc != 8:
|
||||
for i in range(1, kc):
|
||||
tk[i] ^= tk[i - 1]
|
||||
|
||||
# Key expansion for 256-bit keys is "slightly different" (FIPS-197)
|
||||
else:
|
||||
for i in range(1, kc // 2):
|
||||
tk[i] ^= tk[i - 1]
|
||||
tt = tk[kc // 2 - 1]
|
||||
|
||||
tk[kc // 2] ^= (
|
||||
self._S[tt & 0xFF]
|
||||
^ (self._S[(tt >> 8) & 0xFF] << 8)
|
||||
^ (self._S[(tt >> 16) & 0xFF] << 16)
|
||||
^ (self._S[(tt >> 24) & 0xFF] << 24)
|
||||
)
|
||||
|
||||
for i in range(kc // 2 + 1, kc):
|
||||
tk[i] ^= tk[i - 1]
|
||||
|
||||
# Copy values into round key arrays
|
||||
j = 0
|
||||
while j < kc and t < round_key_count:
|
||||
self._ke[t // 4][t % 4] = tk[j]
|
||||
self._kd[rounds - (t // 4)][t % 4] = tk[j]
|
||||
j += 1
|
||||
t += 1
|
||||
|
||||
# Inverse-Cipher-ify the decryption round key (FIPS-197 section 5.3)
|
||||
for r in range(1, rounds):
|
||||
for j in range(0, 4):
|
||||
tt = self._kd[r][j]
|
||||
self._kd[r][j] = (
|
||||
self._U1[(tt >> 24) & 0xFF]
|
||||
^ self._U2[(tt >> 16) & 0xFF]
|
||||
^ self._U3[(tt >> 8) & 0xFF]
|
||||
^ self._U4[tt & 0xFF]
|
||||
)
|
||||
|
||||
def encrypt(self, plaintext: bytes) -> bytes:
|
||||
"""Encrypt a block of plain text using the AES block cipher."""
|
||||
|
||||
if len(plaintext) != 16:
|
||||
raise core.InvalidArgumentError(f'wrong block length {len(plaintext)}')
|
||||
|
||||
rounds = len(self._ke) - 1
|
||||
(s1, s2, s3) = [1, 2, 3]
|
||||
a = [0, 0, 0, 0]
|
||||
|
||||
# Convert plaintext to (ints ^ key)
|
||||
t = [
|
||||
(_compact_word(plaintext[4 * i : 4 * i + 4]) ^ self._ke[0][i])
|
||||
for i in range(0, 4)
|
||||
]
|
||||
|
||||
# Apply round transforms
|
||||
for r in range(1, rounds):
|
||||
for i in range(0, 4):
|
||||
a[i] = (
|
||||
self._T1[(t[i] >> 24) & 0xFF]
|
||||
^ self._T2[(t[(i + s1) % 4] >> 16) & 0xFF]
|
||||
^ self._T3[(t[(i + s2) % 4] >> 8) & 0xFF]
|
||||
^ self._T4[t[(i + s3) % 4] & 0xFF]
|
||||
^ self._ke[r][i]
|
||||
)
|
||||
t = copy.copy(a)
|
||||
|
||||
# The last round is special
|
||||
result = []
|
||||
for i in range(0, 4):
|
||||
tt = self._ke[rounds][i]
|
||||
result.append((self._S[(t[i] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF)
|
||||
result.append((self._S[(t[(i + s1) % 4] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF)
|
||||
result.append((self._S[(t[(i + s2) % 4] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF)
|
||||
result.append((self._S[t[(i + s3) % 4] & 0xFF] ^ tt) & 0xFF)
|
||||
|
||||
return bytes(result)
|
||||
|
||||
def decrypt(self, cipher_text: bytes) -> bytes:
|
||||
"""Decrypt a block of cipher text using the AES block cipher."""
|
||||
|
||||
if len(cipher_text) != 16:
|
||||
raise core.InvalidArgumentError(f'wrong block length {len(cipher_text)}')
|
||||
|
||||
rounds = len(self._kd) - 1
|
||||
(s1, s2, s3) = [3, 2, 1]
|
||||
a = [0, 0, 0, 0]
|
||||
|
||||
# Convert ciphertext to (ints ^ key)
|
||||
t = [
|
||||
(_compact_word(cipher_text[4 * i : 4 * i + 4]) ^ self._kd[0][i])
|
||||
for i in range(0, 4)
|
||||
]
|
||||
|
||||
# Apply round transforms
|
||||
for r in range(1, rounds):
|
||||
for i in range(0, 4):
|
||||
a[i] = (
|
||||
self._T5[(t[i] >> 24) & 0xFF]
|
||||
^ self._T6[(t[(i + s1) % 4] >> 16) & 0xFF]
|
||||
^ self._T7[(t[(i + s2) % 4] >> 8) & 0xFF]
|
||||
^ self._T8[t[(i + s3) % 4] & 0xFF]
|
||||
^ self._kd[r][i]
|
||||
)
|
||||
t = copy.copy(a)
|
||||
|
||||
# The last round is special
|
||||
result = []
|
||||
for i in range(0, 4):
|
||||
tt = self._kd[rounds][i]
|
||||
result.append((self._S_INV[(t[i] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF)
|
||||
result.append(
|
||||
(self._S_INV[(t[(i + s1) % 4] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF
|
||||
)
|
||||
result.append(
|
||||
(self._S_INV[(t[(i + s2) % 4] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF
|
||||
)
|
||||
result.append((self._S_INV[t[(i + s3) % 4] & 0xFF] ^ tt) & 0xFF)
|
||||
|
||||
return bytes(result)
|
||||
|
||||
|
||||
class _ECB:
|
||||
def __init__(self, key: bytes):
|
||||
self._aes = _AES(key)
|
||||
|
||||
def encrypt(self, plaintext: bytes) -> bytes:
|
||||
return b"".join(
|
||||
[
|
||||
self._aes.encrypt(
|
||||
plaintext[offset : offset + 16].ljust(16, b"\x00") # Pad 0.
|
||||
)
|
||||
for offset in range(0, len(plaintext), 16)
|
||||
]
|
||||
)
|
||||
|
||||
def decrypt(self, cipher_text: bytes) -> bytes:
|
||||
return b"".join(
|
||||
[
|
||||
self._aes.encrypt(cipher_text[offset : offset + 16])
|
||||
for offset in range(0, len(cipher_text), 16)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class _CBC:
|
||||
def __init__(self, key: bytes, iv: bytes = bytes(16)) -> None:
|
||||
if len(iv) != 16:
|
||||
raise core.InvalidArgumentError(
|
||||
f'initialization vector must be 16 bytes, get {len(iv)}'
|
||||
)
|
||||
else:
|
||||
self._last_cipher_block = iv
|
||||
self._aes = _AES(key)
|
||||
|
||||
def encrypt(self, plaintext: bytes) -> bytes:
|
||||
cipher_text = b""
|
||||
for offset in range(0, len(plaintext), 16):
|
||||
pre_cipher_block = _xor(
|
||||
plaintext[offset : offset + 16], self._last_cipher_block
|
||||
)
|
||||
self._last_cipher_block = self._aes.encrypt(pre_cipher_block)
|
||||
cipher_text += self._last_cipher_block
|
||||
return cipher_text
|
||||
|
||||
def decrypt(self, cipher_text: bytes) -> bytes:
|
||||
plaintext = b""
|
||||
for offset in range(0, len(cipher_text), 16):
|
||||
plaintext += _xor(
|
||||
self._aes.decrypt(cipher_text[offset : offset + 16]),
|
||||
self._last_cipher_block,
|
||||
)
|
||||
self._last_cipher_block = cipher_text[offset : offset + 16]
|
||||
|
||||
return plaintext
|
||||
|
||||
|
||||
class _CMAC:
|
||||
def __init__(
|
||||
self,
|
||||
key: bytes,
|
||||
msg: bytes = bytes(16),
|
||||
mac_len: int = 16,
|
||||
update_after_digest: bool = False,
|
||||
) -> None:
|
||||
self.digest_size = mac_len
|
||||
self._key = key
|
||||
self._block_size = bs = 16
|
||||
self._mac_tag: bytes | None = None
|
||||
self._update_after_digest = update_after_digest
|
||||
|
||||
# Section 5.3 of NIST SP 800 38B and Appendix B
|
||||
if bs == 8:
|
||||
const_Rb = 0x1B
|
||||
self._max_size = 8 * (2**21)
|
||||
elif bs == 16:
|
||||
const_Rb = 0x87
|
||||
self._max_size = 16 * (2**48)
|
||||
else:
|
||||
raise core.InvalidArgumentError(
|
||||
f"CMAC requires a cipher with a block size of 8 or 16 bytes, not {bs}"
|
||||
)
|
||||
|
||||
# Compute sub-keys
|
||||
zero_block = bytes(bs)
|
||||
self._ecb = _ECB(key)
|
||||
L = self._ecb.encrypt(zero_block)
|
||||
if L[0] & 0x80:
|
||||
self._k1 = _shift_bytes(L, const_Rb)
|
||||
else:
|
||||
self._k1 = _shift_bytes(L)
|
||||
if self._k1[0] & 0x80:
|
||||
self._k2 = _shift_bytes(self._k1, const_Rb)
|
||||
else:
|
||||
self._k2 = _shift_bytes(self._k1)
|
||||
|
||||
# Initialize CBC cipher with zero IV
|
||||
self._cbc = _CBC(key, zero_block)
|
||||
|
||||
# Cache for outstanding data to authenticate
|
||||
self._cache = bytearray(bs)
|
||||
self._cache_n = 0
|
||||
|
||||
# Last piece of cipher text produced
|
||||
self._last_ct = zero_block
|
||||
|
||||
# Last block that was encrypted with AES
|
||||
self._last_pt: bytes | None = None
|
||||
|
||||
# Counter for total message size
|
||||
self._data_size = 0
|
||||
|
||||
if msg:
|
||||
self.update(msg)
|
||||
|
||||
def update(self, msg: bytes) -> _CMAC:
|
||||
"""Authenticate the next chunk of message.
|
||||
|
||||
Args:
|
||||
data (byte string/byte array/memoryview): The next chunk of data
|
||||
"""
|
||||
|
||||
if self._mac_tag is not None and not self._update_after_digest:
|
||||
raise core.InvalidStateError(
|
||||
"update() cannot be called after digest() or verify()"
|
||||
)
|
||||
|
||||
self._data_size += len(msg)
|
||||
bs = self._block_size
|
||||
|
||||
if self._cache_n > 0:
|
||||
filler = min(bs - self._cache_n, len(msg))
|
||||
self._cache[self._cache_n : self._cache_n + filler] = msg[:filler]
|
||||
self._cache_n += filler
|
||||
|
||||
if self._cache_n < bs:
|
||||
return self
|
||||
|
||||
msg = msg[filler:]
|
||||
self._update(self._cache)
|
||||
self._cache_n = 0
|
||||
|
||||
remain = len(msg) % bs
|
||||
if remain > 0:
|
||||
self._update(msg[:-remain])
|
||||
self._cache[:remain] = msg[-remain:]
|
||||
else:
|
||||
self._update(msg)
|
||||
self._cache_n = remain
|
||||
return self
|
||||
|
||||
def _update(self, data_block: bytes) -> None:
|
||||
"""Update a block aligned to the block boundary"""
|
||||
|
||||
bs = self._block_size
|
||||
assert len(data_block) % bs == 0
|
||||
|
||||
if len(data_block) == 0:
|
||||
return
|
||||
|
||||
ct = self._cbc.encrypt(data_block)
|
||||
if len(data_block) == bs:
|
||||
second_last = self._last_ct
|
||||
else:
|
||||
second_last = ct[-bs * 2 : -bs]
|
||||
self._last_ct = ct[-bs:]
|
||||
self._last_pt = _xor(second_last, data_block[-bs:])
|
||||
|
||||
def digest(self) -> bytes:
|
||||
bs = self._block_size
|
||||
|
||||
if self._mac_tag is not None and not self._update_after_digest:
|
||||
return self._mac_tag
|
||||
|
||||
if self._data_size > self._max_size:
|
||||
raise core.InvalidArgumentError("MAC is unsafe for this message")
|
||||
|
||||
if self._cache_n == 0 and self._data_size > 0 and self._last_pt:
|
||||
# Last block was full
|
||||
pt = _xor(self._last_pt, self._k1)
|
||||
else:
|
||||
# Last block is partial (or message length is zero)
|
||||
partial = self._cache[:]
|
||||
partial[self._cache_n :] = b'\x80' + b'\x00' * (bs - self._cache_n - 1)
|
||||
pt = _xor(_xor(self._last_ct, partial), self._k2)
|
||||
|
||||
self._mac_tag = self._ecb.encrypt(pt)[: self.digest_size]
|
||||
|
||||
return self._mac_tag
|
||||
|
||||
|
||||
# Define the original Point class for clarity and conversion purposes
|
||||
@dataclasses.dataclass
|
||||
class _Point:
|
||||
"""Represents a point on the elliptic curve in affine coordinates."""
|
||||
|
||||
curve: _EllipticCurve
|
||||
x: int = 0
|
||||
y: int = 0
|
||||
infinite: bool = False
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class _JacobianPoint:
|
||||
"""Represents a point on the elliptic curve in Jacobian coordinates."""
|
||||
|
||||
curve: _EllipticCurve
|
||||
x: int = 1 # For point at infinity (1:1:0)
|
||||
y: int = 1
|
||||
z: int = 0 # z = 0 indicates point at infinity
|
||||
|
||||
@classmethod
|
||||
def point_at_infinity(cls, curve: _EllipticCurve) -> _JacobianPoint:
|
||||
return _JacobianPoint(curve=curve, x=1, y=1, z=0)
|
||||
|
||||
@classmethod
|
||||
def from_affine(cls, affine_point: _Point) -> _JacobianPoint:
|
||||
if affine_point.infinite:
|
||||
return _JacobianPoint.point_at_infinity(affine_point.curve)
|
||||
# A simple conversion is (x, y, 1)
|
||||
return _JacobianPoint(
|
||||
curve=affine_point.curve, x=affine_point.x, y=affine_point.y, z=1
|
||||
)
|
||||
|
||||
def to_affine(self) -> _Point:
|
||||
if self.z == 0:
|
||||
return _Point(infinite=True, curve=self.curve)
|
||||
|
||||
p = self.curve.p
|
||||
inv_z = pow(self.z, -1, p)
|
||||
affine_x = (self.x * inv_z**2) % p
|
||||
affine_y = (self.y * inv_z**3) % p
|
||||
|
||||
return _Point(curve=self.curve, x=affine_x, y=affine_y, infinite=False)
|
||||
|
||||
def double(self) -> _JacobianPoint:
|
||||
if self.z == 0 or self.y == 0:
|
||||
return _JacobianPoint.point_at_infinity(self.curve)
|
||||
|
||||
s = 4 * self.x * self.y**2
|
||||
m = 3 * self.x**2 + self.curve.a * self.z**4
|
||||
x2 = m**2 - 2 * s
|
||||
y2 = m * (s - x2) - 8 * self.y**4
|
||||
z2 = 2 * self.y * self.z
|
||||
p = self.curve.p
|
||||
|
||||
return _JacobianPoint(curve=self.curve, x=x2 % p, y=y2 % p, z=z2 % p)
|
||||
|
||||
def __add__(self, other: _JacobianPoint) -> _JacobianPoint:
|
||||
if self.z == 0 and other.z == 0:
|
||||
return _JacobianPoint.point_at_infinity(self.curve)
|
||||
elif self.z == 0:
|
||||
return other
|
||||
elif other.z == 0:
|
||||
return self
|
||||
|
||||
x1 = self.x
|
||||
y1 = self.y
|
||||
z1 = self.z
|
||||
x2 = other.x
|
||||
y2 = other.y
|
||||
z2 = other.z
|
||||
p = self.curve.p
|
||||
u1 = (x1 * z2**2) % p
|
||||
u2 = (x2 * z1**2) % p
|
||||
s1 = (y1 * z2**3) % p
|
||||
s2 = (y2 * z1**3) % p
|
||||
|
||||
if u1 == u2:
|
||||
if s1 != s2:
|
||||
return _JacobianPoint.point_at_infinity(self.curve)
|
||||
else:
|
||||
return self.double()
|
||||
else:
|
||||
h = u2 - u1
|
||||
r = s2 - s1
|
||||
|
||||
h3 = h**3 % p
|
||||
u1h2 = (u1 * h**2) % p
|
||||
x3 = r**2 - h3 - 2 * u1h2
|
||||
y3 = r * (u1h2 - x3) - s1 * h3
|
||||
z3 = h * z1 * z2
|
||||
|
||||
return _JacobianPoint(self.curve, x3 % p, y3 % p, z3 % p)
|
||||
|
||||
def __mul__(self, k: int) -> _JacobianPoint:
|
||||
addend = self
|
||||
result = _JacobianPoint.point_at_infinity(self.curve)
|
||||
|
||||
while k > 0:
|
||||
if k % 2 != 0:
|
||||
result = result + addend
|
||||
addend = addend.double()
|
||||
k = k >> 1
|
||||
return result
|
||||
|
||||
def __rmul__(self, k: int) -> _JacobianPoint:
|
||||
return self * k
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class _EllipticCurve:
|
||||
p: int
|
||||
a: int
|
||||
b: int
|
||||
n: int
|
||||
g_x: int
|
||||
g_y: int
|
||||
|
||||
_generator_jacobian: _JacobianPoint = dataclasses.field(init=False)
|
||||
|
||||
def __post_init__(self):
|
||||
self._generator_jacobian = _JacobianPoint(
|
||||
curve=self, x=self.g_x, y=self.g_y, z=1
|
||||
)
|
||||
|
||||
@dataclasses.dataclass
|
||||
class PrivateKey:
|
||||
key: int
|
||||
curve: _EllipticCurve
|
||||
|
||||
def generate_private_key(self) -> PrivateKey:
|
||||
"""Generates a random private key."""
|
||||
return self.PrivateKey(key=secrets.randbelow(self.n), curve=self)
|
||||
|
||||
def generate_public_key(self, private_key: int) -> _Point:
|
||||
"""Generates a public key from a private key using Jacobian coordinates for scalar multiplication."""
|
||||
public_key_jacobian = self._generator_jacobian * private_key
|
||||
return public_key_jacobian.to_affine()
|
||||
|
||||
def ecdh_shared_secret(self, private_key: int, other_public_key: _Point) -> bytes:
|
||||
"""Computes the shared secret using ECDH."""
|
||||
other_public_key_jacobian = _JacobianPoint.from_affine(other_public_key)
|
||||
shared_point_jacobian = other_public_key_jacobian * private_key
|
||||
shared_point_affine = shared_point_jacobian.to_affine()
|
||||
if shared_point_affine.infinite:
|
||||
raise core.InvalidPacketError(
|
||||
"Shared secret calculation resulted in the point at infinite"
|
||||
)
|
||||
return shared_point_affine.x.to_bytes(32, 'big')
|
||||
|
||||
@classmethod
|
||||
def SECP256R1(cls) -> _EllipticCurve:
|
||||
p = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF
|
||||
a = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC
|
||||
b = 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B
|
||||
n = 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551 # Curve order
|
||||
g_x = 0x6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296
|
||||
g_y = 0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5
|
||||
|
||||
return _EllipticCurve(p=p, a=a, b=b, n=n, g_x=g_x, g_y=g_y)
|
||||
|
||||
|
||||
class EccKey:
|
||||
def __init__(self, private_key: _EllipticCurve.PrivateKey) -> None:
|
||||
self.private_key = private_key
|
||||
|
||||
@functools.cached_property
|
||||
def x(self) -> bytes:
|
||||
return self.private_key.curve.generate_public_key(
|
||||
self.private_key.key
|
||||
).x.to_bytes(32, byteorder='big')
|
||||
|
||||
@functools.cached_property
|
||||
def y(self) -> bytes:
|
||||
return self.private_key.curve.generate_public_key(
|
||||
self.private_key.key
|
||||
).y.to_bytes(32, byteorder='big')
|
||||
|
||||
def dh(self, public_key_x: bytes, public_key_y: bytes) -> bytes:
|
||||
x = int.from_bytes(public_key_x, byteorder='big', signed=False)
|
||||
y = int.from_bytes(public_key_y, byteorder='big', signed=False)
|
||||
return self.private_key.curve.ecdh_shared_secret(
|
||||
self.private_key.key,
|
||||
_Point(x=x, y=y, curve=self.private_key.curve),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def generate(cls) -> EccKey:
|
||||
return EccKey(_EllipticCurve.SECP256R1().generate_private_key())
|
||||
|
||||
@classmethod
|
||||
def from_private_key_bytes(cls, d_bytes: bytes) -> EccKey:
|
||||
d = int.from_bytes(d_bytes, byteorder='big', signed=False)
|
||||
return EccKey(_EllipticCurve.PrivateKey(d, _EllipticCurve.SECP256R1()))
|
||||
|
||||
|
||||
def e(key: bytes, data: bytes) -> bytes:
|
||||
'''
|
||||
AES-128 ECB, expecting byte-swapped inputs and producing a byte-swapped output.
|
||||
|
||||
See Bluetooth spec Vol 3, Part H - 2.2.1 Security function e
|
||||
'''
|
||||
|
||||
return _ECB(key[::-1]).encrypt(data[::-1])[::-1]
|
||||
|
||||
|
||||
def aes_cmac(m: bytes, k: bytes) -> bytes:
|
||||
'''
|
||||
See Bluetooth spec, Vol 3, Part H - 2.2.5 FunctionAES-CMAC
|
||||
|
||||
NOTE: the input and output of this internal function are in big-endian byte order
|
||||
'''
|
||||
return _CMAC(key=k, msg=m).digest()
|
||||
@@ -1,82 +0,0 @@
|
||||
# Copyright 2021-2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License")
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
|
||||
from cryptography.hazmat.primitives import ciphers, cmac
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
from cryptography.hazmat.primitives.ciphers import algorithms, modes
|
||||
|
||||
|
||||
def e(key: bytes, data: bytes) -> bytes:
|
||||
'''
|
||||
AES-128 ECB, expecting byte-swapped inputs and producing a byte-swapped output.
|
||||
|
||||
See Bluetooth spec Vol 3, Part H - 2.2.1 Security function e
|
||||
'''
|
||||
|
||||
cipher = ciphers.Cipher(algorithms.AES(key[::-1]), modes.ECB())
|
||||
encryptor = cipher.encryptor()
|
||||
return encryptor.update(data[::-1])[::-1]
|
||||
|
||||
|
||||
class EccKey:
|
||||
def __init__(self, private_key: ec.EllipticCurvePrivateKey) -> None:
|
||||
self.private_key = private_key
|
||||
|
||||
@classmethod
|
||||
def generate(cls) -> EccKey:
|
||||
return EccKey(ec.generate_private_key(ec.SECP256R1()))
|
||||
|
||||
@classmethod
|
||||
def from_private_key_bytes(cls, d_bytes: bytes) -> EccKey:
|
||||
d = int.from_bytes(d_bytes, byteorder='big', signed=False)
|
||||
return EccKey(ec.derive_private_key(d, ec.SECP256R1()))
|
||||
|
||||
@functools.cached_property
|
||||
def x(self) -> bytes:
|
||||
return (
|
||||
self.private_key.public_key()
|
||||
.public_numbers()
|
||||
.x.to_bytes(32, byteorder='big')
|
||||
)
|
||||
|
||||
@functools.cached_property
|
||||
def y(self) -> bytes:
|
||||
return (
|
||||
self.private_key.public_key()
|
||||
.public_numbers()
|
||||
.y.to_bytes(32, byteorder='big')
|
||||
)
|
||||
|
||||
def dh(self, public_key_x: bytes, public_key_y: bytes) -> bytes:
|
||||
x = int.from_bytes(public_key_x, byteorder='big', signed=False)
|
||||
y = int.from_bytes(public_key_y, byteorder='big', signed=False)
|
||||
return self.private_key.exchange(
|
||||
ec.ECDH(),
|
||||
ec.EllipticCurvePublicNumbers(x, y, ec.SECP256R1()).public_key(),
|
||||
)
|
||||
|
||||
|
||||
def aes_cmac(m: bytes, k: bytes) -> bytes:
|
||||
'''
|
||||
See Bluetooth spec, Vol 3, Part H - 2.2.5 FunctionAES-CMAC
|
||||
|
||||
NOTE: the input and output of this internal function are in big-endian byte order
|
||||
'''
|
||||
mac = cmac.CMAC(algorithms.AES(k))
|
||||
mac.update(m)
|
||||
return mac.finalize()
|
||||
1026
bumble/data_types.py
1026
bumble/data_types.py
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from typing import Union
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
@@ -166,12 +167,12 @@ class G722Decoder:
|
||||
# The initial value in BLOCK 3H
|
||||
self._band[1].det = 8
|
||||
|
||||
def decode_frame(self, encoded_data: bytes | bytearray) -> bytearray:
|
||||
def decode_frame(self, encoded_data: Union[bytes, bytearray]) -> bytearray:
|
||||
result_array = bytearray(len(encoded_data) * 4)
|
||||
self.g722_decode(result_array, encoded_data)
|
||||
return result_array
|
||||
|
||||
def g722_decode(self, result_array, encoded_data: bytes | bytearray) -> int:
|
||||
def g722_decode(self, result_array, encoded_data: Union[bytes, bytearray]) -> int:
|
||||
"""Decode the data frame using g722 decoder."""
|
||||
result_length = 0
|
||||
|
||||
|
||||
1680
bumble/device.py
1680
bumble/device.py
File diff suppressed because it is too large
Load Diff
@@ -20,14 +20,12 @@ like loading firmware after a cold start.
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import pathlib
|
||||
import platform
|
||||
from collections.abc import Iterable
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Dict, Iterable, Optional, Type, TYPE_CHECKING
|
||||
|
||||
from bumble.drivers import intel, rtk
|
||||
from bumble.drivers import rtk, intel
|
||||
from bumble.drivers.common import Driver
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -42,18 +40,14 @@ logger = logging.getLogger(__name__)
|
||||
# -----------------------------------------------------------------------------
|
||||
# Functions
|
||||
# -----------------------------------------------------------------------------
|
||||
async def get_driver_for_host(host: Host) -> Driver | None:
|
||||
async def get_driver_for_host(host: Host) -> Optional[Driver]:
|
||||
"""Probe diver classes until one returns a valid instance for a host, or none is
|
||||
found.
|
||||
If a "driver" HCI metadata entry is present, only that driver class will be probed.
|
||||
"""
|
||||
driver_classes: dict[str, type[Driver]] = {"rtk": rtk.Driver, "intel": intel.Driver}
|
||||
driver_classes: Dict[str, Type[Driver]] = {"rtk": rtk.Driver, "intel": intel.Driver}
|
||||
probe_list: Iterable[str]
|
||||
if driver_name := host.hci_metadata.get("driver"):
|
||||
# The "driver" metadata may include runtime options after a '/' (for example
|
||||
# "intel/ddc=..."). Keep only the base driver name (the portion before the
|
||||
# first slash) so it matches a key in driver_classes (e.g. "intel").
|
||||
driver_name = driver_name.split("/")[0]
|
||||
# Only probe a single driver
|
||||
probe_list = [driver_name]
|
||||
else:
|
||||
|
||||
@@ -20,6 +20,8 @@ Common types for drivers.
|
||||
# -----------------------------------------------------------------------------
|
||||
import abc
|
||||
|
||||
from bumble import core
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Classes
|
||||
|
||||
@@ -20,7 +20,6 @@ Loosely based on the Fuchsia OS implementation.
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import collections
|
||||
import dataclasses
|
||||
@@ -29,10 +28,12 @@ import os
|
||||
import pathlib
|
||||
import platform
|
||||
import struct
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import Any, Deque, Optional, TYPE_CHECKING
|
||||
|
||||
from bumble import core, hci, utils
|
||||
from bumble import core
|
||||
from bumble.drivers import common
|
||||
from bumble import hci
|
||||
from bumble import utils
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bumble.host import Host
|
||||
@@ -49,7 +50,6 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
INTEL_USB_PRODUCTS = {
|
||||
(0x8087, 0x0032), # AX210
|
||||
(0x8087, 0x0033), # AX211
|
||||
(0x8087, 0x0036), # BE200
|
||||
}
|
||||
|
||||
@@ -89,51 +89,54 @@ HCI_INTEL_WRITE_BOOT_PARAMS_COMMAND = hci.hci_vendor_command_op_code(0x000E)
|
||||
hci.HCI_Command.register_commands(globals())
|
||||
|
||||
|
||||
@hci.HCI_Command.command
|
||||
@dataclasses.dataclass
|
||||
class HCI_Intel_Read_Version_Command(hci.HCI_Command):
|
||||
param0: int = dataclasses.field(metadata=hci.metadata(1))
|
||||
|
||||
return_parameters_fields = [
|
||||
@hci.HCI_Command.command(
|
||||
fields=[
|
||||
("param0", 1),
|
||||
],
|
||||
return_parameters_fields=[
|
||||
("status", hci.STATUS_SPEC),
|
||||
("tlv", "*"),
|
||||
]
|
||||
],
|
||||
)
|
||||
class HCI_Intel_Read_Version_Command(hci.HCI_Command):
|
||||
pass
|
||||
|
||||
|
||||
@hci.HCI_Command.command
|
||||
@dataclasses.dataclass
|
||||
class Hci_Intel_Secure_Send_Command(hci.HCI_Command):
|
||||
data_type: int = dataclasses.field(metadata=hci.metadata(1))
|
||||
data: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
|
||||
return_parameters_fields = [
|
||||
@hci.HCI_Command.command(
|
||||
fields=[("data_type", 1), ("data", "*")],
|
||||
return_parameters_fields=[
|
||||
("status", 1),
|
||||
]
|
||||
],
|
||||
)
|
||||
class Hci_Intel_Secure_Send_Command(hci.HCI_Command):
|
||||
pass
|
||||
|
||||
|
||||
@hci.HCI_Command.command
|
||||
@dataclasses.dataclass
|
||||
class HCI_Intel_Reset_Command(hci.HCI_Command):
|
||||
reset_type: int = dataclasses.field(metadata=hci.metadata(1))
|
||||
patch_enable: int = dataclasses.field(metadata=hci.metadata(1))
|
||||
ddc_reload: int = dataclasses.field(metadata=hci.metadata(1))
|
||||
boot_option: int = dataclasses.field(metadata=hci.metadata(1))
|
||||
boot_address: int = dataclasses.field(metadata=hci.metadata(4))
|
||||
|
||||
return_parameters_fields = [
|
||||
@hci.HCI_Command.command(
|
||||
fields=[
|
||||
("reset_type", 1),
|
||||
("patch_enable", 1),
|
||||
("ddc_reload", 1),
|
||||
("boot_option", 1),
|
||||
("boot_address", 4),
|
||||
],
|
||||
return_parameters_fields=[
|
||||
("data", "*"),
|
||||
]
|
||||
],
|
||||
)
|
||||
class HCI_Intel_Reset_Command(hci.HCI_Command):
|
||||
pass
|
||||
|
||||
|
||||
@hci.HCI_Command.command
|
||||
@dataclasses.dataclass
|
||||
class Hci_Intel_Write_Device_Config_Command(hci.HCI_Command):
|
||||
data: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
|
||||
return_parameters_fields = [
|
||||
@hci.HCI_Command.command(
|
||||
fields=[("data", "*")],
|
||||
return_parameters_fields=[
|
||||
("status", hci.STATUS_SPEC),
|
||||
("params", "*"),
|
||||
]
|
||||
],
|
||||
)
|
||||
class Hci_Intel_Write_Device_Config_Command(hci.HCI_Command):
|
||||
pass
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -290,7 +293,6 @@ class HardwareVariant(utils.OpenIntEnum):
|
||||
# This is a just a partial list.
|
||||
# Add other constants here as new hardware is encountered and tested.
|
||||
TYPHOON_PEAK = 0x17
|
||||
GARFIELD_PEAK = 0x19
|
||||
GALE_PEAK = 0x1C
|
||||
|
||||
|
||||
@@ -344,7 +346,7 @@ class Driver(common.Driver):
|
||||
def __init__(self, host: Host) -> None:
|
||||
self.host = host
|
||||
self.max_in_flight_firmware_load_commands = 1
|
||||
self.pending_firmware_load_commands: collections.deque[hci.HCI_Command] = (
|
||||
self.pending_firmware_load_commands: Deque[hci.HCI_Command] = (
|
||||
collections.deque()
|
||||
)
|
||||
self.can_send_firmware_load_command = asyncio.Event()
|
||||
@@ -353,8 +355,8 @@ class Driver(common.Driver):
|
||||
self.reset_complete = asyncio.Event()
|
||||
|
||||
# Parse configuration options from the driver name.
|
||||
self.ddc_addon: bytes | None = None
|
||||
self.ddc_override: bytes | None = None
|
||||
self.ddc_addon: Optional[bytes] = None
|
||||
self.ddc_override: Optional[bytes] = None
|
||||
driver = host.hci_metadata.get("driver")
|
||||
if driver is not None and driver.startswith("intel/"):
|
||||
for key, value in [
|
||||
@@ -380,7 +382,7 @@ class Driver(common.Driver):
|
||||
|
||||
if (vendor_id, product_id) not in INTEL_USB_PRODUCTS:
|
||||
logger.debug(
|
||||
f"USB device ({vendor_id:04X}, {product_id:04X}) not in known list"
|
||||
f"USB device ({vendor_id:04X}, {product_id:04X}) " "not in known list"
|
||||
)
|
||||
return False
|
||||
|
||||
@@ -459,10 +461,6 @@ class Driver(common.Driver):
|
||||
== ModeOfOperation.OPERATIONAL
|
||||
):
|
||||
logger.debug("firmware already loaded")
|
||||
# If the firmeare is already loaded, still attempt to load any
|
||||
# device configuration (DDC). DDC can be applied independently of a
|
||||
# firmware reload and may contain runtime overrides or patches.
|
||||
await self.load_ddc_if_any()
|
||||
return
|
||||
|
||||
# We only support some platforms and variants.
|
||||
@@ -473,7 +471,6 @@ class Driver(common.Driver):
|
||||
raise DriverError("hardware platform not supported")
|
||||
if hardware_info.variant not in (
|
||||
HardwareVariant.TYPHOON_PEAK,
|
||||
HardwareVariant.GARFIELD_PEAK,
|
||||
HardwareVariant.GALE_PEAK,
|
||||
):
|
||||
raise DriverError("hardware variant not supported")
|
||||
@@ -483,7 +480,9 @@ class Driver(common.Driver):
|
||||
raise DriverError("insufficient device info, missing CNVI or CNVR")
|
||||
|
||||
firmware_base_name = (
|
||||
f"ibt-{device_info[ValueType.CNVI]:04X}-{device_info[ValueType.CNVR]:04X}"
|
||||
"ibt-"
|
||||
f"{device_info[ValueType.CNVI]:04X}-"
|
||||
f"{device_info[ValueType.CNVR]:04X}"
|
||||
)
|
||||
logger.debug(f"FW base name: {firmware_base_name}")
|
||||
|
||||
@@ -600,39 +599,17 @@ class Driver(common.Driver):
|
||||
await self.reset_complete.wait()
|
||||
logger.debug("reset complete")
|
||||
|
||||
await self.load_ddc_if_any(firmware_base_name)
|
||||
|
||||
async def load_ddc_if_any(self, firmware_base_name: str | None = None) -> None:
|
||||
"""
|
||||
Check for and load any Device Data Configuration (DDC) blobs.
|
||||
|
||||
Args:
|
||||
firmware_base_name: Base name of the selected firmware (e.g. "ibt-XXXX-YYYY").
|
||||
If None, don't attempt to look up a .ddc file that
|
||||
corresponds to the firmware image.
|
||||
Priority:
|
||||
1. If a ddc_override was provided via driver metadata, use it (highest priority).
|
||||
2. Otherwise, if firmware_base_name is provided, attempt to find a .ddc file
|
||||
that corresponds to the selected firmware image.
|
||||
3. Finally, if a ddc_addon was provided, append/load it after the primary DDC.
|
||||
"""
|
||||
# If an explicit DDC override was supplied, use it and skip file lookup.
|
||||
# Load the device config if there is one.
|
||||
if self.ddc_override:
|
||||
logger.debug("loading overridden DDC")
|
||||
await self.load_device_config(self.ddc_override)
|
||||
else:
|
||||
# Only attempt .ddc file lookup if a firmware_base_name was provided.
|
||||
if firmware_base_name is None:
|
||||
logger.debug(
|
||||
"no firmware_base_name provided; skipping .ddc file lookup"
|
||||
)
|
||||
else:
|
||||
ddc_name = f"{firmware_base_name}.ddc"
|
||||
ddc_path = _find_binary_path(ddc_name)
|
||||
if ddc_path:
|
||||
logger.debug(f"loading DDC from {ddc_path}")
|
||||
ddc_data = ddc_path.read_bytes()
|
||||
await self.load_device_config(ddc_data)
|
||||
ddc_name = f"{firmware_base_name}.ddc"
|
||||
ddc_path = _find_binary_path(ddc_name)
|
||||
if ddc_path:
|
||||
logger.debug(f"loading DDC from {ddc_path}")
|
||||
ddc_data = ddc_path.read_bytes()
|
||||
await self.load_device_config(ddc_data)
|
||||
if self.ddc_addon:
|
||||
logger.debug("loading DDC addon")
|
||||
await self.load_device_config(self.ddc_addon)
|
||||
|
||||
@@ -17,6 +17,10 @@ Based on various online bits of information, including the Linux kernel.
|
||||
(see `drivers/bluetooth/btrtl.c`)
|
||||
"""
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from dataclasses import dataclass
|
||||
import asyncio
|
||||
import enum
|
||||
import logging
|
||||
@@ -25,14 +29,19 @@ import os
|
||||
import pathlib
|
||||
import platform
|
||||
import struct
|
||||
from typing import Tuple
|
||||
import weakref
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from bumble import core, hci
|
||||
from bumble import core
|
||||
from bumble.hci import (
|
||||
hci_vendor_command_op_code,
|
||||
STATUS_SPEC,
|
||||
HCI_SUCCESS,
|
||||
HCI_Command,
|
||||
HCI_Reset_Command,
|
||||
HCI_Read_Local_Version_Information_Command,
|
||||
)
|
||||
from bumble.drivers import common
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -115,14 +124,12 @@ RTK_USB_PRODUCTS = {
|
||||
# Realtek 8761BUV
|
||||
(0x0B05, 0x190E),
|
||||
(0x0BDA, 0x8771),
|
||||
(0x0BDA, 0x877B),
|
||||
(0x0BDA, 0xA728),
|
||||
(0x0BDA, 0xA729),
|
||||
(0x2230, 0x0016),
|
||||
(0x2357, 0x0604),
|
||||
(0x2550, 0x8761),
|
||||
(0x2B89, 0x8761),
|
||||
(0x7392, 0xC611),
|
||||
(0x0BDA, 0x877B),
|
||||
# Realtek 8821AE
|
||||
(0x0B05, 0x17DC),
|
||||
(0x13D3, 0x3414),
|
||||
@@ -176,29 +183,27 @@ RTK_USB_PRODUCTS = {
|
||||
# -----------------------------------------------------------------------------
|
||||
# HCI Commands
|
||||
# -----------------------------------------------------------------------------
|
||||
HCI_RTK_READ_ROM_VERSION_COMMAND = hci.hci_vendor_command_op_code(0x6D)
|
||||
HCI_RTK_DOWNLOAD_COMMAND = hci.hci_vendor_command_op_code(0x20)
|
||||
HCI_RTK_DROP_FIRMWARE_COMMAND = hci.hci_vendor_command_op_code(0x66)
|
||||
hci.HCI_Command.register_commands(globals())
|
||||
HCI_RTK_READ_ROM_VERSION_COMMAND = hci_vendor_command_op_code(0x6D)
|
||||
HCI_RTK_DOWNLOAD_COMMAND = hci_vendor_command_op_code(0x20)
|
||||
HCI_RTK_DROP_FIRMWARE_COMMAND = hci_vendor_command_op_code(0x66)
|
||||
HCI_Command.register_commands(globals())
|
||||
|
||||
|
||||
@hci.HCI_Command.command
|
||||
@dataclass
|
||||
class HCI_RTK_Read_ROM_Version_Command(hci.HCI_Command):
|
||||
return_parameters_fields = [("status", hci.STATUS_SPEC), ("version", 1)]
|
||||
@HCI_Command.command(return_parameters_fields=[("status", STATUS_SPEC), ("version", 1)])
|
||||
class HCI_RTK_Read_ROM_Version_Command(HCI_Command):
|
||||
pass
|
||||
|
||||
|
||||
@hci.HCI_Command.command
|
||||
@dataclass
|
||||
class HCI_RTK_Download_Command(hci.HCI_Command):
|
||||
index: int = field(metadata=hci.metadata(1))
|
||||
payload: bytes = field(metadata=hci.metadata(RTK_FRAGMENT_LENGTH))
|
||||
return_parameters_fields = [("status", hci.STATUS_SPEC), ("index", 1)]
|
||||
@HCI_Command.command(
|
||||
fields=[("index", 1), ("payload", RTK_FRAGMENT_LENGTH)],
|
||||
return_parameters_fields=[("status", STATUS_SPEC), ("index", 1)],
|
||||
)
|
||||
class HCI_RTK_Download_Command(HCI_Command):
|
||||
pass
|
||||
|
||||
|
||||
@hci.HCI_Command.command
|
||||
@dataclass
|
||||
class HCI_RTK_Drop_Firmware_Command(hci.HCI_Command):
|
||||
@HCI_Command.command()
|
||||
class HCI_RTK_Drop_Firmware_Command(HCI_Command):
|
||||
pass
|
||||
|
||||
|
||||
@@ -289,7 +294,7 @@ class Driver(common.Driver):
|
||||
@dataclass
|
||||
class DriverInfo:
|
||||
rom: int
|
||||
hci: tuple[int, int]
|
||||
hci: Tuple[int, int]
|
||||
config_needed: bool
|
||||
has_rom_version: bool
|
||||
has_msft_ext: bool = False
|
||||
@@ -484,42 +489,27 @@ class Driver(common.Driver):
|
||||
|
||||
if (vendor_id, product_id) not in RTK_USB_PRODUCTS:
|
||||
logger.debug(
|
||||
f"USB device ({vendor_id:04X}, {product_id:04X}) not in known list"
|
||||
f"USB device ({vendor_id:04X}, {product_id:04X}) " "not in known list"
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
async def get_loaded_firmware_version(host):
|
||||
response = await host.send_command(HCI_RTK_Read_ROM_Version_Command())
|
||||
|
||||
if response.return_parameters.status != hci.HCI_SUCCESS:
|
||||
return None
|
||||
|
||||
response = await host.send_command(
|
||||
hci.HCI_Read_Local_Version_Information_Command(), check_result=True
|
||||
)
|
||||
return (
|
||||
response.return_parameters.hci_subversion << 16
|
||||
| response.return_parameters.lmp_subversion
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def driver_info_for_host(cls, host):
|
||||
try:
|
||||
await host.send_command(
|
||||
hci.HCI_Reset_Command(),
|
||||
HCI_Reset_Command(),
|
||||
check_result=True,
|
||||
response_timeout=cls.POST_RESET_DELAY,
|
||||
)
|
||||
host.ready = True # Needed to let the host know the controller is ready.
|
||||
except asyncio.exceptions.TimeoutError:
|
||||
logger.warning("timeout waiting for hci reset, retrying")
|
||||
await host.send_command(hci.HCI_Reset_Command(), check_result=True)
|
||||
await host.send_command(HCI_Reset_Command(), check_result=True)
|
||||
host.ready = True
|
||||
|
||||
command = hci.HCI_Read_Local_Version_Information_Command()
|
||||
command = HCI_Read_Local_Version_Information_Command()
|
||||
response = await host.send_command(command, check_result=True)
|
||||
if response.command_opcode != command.op_code:
|
||||
logger.error("failed to probe local version information")
|
||||
@@ -606,9 +596,9 @@ class Driver(common.Driver):
|
||||
response = await self.host.send_command(
|
||||
HCI_RTK_Read_ROM_Version_Command(), check_result=True
|
||||
)
|
||||
if response.return_parameters.status != hci.HCI_SUCCESS:
|
||||
if response.return_parameters.status != HCI_SUCCESS:
|
||||
logger.warning("can't get ROM version")
|
||||
return None
|
||||
return
|
||||
rom_version = response.return_parameters.version
|
||||
logger.debug(f"ROM version before download: {rom_version:04X}")
|
||||
else:
|
||||
@@ -616,14 +606,13 @@ class Driver(common.Driver):
|
||||
|
||||
firmware = Firmware(self.firmware)
|
||||
logger.debug(f"firmware: project_id=0x{firmware.project_id:04X}")
|
||||
logger.debug(f"firmware: version=0x{firmware.version:04X}")
|
||||
for patch in firmware.patches:
|
||||
if patch[0] == rom_version + 1:
|
||||
logger.debug(f"using patch {patch[0]}")
|
||||
break
|
||||
else:
|
||||
logger.warning("no valid patch found for rom version {rom_version}")
|
||||
return None
|
||||
return
|
||||
|
||||
# Append the config if there is one.
|
||||
if self.config:
|
||||
@@ -645,8 +634,9 @@ class Driver(common.Driver):
|
||||
fragment = payload[fragment_offset : fragment_offset + RTK_FRAGMENT_LENGTH]
|
||||
logger.debug(f"downloading fragment {fragment_index}")
|
||||
await self.host.send_command(
|
||||
HCI_RTK_Download_Command(index=download_index, payload=fragment),
|
||||
check_result=True,
|
||||
HCI_RTK_Download_Command(
|
||||
index=download_index, payload=fragment, check_result=True
|
||||
)
|
||||
)
|
||||
|
||||
logger.debug("download complete!")
|
||||
@@ -655,13 +645,11 @@ class Driver(common.Driver):
|
||||
response = await self.host.send_command(
|
||||
HCI_RTK_Read_ROM_Version_Command(), check_result=True
|
||||
)
|
||||
if response.return_parameters.status != hci.HCI_SUCCESS:
|
||||
if response.return_parameters.status != HCI_SUCCESS:
|
||||
logger.warning("can't get ROM version")
|
||||
else:
|
||||
rom_version = response.return_parameters.version
|
||||
logger.debug(f"ROM version after download: {rom_version:02X}")
|
||||
|
||||
return firmware.version
|
||||
logger.debug(f"ROM version after download: {rom_version:04X}")
|
||||
|
||||
async def download_firmware(self):
|
||||
if self.driver_info.rom == RTK_ROM_LMP_8723A:
|
||||
@@ -680,7 +668,7 @@ class Driver(common.Driver):
|
||||
|
||||
async def init_controller(self):
|
||||
await self.download_firmware()
|
||||
await self.host.send_command(hci.HCI_Reset_Command(), check_result=True)
|
||||
await self.host.send_command(HCI_Reset_Command(), check_result=True)
|
||||
logger.info(f"loaded FW image {self.driver_info.fw_name}")
|
||||
|
||||
|
||||
|
||||
@@ -19,11 +19,11 @@ import logging
|
||||
import struct
|
||||
|
||||
from bumble.gatt import (
|
||||
GATT_APPEARANCE_CHARACTERISTIC,
|
||||
GATT_DEVICE_NAME_CHARACTERISTIC,
|
||||
GATT_GENERIC_ACCESS_SERVICE,
|
||||
Characteristic,
|
||||
Service,
|
||||
Characteristic,
|
||||
GATT_GENERIC_ACCESS_SERVICE,
|
||||
GATT_DEVICE_NAME_CHARACTERISTIC,
|
||||
GATT_APPEARANCE_CHARACTERISTIC,
|
||||
)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -23,17 +23,15 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
import functools
|
||||
import logging
|
||||
import struct
|
||||
from collections.abc import Iterable, Sequence
|
||||
from typing import TypeVar
|
||||
from typing import Iterable, List, Optional, Sequence, TypeVar, Union
|
||||
|
||||
from bumble.att import Attribute, AttributeValue, AttributeValueV2
|
||||
from bumble.colors import color
|
||||
from bumble.core import UUID, BaseBumbleError
|
||||
from bumble.core import BaseBumbleError, UUID
|
||||
from bumble.att import Attribute, AttributeValue
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Typing
|
||||
@@ -228,6 +226,7 @@ GATT_MEDIA_CONTROL_POINT_CHARACTERISTIC = UUID.from_16_bits(0x
|
||||
GATT_MEDIA_CONTROL_POINT_OPCODES_SUPPORTED_CHARACTERISTIC = UUID.from_16_bits(0x2BA5, 'Media Control Point Opcodes Supported')
|
||||
GATT_SEARCH_RESULTS_OBJECT_ID_CHARACTERISTIC = UUID.from_16_bits(0x2BA6, 'Search Results Object ID')
|
||||
GATT_SEARCH_CONTROL_POINT_CHARACTERISTIC = UUID.from_16_bits(0x2BA7, 'Search Control Point')
|
||||
GATT_CONTENT_CONTROL_ID_CHARACTERISTIC = UUID.from_16_bits(0x2BBA, 'Content Control Id')
|
||||
|
||||
# Telephone Bearer Service (TBS)
|
||||
GATT_BEARER_PROVIDER_NAME_CHARACTERISTIC = UUID.from_16_bits(0x2BB3, 'Bearer Provider Name')
|
||||
@@ -351,12 +350,12 @@ class Service(Attribute):
|
||||
'''
|
||||
|
||||
uuid: UUID
|
||||
characteristics: list[Characteristic]
|
||||
included_services: list[Service]
|
||||
characteristics: List[Characteristic]
|
||||
included_services: List[Service]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
uuid: str | UUID,
|
||||
uuid: Union[str, UUID],
|
||||
characteristics: Iterable[Characteristic],
|
||||
primary=True,
|
||||
included_services: Iterable[Service] = (),
|
||||
@@ -379,7 +378,7 @@ class Service(Attribute):
|
||||
self.characteristics = list(characteristics)
|
||||
self.primary = primary
|
||||
|
||||
def get_advertising_data(self) -> bytes | None:
|
||||
def get_advertising_data(self) -> Optional[bytes]:
|
||||
"""
|
||||
Get Service specific advertising data
|
||||
Defined by each Service, default value is empty
|
||||
@@ -475,7 +474,7 @@ class Characteristic(Attribute[_T]):
|
||||
# The check for `p.name is not None` here is needed because for InFlag
|
||||
# enums, the .name property can be None, when the enum value is 0,
|
||||
# so the type hint for .name is Optional[str].
|
||||
enum_list: list[str] = [p.name for p in cls if p.name is not None]
|
||||
enum_list: List[str] = [p.name for p in cls if p.name is not None]
|
||||
enum_list_str = ",".join(enum_list)
|
||||
raise TypeError(
|
||||
f"Characteristic.Properties::from_string() error:\nExpected a string containing any of the keys, separated by , or |: {enum_list_str}\nGot: {properties_str}"
|
||||
@@ -503,10 +502,10 @@ class Characteristic(Attribute[_T]):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
uuid: str | bytes | UUID,
|
||||
uuid: Union[str, bytes, UUID],
|
||||
properties: Characteristic.Properties,
|
||||
permissions: str | Attribute.Permissions,
|
||||
value: AttributeValue[_T] | _T | None = None,
|
||||
permissions: Union[str, Attribute.Permissions],
|
||||
value: Union[AttributeValue[_T], _T, None] = None,
|
||||
descriptors: Sequence[Descriptor] = (),
|
||||
):
|
||||
super().__init__(uuid, permissions, value)
|
||||
@@ -579,8 +578,12 @@ class Descriptor(Attribute):
|
||||
def __str__(self) -> str:
|
||||
if isinstance(self.value, bytes):
|
||||
value_str = self.value.hex()
|
||||
elif isinstance(self.value, (AttributeValue, AttributeValueV2)):
|
||||
value_str = '<dynamic>'
|
||||
elif isinstance(self.value, CharacteristicValue):
|
||||
value = self.value.read(None)
|
||||
if isinstance(value, bytes):
|
||||
value_str = value.hex()
|
||||
else:
|
||||
value_str = '<async>'
|
||||
else:
|
||||
value_str = '<...>'
|
||||
return (
|
||||
|
||||
@@ -20,15 +20,23 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
from collections.abc import Callable, Iterable
|
||||
from typing import Any, Generic, Literal, TypeVar
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Generic,
|
||||
Iterable,
|
||||
Literal,
|
||||
Optional,
|
||||
Type,
|
||||
TypeVar,
|
||||
)
|
||||
|
||||
from bumble import utils
|
||||
from bumble.core import InvalidOperationError
|
||||
from bumble.gatt import Characteristic
|
||||
from bumble.gatt_client import CharacteristicProxy
|
||||
from bumble import utils
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Typing
|
||||
@@ -75,8 +83,8 @@ class DelegatedCharacteristicAdapter(CharacteristicAdapter[_T]):
|
||||
def __init__(
|
||||
self,
|
||||
characteristic: Characteristic,
|
||||
encode: Callable[[_T], bytes] | None = None,
|
||||
decode: Callable[[bytes], _T] | None = None,
|
||||
encode: Optional[Callable[[_T], bytes]] = None,
|
||||
decode: Optional[Callable[[bytes], _T]] = None,
|
||||
):
|
||||
super().__init__(characteristic)
|
||||
self.encode = encode
|
||||
@@ -102,8 +110,8 @@ class DelegatedCharacteristicProxyAdapter(CharacteristicProxyAdapter[_T]):
|
||||
def __init__(
|
||||
self,
|
||||
characteristic_proxy: CharacteristicProxy,
|
||||
encode: Callable[[_T], bytes] | None = None,
|
||||
decode: Callable[[bytes], _T] | None = None,
|
||||
encode: Optional[Callable[[_T], bytes]] = None,
|
||||
decode: Optional[Callable[[bytes], _T]] = None,
|
||||
):
|
||||
super().__init__(characteristic_proxy)
|
||||
self.encode = encode
|
||||
@@ -262,7 +270,7 @@ class SerializableCharacteristicAdapter(CharacteristicAdapter[_T2]):
|
||||
`to_bytes` and `__bytes__` methods, respectively.
|
||||
'''
|
||||
|
||||
def __init__(self, characteristic: Characteristic, cls: type[_T2]) -> None:
|
||||
def __init__(self, characteristic: Characteristic, cls: Type[_T2]) -> None:
|
||||
super().__init__(characteristic)
|
||||
self.cls = cls
|
||||
|
||||
@@ -281,7 +289,7 @@ class SerializableCharacteristicProxyAdapter(CharacteristicProxyAdapter[_T2]):
|
||||
'''
|
||||
|
||||
def __init__(
|
||||
self, characteristic_proxy: CharacteristicProxy, cls: type[_T2]
|
||||
self, characteristic_proxy: CharacteristicProxy, cls: Type[_T2]
|
||||
) -> None:
|
||||
super().__init__(characteristic_proxy)
|
||||
self.cls = cls
|
||||
@@ -303,7 +311,7 @@ class EnumCharacteristicAdapter(CharacteristicAdapter[_T3]):
|
||||
def __init__(
|
||||
self,
|
||||
characteristic: Characteristic,
|
||||
cls: type[_T3],
|
||||
cls: Type[_T3],
|
||||
length: int,
|
||||
byteorder: Literal['little', 'big'] = 'little',
|
||||
):
|
||||
@@ -339,7 +347,7 @@ class EnumCharacteristicProxyAdapter(CharacteristicProxyAdapter[_T3]):
|
||||
def __init__(
|
||||
self,
|
||||
characteristic_proxy: CharacteristicProxy,
|
||||
cls: type[_T3],
|
||||
cls: Type[_T3],
|
||||
length: int,
|
||||
byteorder: Literal['little', 'big'] = 'little',
|
||||
):
|
||||
@@ -362,4 +370,5 @@ class EnumCharacteristicProxyAdapter(CharacteristicProxyAdapter[_T3]):
|
||||
|
||||
def decode_value(self, value: bytes) -> _T3:
|
||||
int_value = int.from_bytes(value, self.byteorder)
|
||||
a = self.cls(int_value)
|
||||
return self.cls(int_value)
|
||||
|
||||
@@ -24,44 +24,72 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import functools
|
||||
import logging
|
||||
import struct
|
||||
from collections.abc import Callable, Iterable
|
||||
from datetime import datetime
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
Generic,
|
||||
Iterable,
|
||||
List,
|
||||
Optional,
|
||||
Set,
|
||||
Tuple,
|
||||
Union,
|
||||
Type,
|
||||
TypeVar,
|
||||
overload,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
from bumble import att, core, l2cap, utils
|
||||
|
||||
from bumble.colors import color
|
||||
from bumble.hci import HCI_Constant
|
||||
from bumble.att import (
|
||||
ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
||||
ATT_ATTRIBUTE_NOT_LONG_ERROR,
|
||||
ATT_CID,
|
||||
ATT_DEFAULT_MTU,
|
||||
ATT_ERROR_RESPONSE,
|
||||
ATT_INVALID_OFFSET_ERROR,
|
||||
ATT_PDU,
|
||||
ATT_RESPONSES,
|
||||
ATT_Exchange_MTU_Request,
|
||||
ATT_Find_By_Type_Value_Request,
|
||||
ATT_Find_Information_Request,
|
||||
ATT_Handle_Value_Confirmation,
|
||||
ATT_Read_Blob_Request,
|
||||
ATT_Read_By_Group_Type_Request,
|
||||
ATT_Read_By_Type_Request,
|
||||
ATT_Read_Request,
|
||||
ATT_Write_Command,
|
||||
ATT_Write_Request,
|
||||
ATT_Error,
|
||||
)
|
||||
from bumble import utils
|
||||
from bumble import core
|
||||
from bumble.core import UUID, InvalidStateError
|
||||
from bumble.gatt import (
|
||||
GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
|
||||
GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
|
||||
GATT_INCLUDE_ATTRIBUTE_TYPE,
|
||||
GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
|
||||
GATT_REQUEST_TIMEOUT,
|
||||
GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE,
|
||||
GATT_INCLUDE_ATTRIBUTE_TYPE,
|
||||
Characteristic,
|
||||
ClientCharacteristicConfigurationBits,
|
||||
InvalidServiceError,
|
||||
TemplateService,
|
||||
)
|
||||
from bumble.hci import HCI_Constant
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bumble import device as device_module
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Typing
|
||||
# -----------------------------------------------------------------------------
|
||||
if TYPE_CHECKING:
|
||||
from bumble.device import Connection
|
||||
|
||||
_T = TypeVar('_T')
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -121,8 +149,8 @@ class AttributeProxy(utils.EventEmitter, Generic[_T]):
|
||||
|
||||
class ServiceProxy(AttributeProxy):
|
||||
uuid: UUID
|
||||
characteristics: list[CharacteristicProxy[bytes]]
|
||||
included_services: list[ServiceProxy]
|
||||
characteristics: List[CharacteristicProxy[bytes]]
|
||||
included_services: List[ServiceProxy]
|
||||
|
||||
@staticmethod
|
||||
def from_client(service_class, client: Client, service_uuid: UUID):
|
||||
@@ -171,8 +199,8 @@ class ServiceProxy(AttributeProxy):
|
||||
|
||||
class CharacteristicProxy(AttributeProxy[_T]):
|
||||
properties: Characteristic.Properties
|
||||
descriptors: list[DescriptorProxy]
|
||||
subscribers: dict[Any, Callable[[_T], Any]]
|
||||
descriptors: List[DescriptorProxy]
|
||||
subscribers: Dict[Any, Callable[[_T], Any]]
|
||||
|
||||
EVENT_UPDATE = "update"
|
||||
|
||||
@@ -191,7 +219,7 @@ class CharacteristicProxy(AttributeProxy[_T]):
|
||||
self.descriptors_discovered = False
|
||||
self.subscribers = {} # Map from subscriber to proxy subscriber
|
||||
|
||||
def get_descriptor(self, descriptor_type: UUID) -> DescriptorProxy | None:
|
||||
def get_descriptor(self, descriptor_type: UUID) -> Optional[DescriptorProxy]:
|
||||
for descriptor in self.descriptors:
|
||||
if descriptor.type == descriptor_type:
|
||||
return descriptor
|
||||
@@ -203,7 +231,7 @@ class CharacteristicProxy(AttributeProxy[_T]):
|
||||
|
||||
async def subscribe(
|
||||
self,
|
||||
subscriber: Callable[[_T], Any] | None = None,
|
||||
subscriber: Optional[Callable[[_T], Any]] = None,
|
||||
prefer_notify: bool = True,
|
||||
) -> None:
|
||||
if subscriber is not None:
|
||||
@@ -249,10 +277,10 @@ class ProfileServiceProxy:
|
||||
Base class for profile-specific service proxies
|
||||
'''
|
||||
|
||||
SERVICE_CLASS: type[TemplateService]
|
||||
SERVICE_CLASS: Type[TemplateService]
|
||||
|
||||
@classmethod
|
||||
def from_client(cls, client: Client) -> ProfileServiceProxy | None:
|
||||
def from_client(cls, client: Client) -> Optional[ProfileServiceProxy]:
|
||||
return ServiceProxy.from_client(cls, client, cls.SERVICE_CLASS.UUID)
|
||||
|
||||
|
||||
@@ -260,17 +288,19 @@ class ProfileServiceProxy:
|
||||
# GATT Client
|
||||
# -----------------------------------------------------------------------------
|
||||
class Client:
|
||||
services: list[ServiceProxy]
|
||||
cached_values: dict[int, tuple[datetime, bytes]]
|
||||
notification_subscribers: dict[
|
||||
int, set[CharacteristicProxy | Callable[[bytes], Any]]
|
||||
services: List[ServiceProxy]
|
||||
cached_values: Dict[int, Tuple[datetime, bytes]]
|
||||
notification_subscribers: Dict[
|
||||
int, Set[Union[CharacteristicProxy, Callable[[bytes], Any]]]
|
||||
]
|
||||
indication_subscribers: dict[int, set[CharacteristicProxy | Callable[[bytes], Any]]]
|
||||
pending_response: asyncio.futures.Future[att.ATT_PDU] | None
|
||||
pending_request: att.ATT_PDU | None
|
||||
indication_subscribers: Dict[
|
||||
int, Set[Union[CharacteristicProxy, Callable[[bytes], Any]]]
|
||||
]
|
||||
pending_response: Optional[asyncio.futures.Future[ATT_PDU]]
|
||||
pending_request: Optional[ATT_PDU]
|
||||
|
||||
def __init__(self, bearer: att.Bearer) -> None:
|
||||
self.bearer = bearer
|
||||
def __init__(self, connection: Connection) -> None:
|
||||
self.connection = connection
|
||||
self.mtu_exchange_done = False
|
||||
self.request_semaphore = asyncio.Semaphore(1)
|
||||
self.pending_request = None
|
||||
@@ -280,78 +310,21 @@ class Client:
|
||||
self.services = []
|
||||
self.cached_values = {}
|
||||
|
||||
if att.is_enhanced_bearer(bearer):
|
||||
bearer.on(bearer.EVENT_CLOSE, self.on_disconnection)
|
||||
self._bearer_id = (
|
||||
f'[0x{bearer.connection.handle:04X}|CID=0x{bearer.source_cid:04X}]'
|
||||
)
|
||||
# Fill the mtu.
|
||||
bearer.on_att_mtu_update(att.ATT_DEFAULT_MTU)
|
||||
self.connection = bearer.connection
|
||||
else:
|
||||
bearer.on(bearer.EVENT_DISCONNECTION, self.on_disconnection)
|
||||
self._bearer_id = f'[0x{bearer.handle:04X}]'
|
||||
self.connection = bearer
|
||||
|
||||
@overload
|
||||
@classmethod
|
||||
async def connect_eatt(
|
||||
cls,
|
||||
connection: device_module.Connection,
|
||||
spec: l2cap.LeCreditBasedChannelSpec | None = None,
|
||||
) -> Client: ...
|
||||
|
||||
@overload
|
||||
@classmethod
|
||||
async def connect_eatt(
|
||||
cls,
|
||||
connection: device_module.Connection,
|
||||
spec: l2cap.LeCreditBasedChannelSpec | None = None,
|
||||
count: int = 1,
|
||||
) -> list[Client]: ...
|
||||
|
||||
@classmethod
|
||||
async def connect_eatt(
|
||||
cls,
|
||||
connection: device_module.Connection,
|
||||
spec: l2cap.LeCreditBasedChannelSpec | None = None,
|
||||
count: int = 1,
|
||||
) -> list[Client] | Client:
|
||||
channels = await connection.device.l2cap_channel_manager.create_enhanced_credit_based_channels(
|
||||
connection,
|
||||
spec or l2cap.LeCreditBasedChannelSpec(psm=att.EATT_PSM),
|
||||
count,
|
||||
)
|
||||
|
||||
def on_pdu(client: Client, pdu: bytes):
|
||||
client.on_gatt_pdu(att.ATT_PDU.from_bytes(pdu))
|
||||
|
||||
clients = [cls(channel) for channel in channels]
|
||||
for channel, client in zip(channels, clients):
|
||||
channel.sink = functools.partial(on_pdu, client)
|
||||
channel.att_mtu = att.ATT_DEFAULT_MTU
|
||||
return clients[0] if count == 1 else clients
|
||||
|
||||
@property
|
||||
def mtu(self) -> int:
|
||||
return self.bearer.att_mtu
|
||||
|
||||
@mtu.setter
|
||||
def mtu(self, value: int) -> None:
|
||||
self.bearer.on_att_mtu_update(value)
|
||||
connection.on(connection.EVENT_DISCONNECTION, self.on_disconnection)
|
||||
|
||||
def send_gatt_pdu(self, pdu: bytes) -> None:
|
||||
if att.is_enhanced_bearer(self.bearer):
|
||||
self.bearer.write(pdu)
|
||||
else:
|
||||
self.bearer.send_l2cap_pdu(att.ATT_CID, pdu)
|
||||
self.connection.send_l2cap_pdu(ATT_CID, pdu)
|
||||
|
||||
async def send_command(self, command: att.ATT_PDU) -> None:
|
||||
logger.debug(f'GATT Command from client: {self._bearer_id} {command}')
|
||||
async def send_command(self, command: ATT_PDU) -> None:
|
||||
logger.debug(
|
||||
f'GATT Command from client: [0x{self.connection.handle:04X}] {command}'
|
||||
)
|
||||
self.send_gatt_pdu(bytes(command))
|
||||
|
||||
async def send_request(self, request: att.ATT_PDU):
|
||||
logger.debug(f'GATT Request from client: {self._bearer_id} {request}')
|
||||
async def send_request(self, request: ATT_PDU):
|
||||
logger.debug(
|
||||
f'GATT Request from client: [0x{self.connection.handle:04X}] {request}'
|
||||
)
|
||||
|
||||
# Wait until we can send (only one pending command at a time for the connection)
|
||||
response = None
|
||||
@@ -377,42 +350,41 @@ class Client:
|
||||
|
||||
return response
|
||||
|
||||
def send_confirmation(
|
||||
self, confirmation: att.ATT_Handle_Value_Confirmation
|
||||
) -> None:
|
||||
logger.debug(f'GATT Confirmation from client: {self._bearer_id} {confirmation}')
|
||||
def send_confirmation(self, confirmation: ATT_Handle_Value_Confirmation) -> None:
|
||||
logger.debug(
|
||||
f'GATT Confirmation from client: [0x{self.connection.handle:04X}] '
|
||||
f'{confirmation}'
|
||||
)
|
||||
self.send_gatt_pdu(bytes(confirmation))
|
||||
|
||||
async def request_mtu(self, mtu: int) -> int:
|
||||
# Check the range
|
||||
if mtu < att.ATT_DEFAULT_MTU:
|
||||
raise core.InvalidArgumentError(f'MTU must be >= {att.ATT_DEFAULT_MTU}')
|
||||
if mtu < ATT_DEFAULT_MTU:
|
||||
raise core.InvalidArgumentError(f'MTU must be >= {ATT_DEFAULT_MTU}')
|
||||
if mtu > 0xFFFF:
|
||||
raise core.InvalidArgumentError('MTU must be <= 0xFFFF')
|
||||
|
||||
# We can only send one request per connection
|
||||
if self.mtu_exchange_done:
|
||||
return self.mtu
|
||||
return self.connection.att_mtu
|
||||
|
||||
# Send the request
|
||||
self.mtu_exchange_done = True
|
||||
response = await self.send_request(
|
||||
att.ATT_Exchange_MTU_Request(client_rx_mtu=mtu)
|
||||
)
|
||||
if response.op_code == att.Opcode.ATT_ERROR_RESPONSE:
|
||||
raise att.ATT_Error(error_code=response.error_code, message=response)
|
||||
response = await self.send_request(ATT_Exchange_MTU_Request(client_rx_mtu=mtu))
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
raise ATT_Error(error_code=response.error_code, message=response)
|
||||
|
||||
# Compute the final MTU
|
||||
self.mtu = min(mtu, response.server_rx_mtu)
|
||||
self.connection.att_mtu = min(mtu, response.server_rx_mtu)
|
||||
|
||||
return self.mtu
|
||||
return self.connection.att_mtu
|
||||
|
||||
def get_services_by_uuid(self, uuid: UUID) -> list[ServiceProxy]:
|
||||
def get_services_by_uuid(self, uuid: UUID) -> List[ServiceProxy]:
|
||||
return [service for service in self.services if service.uuid == uuid]
|
||||
|
||||
def get_characteristics_by_uuid(
|
||||
self, uuid: UUID, service: ServiceProxy | None = None
|
||||
) -> list[CharacteristicProxy[bytes]]:
|
||||
self, uuid: UUID, service: Optional[ServiceProxy] = None
|
||||
) -> List[CharacteristicProxy[bytes]]:
|
||||
services = [service] if service else self.services
|
||||
return [
|
||||
c
|
||||
@@ -420,14 +392,13 @@ class Client:
|
||||
if c.uuid == uuid
|
||||
]
|
||||
|
||||
def get_attribute_grouping(
|
||||
self, attribute_handle: int
|
||||
) -> (
|
||||
ServiceProxy
|
||||
| tuple[ServiceProxy, CharacteristicProxy]
|
||||
| tuple[ServiceProxy, CharacteristicProxy, DescriptorProxy]
|
||||
| None
|
||||
):
|
||||
def get_attribute_grouping(self, attribute_handle: int) -> Optional[
|
||||
Union[
|
||||
ServiceProxy,
|
||||
Tuple[ServiceProxy, CharacteristicProxy],
|
||||
Tuple[ServiceProxy, CharacteristicProxy, DescriptorProxy],
|
||||
]
|
||||
]:
|
||||
"""
|
||||
Get the attribute(s) associated with an attribute handle
|
||||
"""
|
||||
@@ -458,7 +429,7 @@ class Client:
|
||||
if not already_known:
|
||||
self.services.append(service)
|
||||
|
||||
async def discover_services(self, uuids: Iterable[UUID] = ()) -> list[ServiceProxy]:
|
||||
async def discover_services(self, uuids: Iterable[UUID] = ()) -> List[ServiceProxy]:
|
||||
'''
|
||||
See Vol 3, Part G - 4.4.1 Discover All Primary Services
|
||||
'''
|
||||
@@ -466,7 +437,7 @@ class Client:
|
||||
services = []
|
||||
while starting_handle < 0xFFFF:
|
||||
response = await self.send_request(
|
||||
att.ATT_Read_By_Group_Type_Request(
|
||||
ATT_Read_By_Group_Type_Request(
|
||||
starting_handle=starting_handle,
|
||||
ending_handle=0xFFFF,
|
||||
attribute_group_type=GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
|
||||
@@ -477,14 +448,14 @@ class Client:
|
||||
return []
|
||||
|
||||
# Check if we reached the end of the iteration
|
||||
if response.op_code == att.Opcode.ATT_ERROR_RESPONSE:
|
||||
if response.error_code != att.ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
# Unexpected end
|
||||
logger.warning(
|
||||
'!!! unexpected error while discovering services: '
|
||||
f'{HCI_Constant.error_name(response.error_code)}'
|
||||
)
|
||||
raise att.ATT_Error(
|
||||
raise ATT_Error(
|
||||
error_code=response.error_code,
|
||||
message='Unexpected error while discovering services',
|
||||
)
|
||||
@@ -530,7 +501,7 @@ class Client:
|
||||
|
||||
return services
|
||||
|
||||
async def discover_service(self, uuid: str | UUID) -> list[ServiceProxy]:
|
||||
async def discover_service(self, uuid: Union[str, UUID]) -> List[ServiceProxy]:
|
||||
'''
|
||||
See Vol 3, Part G - 4.4.2 Discover Primary Service by Service UUID
|
||||
'''
|
||||
@@ -543,7 +514,7 @@ class Client:
|
||||
services = []
|
||||
while starting_handle < 0xFFFF:
|
||||
response = await self.send_request(
|
||||
att.ATT_Find_By_Type_Value_Request(
|
||||
ATT_Find_By_Type_Value_Request(
|
||||
starting_handle=starting_handle,
|
||||
ending_handle=0xFFFF,
|
||||
attribute_type=GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
|
||||
@@ -555,8 +526,8 @@ class Client:
|
||||
return []
|
||||
|
||||
# Check if we reached the end of the iteration
|
||||
if response.op_code == att.Opcode.ATT_ERROR_RESPONSE:
|
||||
if response.error_code != att.ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
# Unexpected end
|
||||
logger.warning(
|
||||
'!!! unexpected error while discovering services: '
|
||||
@@ -601,7 +572,7 @@ class Client:
|
||||
|
||||
async def discover_included_services(
|
||||
self, service: ServiceProxy
|
||||
) -> list[ServiceProxy]:
|
||||
) -> List[ServiceProxy]:
|
||||
'''
|
||||
See Vol 3, Part G - 4.5.1 Find Included Services
|
||||
'''
|
||||
@@ -609,10 +580,10 @@ class Client:
|
||||
starting_handle = service.handle
|
||||
ending_handle = service.end_group_handle
|
||||
|
||||
included_services: list[ServiceProxy] = []
|
||||
included_services: List[ServiceProxy] = []
|
||||
while starting_handle <= ending_handle:
|
||||
response = await self.send_request(
|
||||
att.ATT_Read_By_Type_Request(
|
||||
ATT_Read_By_Type_Request(
|
||||
starting_handle=starting_handle,
|
||||
ending_handle=ending_handle,
|
||||
attribute_type=GATT_INCLUDE_ATTRIBUTE_TYPE,
|
||||
@@ -623,14 +594,14 @@ class Client:
|
||||
return []
|
||||
|
||||
# Check if we reached the end of the iteration
|
||||
if response.op_code == att.Opcode.ATT_ERROR_RESPONSE:
|
||||
if response.error_code != att.ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
# Unexpected end
|
||||
logger.warning(
|
||||
'!!! unexpected error while discovering included services: '
|
||||
f'{HCI_Constant.error_name(response.error_code)}'
|
||||
)
|
||||
raise att.ATT_Error(
|
||||
raise ATT_Error(
|
||||
error_code=response.error_code,
|
||||
message='Unexpected error while discovering included services',
|
||||
)
|
||||
@@ -664,8 +635,8 @@ class Client:
|
||||
return included_services
|
||||
|
||||
async def discover_characteristics(
|
||||
self, uuids, service: ServiceProxy | None
|
||||
) -> list[CharacteristicProxy[bytes]]:
|
||||
self, uuids, service: Optional[ServiceProxy]
|
||||
) -> List[CharacteristicProxy[bytes]]:
|
||||
'''
|
||||
See Vol 3, Part G - 4.6.1 Discover All Characteristics of a Service and 4.6.2
|
||||
Discover Characteristics by UUID
|
||||
@@ -678,15 +649,15 @@ class Client:
|
||||
services = [service] if service else self.services
|
||||
|
||||
# Perform characteristic discovery for each service
|
||||
discovered_characteristics: list[CharacteristicProxy[bytes]] = []
|
||||
discovered_characteristics: List[CharacteristicProxy[bytes]] = []
|
||||
for service in services:
|
||||
starting_handle = service.handle
|
||||
ending_handle = service.end_group_handle
|
||||
|
||||
characteristics: list[CharacteristicProxy[bytes]] = []
|
||||
characteristics: List[CharacteristicProxy[bytes]] = []
|
||||
while starting_handle <= ending_handle:
|
||||
response = await self.send_request(
|
||||
att.ATT_Read_By_Type_Request(
|
||||
ATT_Read_By_Type_Request(
|
||||
starting_handle=starting_handle,
|
||||
ending_handle=ending_handle,
|
||||
attribute_type=GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
|
||||
@@ -697,14 +668,14 @@ class Client:
|
||||
return []
|
||||
|
||||
# Check if we reached the end of the iteration
|
||||
if response.op_code == att.Opcode.ATT_ERROR_RESPONSE:
|
||||
if response.error_code != att.ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
# Unexpected end
|
||||
logger.warning(
|
||||
'!!! unexpected error while discovering characteristics: '
|
||||
f'{HCI_Constant.error_name(response.error_code)}'
|
||||
)
|
||||
raise att.ATT_Error(
|
||||
raise ATT_Error(
|
||||
error_code=response.error_code,
|
||||
message='Unexpected error while discovering characteristics',
|
||||
)
|
||||
@@ -751,10 +722,10 @@ class Client:
|
||||
|
||||
async def discover_descriptors(
|
||||
self,
|
||||
characteristic: CharacteristicProxy | None = None,
|
||||
start_handle: int | None = None,
|
||||
end_handle: int | None = None,
|
||||
) -> list[DescriptorProxy]:
|
||||
characteristic: Optional[CharacteristicProxy] = None,
|
||||
start_handle: Optional[int] = None,
|
||||
end_handle: Optional[int] = None,
|
||||
) -> List[DescriptorProxy]:
|
||||
'''
|
||||
See Vol 3, Part G - 4.7.1 Discover All Characteristic Descriptors
|
||||
'''
|
||||
@@ -767,10 +738,10 @@ class Client:
|
||||
else:
|
||||
return []
|
||||
|
||||
descriptors: list[DescriptorProxy] = []
|
||||
descriptors: List[DescriptorProxy] = []
|
||||
while starting_handle <= ending_handle:
|
||||
response = await self.send_request(
|
||||
att.ATT_Find_Information_Request(
|
||||
ATT_Find_Information_Request(
|
||||
starting_handle=starting_handle, ending_handle=ending_handle
|
||||
)
|
||||
)
|
||||
@@ -779,8 +750,8 @@ class Client:
|
||||
return []
|
||||
|
||||
# Check if we reached the end of the iteration
|
||||
if response.op_code == att.Opcode.ATT_ERROR_RESPONSE:
|
||||
if response.error_code != att.ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
# Unexpected end
|
||||
logger.warning(
|
||||
'!!! unexpected error while discovering descriptors: '
|
||||
@@ -816,7 +787,7 @@ class Client:
|
||||
|
||||
return descriptors
|
||||
|
||||
async def discover_attributes(self) -> list[AttributeProxy[bytes]]:
|
||||
async def discover_attributes(self) -> List[AttributeProxy[bytes]]:
|
||||
'''
|
||||
Discover all attributes, regardless of type
|
||||
'''
|
||||
@@ -825,7 +796,7 @@ class Client:
|
||||
attributes = []
|
||||
while True:
|
||||
response = await self.send_request(
|
||||
att.ATT_Find_Information_Request(
|
||||
ATT_Find_Information_Request(
|
||||
starting_handle=starting_handle, ending_handle=ending_handle
|
||||
)
|
||||
)
|
||||
@@ -833,8 +804,8 @@ class Client:
|
||||
return []
|
||||
|
||||
# Check if we reached the end of the iteration
|
||||
if response.op_code == att.Opcode.ATT_ERROR_RESPONSE:
|
||||
if response.error_code != att.ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
# Unexpected end
|
||||
logger.warning(
|
||||
'!!! unexpected error while discovering attributes: '
|
||||
@@ -862,7 +833,7 @@ class Client:
|
||||
async def subscribe(
|
||||
self,
|
||||
characteristic: CharacteristicProxy,
|
||||
subscriber: Callable[[Any], Any] | None = None,
|
||||
subscriber: Optional[Callable[[Any], Any]] = None,
|
||||
prefer_notify: bool = True,
|
||||
) -> None:
|
||||
# If we haven't already discovered the descriptors for this characteristic,
|
||||
@@ -912,7 +883,7 @@ class Client:
|
||||
async def unsubscribe(
|
||||
self,
|
||||
characteristic: CharacteristicProxy,
|
||||
subscriber: Callable[[Any], Any] | None = None,
|
||||
subscriber: Optional[Callable[[Any], Any]] = None,
|
||||
force: bool = False,
|
||||
) -> None:
|
||||
'''
|
||||
@@ -977,7 +948,7 @@ class Client:
|
||||
await self.write_value(cccd, b'\x00\x00', with_response=True)
|
||||
|
||||
async def read_value(
|
||||
self, attribute: int | AttributeProxy, no_long_read: bool = False
|
||||
self, attribute: Union[int, AttributeProxy], no_long_read: bool = False
|
||||
) -> bytes:
|
||||
'''
|
||||
See Vol 3, Part G - 4.8.1 Read Characteristic Value
|
||||
@@ -988,41 +959,39 @@ class Client:
|
||||
# Send a request to read
|
||||
attribute_handle = attribute if isinstance(attribute, int) else attribute.handle
|
||||
response = await self.send_request(
|
||||
att.ATT_Read_Request(attribute_handle=attribute_handle)
|
||||
ATT_Read_Request(attribute_handle=attribute_handle)
|
||||
)
|
||||
if response is None:
|
||||
raise TimeoutError('read timeout')
|
||||
if response.op_code == att.Opcode.ATT_ERROR_RESPONSE:
|
||||
raise att.ATT_Error(error_code=response.error_code, message=response)
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
raise ATT_Error(error_code=response.error_code, message=response)
|
||||
|
||||
# If the value is the max size for the MTU, try to read more unless the caller
|
||||
# specifically asked not to do that
|
||||
attribute_value = response.attribute_value
|
||||
if not no_long_read and len(attribute_value) == self.mtu - 1:
|
||||
if not no_long_read and len(attribute_value) == self.connection.att_mtu - 1:
|
||||
logger.debug('using READ BLOB to get the rest of the value')
|
||||
offset = len(attribute_value)
|
||||
while True:
|
||||
response = await self.send_request(
|
||||
att.ATT_Read_Blob_Request(
|
||||
ATT_Read_Blob_Request(
|
||||
attribute_handle=attribute_handle, value_offset=offset
|
||||
)
|
||||
)
|
||||
if response is None:
|
||||
raise TimeoutError('read timeout')
|
||||
if response.op_code == att.Opcode.ATT_ERROR_RESPONSE:
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
if response.error_code in (
|
||||
att.ATT_ATTRIBUTE_NOT_LONG_ERROR,
|
||||
att.ATT_INVALID_OFFSET_ERROR,
|
||||
ATT_ATTRIBUTE_NOT_LONG_ERROR,
|
||||
ATT_INVALID_OFFSET_ERROR,
|
||||
):
|
||||
break
|
||||
raise att.ATT_Error(
|
||||
error_code=response.error_code, message=response
|
||||
)
|
||||
raise ATT_Error(error_code=response.error_code, message=response)
|
||||
|
||||
part = response.part_attribute_value
|
||||
attribute_value += part
|
||||
|
||||
if len(part) < self.mtu - 1:
|
||||
if len(part) < self.connection.att_mtu - 1:
|
||||
break
|
||||
|
||||
offset += len(part)
|
||||
@@ -1032,8 +1001,8 @@ class Client:
|
||||
return attribute_value
|
||||
|
||||
async def read_characteristics_by_uuid(
|
||||
self, uuid: UUID, service: ServiceProxy | None
|
||||
) -> list[bytes]:
|
||||
self, uuid: UUID, service: Optional[ServiceProxy]
|
||||
) -> List[bytes]:
|
||||
'''
|
||||
See Vol 3, Part G - 4.8.2 Read Using Characteristic UUID
|
||||
'''
|
||||
@@ -1048,7 +1017,7 @@ class Client:
|
||||
characteristics_values = []
|
||||
while starting_handle <= ending_handle:
|
||||
response = await self.send_request(
|
||||
att.ATT_Read_By_Type_Request(
|
||||
ATT_Read_By_Type_Request(
|
||||
starting_handle=starting_handle,
|
||||
ending_handle=ending_handle,
|
||||
attribute_type=uuid,
|
||||
@@ -1059,8 +1028,8 @@ class Client:
|
||||
return []
|
||||
|
||||
# Check if we reached the end of the iteration
|
||||
if response.op_code == att.Opcode.ATT_ERROR_RESPONSE:
|
||||
if response.error_code != att.ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
# Unexpected end
|
||||
logger.warning(
|
||||
'!!! unexpected error while reading characteristics: '
|
||||
@@ -1090,7 +1059,7 @@ class Client:
|
||||
|
||||
async def write_value(
|
||||
self,
|
||||
attribute: int | AttributeProxy,
|
||||
attribute: Union[int, AttributeProxy],
|
||||
value: bytes,
|
||||
with_response: bool = False,
|
||||
) -> None:
|
||||
@@ -1105,27 +1074,28 @@ class Client:
|
||||
attribute_handle = attribute if isinstance(attribute, int) else attribute.handle
|
||||
if with_response:
|
||||
response = await self.send_request(
|
||||
att.ATT_Write_Request(
|
||||
ATT_Write_Request(
|
||||
attribute_handle=attribute_handle, attribute_value=value
|
||||
)
|
||||
)
|
||||
if response.op_code == att.Opcode.ATT_ERROR_RESPONSE:
|
||||
raise att.ATT_Error(error_code=response.error_code, message=response)
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
raise ATT_Error(error_code=response.error_code, message=response)
|
||||
else:
|
||||
await self.send_command(
|
||||
att.ATT_Write_Command(
|
||||
ATT_Write_Command(
|
||||
attribute_handle=attribute_handle, attribute_value=value
|
||||
)
|
||||
)
|
||||
|
||||
def on_disconnection(self, *args) -> None:
|
||||
del args # unused.
|
||||
def on_disconnection(self, _) -> None:
|
||||
if self.pending_response and not self.pending_response.done():
|
||||
self.pending_response.cancel()
|
||||
|
||||
def on_gatt_pdu(self, att_pdu: att.ATT_PDU) -> None:
|
||||
logger.debug(f'GATT Response to client: {self._bearer_id} {att_pdu}')
|
||||
if att_pdu.op_code in att.ATT_RESPONSES:
|
||||
def on_gatt_pdu(self, att_pdu: ATT_PDU) -> None:
|
||||
logger.debug(
|
||||
f'GATT Response to client: [0x{self.connection.handle:04X}] {att_pdu}'
|
||||
)
|
||||
if att_pdu.op_code in ATT_RESPONSES:
|
||||
if self.pending_request is None:
|
||||
# Not expected!
|
||||
logger.warning('!!! unexpected response, there is no pending request')
|
||||
@@ -1133,7 +1103,7 @@ class Client:
|
||||
|
||||
# The response should match the pending request unless it is
|
||||
# an error response
|
||||
if att_pdu.op_code != att.Opcode.ATT_ERROR_RESPONSE:
|
||||
if att_pdu.op_code != ATT_ERROR_RESPONSE:
|
||||
expected_response_name = self.pending_request.name.replace(
|
||||
'_REQUEST', '_RESPONSE'
|
||||
)
|
||||
@@ -1154,15 +1124,14 @@ class Client:
|
||||
else:
|
||||
logger.warning(
|
||||
color(
|
||||
'--- Ignoring GATT Response from ' f'{self._bearer_id}: ',
|
||||
'--- Ignoring GATT Response from '
|
||||
f'[0x{self.connection.handle:04X}]: ',
|
||||
'red',
|
||||
)
|
||||
+ str(att_pdu)
|
||||
)
|
||||
|
||||
def on_att_handle_value_notification(
|
||||
self, notification: att.ATT_Handle_Value_Notification
|
||||
):
|
||||
def on_att_handle_value_notification(self, notification):
|
||||
# Call all subscribers
|
||||
subscribers = self.notification_subscribers.get(
|
||||
notification.attribute_handle, set()
|
||||
@@ -1177,9 +1146,7 @@ class Client:
|
||||
else:
|
||||
subscriber.emit(subscriber.EVENT_UPDATE, notification.attribute_value)
|
||||
|
||||
def on_att_handle_value_indication(
|
||||
self, indication: att.ATT_Handle_Value_Indication
|
||||
):
|
||||
def on_att_handle_value_indication(self, indication):
|
||||
# Call all subscribers
|
||||
subscribers = self.indication_subscribers.get(
|
||||
indication.attribute_handle, set()
|
||||
@@ -1195,7 +1162,7 @@ class Client:
|
||||
subscriber.emit(subscriber.EVENT_UPDATE, indication.attribute_value)
|
||||
|
||||
# Confirm that we received the indication
|
||||
self.send_confirmation(att.ATT_Handle_Value_Confirmation())
|
||||
self.send_confirmation(ATT_Handle_Value_Confirmation())
|
||||
|
||||
def cache_value(self, attribute_handle: int, value: bytes) -> None:
|
||||
self.cached_values[attribute_handle] = (
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
5749
bumble/hci.py
5749
bumble/hci.py
File diff suppressed because it is too large
Load Diff
@@ -17,36 +17,44 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
from collections.abc import Callable, MutableMapping
|
||||
from typing import Any, cast
|
||||
import datetime
|
||||
from typing import cast, Any, Optional
|
||||
import logging
|
||||
|
||||
from bumble import avc, avctp, avdtp, avrcp, crypto, rfcomm, sdp
|
||||
from bumble.att import ATT_CID, ATT_PDU
|
||||
from bumble import avc
|
||||
from bumble import avctp
|
||||
from bumble import avdtp
|
||||
from bumble import avrcp
|
||||
from bumble import crypto
|
||||
from bumble import rfcomm
|
||||
from bumble import sdp
|
||||
from bumble.colors import color
|
||||
from bumble.att import ATT_CID, ATT_PDU
|
||||
from bumble.smp import SMP_CID, SMP_Command
|
||||
from bumble.core import name_or_number
|
||||
from bumble.hci import (
|
||||
HCI_ACL_DATA_PACKET,
|
||||
HCI_DISCONNECTION_COMPLETE_EVENT,
|
||||
HCI_EVENT_PACKET,
|
||||
Address,
|
||||
HCI_AclDataPacket,
|
||||
HCI_AclDataPacketAssembler,
|
||||
HCI_Disconnection_Complete_Event,
|
||||
HCI_Event,
|
||||
HCI_Packet,
|
||||
)
|
||||
from bumble.l2cap import (
|
||||
L2CAP_LE_SIGNALING_CID,
|
||||
L2CAP_PDU,
|
||||
L2CAP_CONNECTION_REQUEST,
|
||||
L2CAP_CONNECTION_RESPONSE,
|
||||
L2CAP_SIGNALING_CID,
|
||||
CommandCode,
|
||||
L2CAP_LE_SIGNALING_CID,
|
||||
L2CAP_Control_Frame,
|
||||
L2CAP_Connection_Request,
|
||||
L2CAP_Connection_Response,
|
||||
L2CAP_Control_Frame,
|
||||
)
|
||||
from bumble.smp import SMP_CID, SMP_Command
|
||||
from bumble.hci import (
|
||||
Address,
|
||||
HCI_EVENT_PACKET,
|
||||
HCI_ACL_DATA_PACKET,
|
||||
HCI_DISCONNECTION_COMPLETE_EVENT,
|
||||
HCI_AclDataPacketAssembler,
|
||||
HCI_Packet,
|
||||
HCI_Event,
|
||||
HCI_AclDataPacket,
|
||||
HCI_Disconnection_Complete_Event,
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -70,7 +78,7 @@ AVCTP_PID_NAMES = {avrcp.AVRCP_PID: 'AVRCP'}
|
||||
class PacketTracer:
|
||||
class AclStream:
|
||||
psms: MutableMapping[int, int]
|
||||
peer: PacketTracer.AclStream | None
|
||||
peer: Optional[PacketTracer.AclStream]
|
||||
avdtp_assemblers: MutableMapping[int, avdtp.MessageAssembler]
|
||||
avctp_assemblers: MutableMapping[int, avctp.MessageAssembler]
|
||||
|
||||
@@ -98,14 +106,14 @@ class PacketTracer:
|
||||
self.analyzer.emit(control_frame)
|
||||
|
||||
# Check if this signals a new channel
|
||||
if control_frame.code == CommandCode.L2CAP_CONNECTION_REQUEST:
|
||||
if control_frame.code == L2CAP_CONNECTION_REQUEST:
|
||||
connection_request = cast(L2CAP_Connection_Request, control_frame)
|
||||
self.psms[connection_request.source_cid] = connection_request.psm
|
||||
elif control_frame.code == CommandCode.L2CAP_CONNECTION_RESPONSE:
|
||||
elif control_frame.code == L2CAP_CONNECTION_RESPONSE:
|
||||
connection_response = cast(L2CAP_Connection_Response, control_frame)
|
||||
if (
|
||||
connection_response.result
|
||||
== L2CAP_Connection_Response.Result.CONNECTION_SUCCESSFUL
|
||||
== L2CAP_Connection_Response.CONNECTION_SUCCESSFUL
|
||||
):
|
||||
if self.peer and (
|
||||
psm := self.peer.psms.get(connection_response.source_cid)
|
||||
@@ -201,7 +209,7 @@ class PacketTracer:
|
||||
self.label = label
|
||||
self.emit_message = emit_message
|
||||
self.acl_streams = {} # ACL streams, by connection handle
|
||||
self.packet_timestamp: datetime.datetime | None = None
|
||||
self.packet_timestamp: Optional[datetime.datetime] = None
|
||||
|
||||
def start_acl_stream(self, connection_handle: int) -> PacketTracer.AclStream:
|
||||
logger.info(
|
||||
@@ -230,7 +238,7 @@ class PacketTracer:
|
||||
self.peer.end_acl_stream(connection_handle)
|
||||
|
||||
def on_packet(
|
||||
self, timestamp: datetime.datetime | None, packet: HCI_Packet
|
||||
self, timestamp: Optional[datetime.datetime], packet: HCI_Packet
|
||||
) -> None:
|
||||
self.packet_timestamp = timestamp
|
||||
self.emit(packet)
|
||||
@@ -262,7 +270,7 @@ class PacketTracer:
|
||||
self,
|
||||
packet: HCI_Packet,
|
||||
direction: int = 0,
|
||||
timestamp: datetime.datetime | None = None,
|
||||
timestamp: Optional[datetime.datetime] = None,
|
||||
) -> None:
|
||||
if direction == 0:
|
||||
self.host_to_controller_analyzer.on_packet(timestamp, packet)
|
||||
|
||||
147
bumble/hfp.py
147
bumble/hfp.py
@@ -17,35 +17,50 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import collections
|
||||
import collections.abc
|
||||
import logging
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import enum
|
||||
import logging
|
||||
import re
|
||||
import traceback
|
||||
from collections.abc import Iterable
|
||||
from typing import TYPE_CHECKING, Any, ClassVar
|
||||
|
||||
import re
|
||||
from typing import (
|
||||
Dict,
|
||||
List,
|
||||
Union,
|
||||
Set,
|
||||
Any,
|
||||
Optional,
|
||||
Type,
|
||||
Tuple,
|
||||
ClassVar,
|
||||
Iterable,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
from typing_extensions import Self
|
||||
|
||||
from bumble import at, device, rfcomm, sdp, utils
|
||||
from bumble import at
|
||||
from bumble import device
|
||||
from bumble import rfcomm
|
||||
from bumble import sdp
|
||||
from bumble import utils
|
||||
from bumble.colors import color
|
||||
from bumble.core import (
|
||||
ProtocolError,
|
||||
BT_GENERIC_AUDIO_SERVICE,
|
||||
BT_HANDSFREE_AUDIO_GATEWAY_SERVICE,
|
||||
BT_HANDSFREE_SERVICE,
|
||||
BT_HANDSFREE_AUDIO_GATEWAY_SERVICE,
|
||||
BT_L2CAP_PROTOCOL_ID,
|
||||
BT_RFCOMM_PROTOCOL_ID,
|
||||
ProtocolError,
|
||||
)
|
||||
from bumble.hci import (
|
||||
CodecID,
|
||||
CodingFormat,
|
||||
HCI_Enhanced_Setup_Synchronous_Connection_Command,
|
||||
CodingFormat,
|
||||
CodecID,
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -81,7 +96,7 @@ class HfpProtocol:
|
||||
|
||||
dlc.sink = self.feed
|
||||
|
||||
def feed(self, data: bytes | str) -> None:
|
||||
def feed(self, data: Union[bytes, str]) -> None:
|
||||
# Convert the data to a string if needed
|
||||
if isinstance(data, bytes):
|
||||
data = data.decode('utf-8')
|
||||
@@ -325,8 +340,8 @@ class CallInfo:
|
||||
status: CallInfoStatus
|
||||
mode: CallInfoMode
|
||||
multi_party: CallInfoMultiParty
|
||||
number: str | None = None
|
||||
type: int | None = None
|
||||
number: Optional[str] = None
|
||||
type: Optional[int] = None
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
@@ -354,13 +369,13 @@ class CallLineIdentification:
|
||||
|
||||
number: str
|
||||
type: int
|
||||
subaddr: str | None = None
|
||||
satype: int | None = None
|
||||
alpha: str | None = None
|
||||
cli_validity: int | None = None
|
||||
subaddr: Optional[str] = None
|
||||
satype: Optional[int] = None
|
||||
alpha: Optional[str] = None
|
||||
cli_validity: Optional[int] = None
|
||||
|
||||
@classmethod
|
||||
def parse_from(cls, parameters: list[bytes]) -> Self:
|
||||
def parse_from(cls: Type[Self], parameters: List[bytes]) -> Self:
|
||||
return cls(
|
||||
number=parameters[0].decode(),
|
||||
type=int(parameters[1]),
|
||||
@@ -490,9 +505,9 @@ STATUS_CODES = {
|
||||
|
||||
@dataclasses.dataclass
|
||||
class HfConfiguration:
|
||||
supported_hf_features: collections.abc.Sequence[HfFeature]
|
||||
supported_hf_indicators: collections.abc.Sequence[HfIndicator]
|
||||
supported_audio_codecs: collections.abc.Sequence[AudioCodec]
|
||||
supported_hf_features: List[HfFeature]
|
||||
supported_hf_indicators: List[HfIndicator]
|
||||
supported_audio_codecs: List[AudioCodec]
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
@@ -520,7 +535,7 @@ class AtResponse:
|
||||
parameters: list
|
||||
|
||||
@classmethod
|
||||
def parse_from(cls: type[Self], buffer: bytearray) -> Self:
|
||||
def parse_from(cls: Type[Self], buffer: bytearray) -> Self:
|
||||
code_and_parameters = buffer.split(b':')
|
||||
parameters = (
|
||||
code_and_parameters[1] if len(code_and_parameters) > 1 else bytearray()
|
||||
@@ -548,7 +563,7 @@ class AtCommand:
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def parse_from(cls: type[Self], buffer: bytearray) -> Self:
|
||||
def parse_from(cls: Type[Self], buffer: bytearray) -> Self:
|
||||
if not (match := cls._PARSE_PATTERN.fullmatch(buffer.decode())):
|
||||
if buffer.startswith(b'ATA'):
|
||||
return cls(code='A', sub_code=AtCommand.SubCode.NONE, parameters=[])
|
||||
@@ -583,9 +598,9 @@ class AgIndicatorState:
|
||||
"""
|
||||
|
||||
indicator: AgIndicator
|
||||
supported_values: set[int]
|
||||
supported_values: Set[int]
|
||||
current_status: int
|
||||
index: int | None = None
|
||||
index: Optional[int] = None
|
||||
enabled: bool = True
|
||||
|
||||
@property
|
||||
@@ -598,17 +613,17 @@ class AgIndicatorState:
|
||||
supported_values_text = (
|
||||
f'({",".join(str(v) for v in self.supported_values)})'
|
||||
)
|
||||
return f'("{self.indicator.value}",{supported_values_text})'
|
||||
return f'(\"{self.indicator.value}\",{supported_values_text})'
|
||||
|
||||
@classmethod
|
||||
def call(cls: type[Self]) -> Self:
|
||||
def call(cls: Type[Self]) -> Self:
|
||||
"""Default call indicator state."""
|
||||
return cls(
|
||||
indicator=AgIndicator.CALL, supported_values={0, 1}, current_status=0
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def callsetup(cls: type[Self]) -> Self:
|
||||
def callsetup(cls: Type[Self]) -> Self:
|
||||
"""Default callsetup indicator state."""
|
||||
return cls(
|
||||
indicator=AgIndicator.CALL_SETUP,
|
||||
@@ -617,7 +632,7 @@ class AgIndicatorState:
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def callheld(cls: type[Self]) -> Self:
|
||||
def callheld(cls: Type[Self]) -> Self:
|
||||
"""Default call indicator state."""
|
||||
return cls(
|
||||
indicator=AgIndicator.CALL_HELD,
|
||||
@@ -626,14 +641,14 @@ class AgIndicatorState:
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def service(cls: type[Self]) -> Self:
|
||||
def service(cls: Type[Self]) -> Self:
|
||||
"""Default service indicator state."""
|
||||
return cls(
|
||||
indicator=AgIndicator.SERVICE, supported_values={0, 1}, current_status=0
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def signal(cls: type[Self]) -> Self:
|
||||
def signal(cls: Type[Self]) -> Self:
|
||||
"""Default signal indicator state."""
|
||||
return cls(
|
||||
indicator=AgIndicator.SIGNAL,
|
||||
@@ -642,14 +657,14 @@ class AgIndicatorState:
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def roam(cls: type[Self]) -> Self:
|
||||
def roam(cls: Type[Self]) -> Self:
|
||||
"""Default roam indicator state."""
|
||||
return cls(
|
||||
indicator=AgIndicator.CALL, supported_values={0, 1}, current_status=0
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def battchg(cls: type[Self]) -> Self:
|
||||
def battchg(cls: Type[Self]) -> Self:
|
||||
"""Default battery charge indicator state."""
|
||||
return cls(
|
||||
indicator=AgIndicator.BATTERY_CHARGE,
|
||||
@@ -717,19 +732,19 @@ class HfProtocol(utils.EventEmitter):
|
||||
"""Termination signal for run() loop."""
|
||||
|
||||
supported_hf_features: int
|
||||
supported_audio_codecs: list[AudioCodec]
|
||||
supported_audio_codecs: List[AudioCodec]
|
||||
|
||||
supported_ag_features: int
|
||||
supported_ag_call_hold_operations: list[CallHoldOperation]
|
||||
supported_ag_call_hold_operations: List[CallHoldOperation]
|
||||
|
||||
ag_indicators: list[AgIndicatorState]
|
||||
hf_indicators: dict[HfIndicator, HfIndicatorState]
|
||||
ag_indicators: List[AgIndicatorState]
|
||||
hf_indicators: Dict[HfIndicator, HfIndicatorState]
|
||||
|
||||
dlc: rfcomm.DLC
|
||||
command_lock: asyncio.Lock
|
||||
if TYPE_CHECKING:
|
||||
response_queue: asyncio.Queue[AtResponse]
|
||||
unsolicited_queue: asyncio.Queue[AtResponse | None]
|
||||
unsolicited_queue: asyncio.Queue[Optional[AtResponse]]
|
||||
else:
|
||||
response_queue: asyncio.Queue
|
||||
unsolicited_queue: asyncio.Queue
|
||||
@@ -754,7 +769,7 @@ class HfProtocol(utils.EventEmitter):
|
||||
|
||||
# Build local features.
|
||||
self.supported_hf_features = sum(configuration.supported_hf_features)
|
||||
self.supported_audio_codecs = list(configuration.supported_audio_codecs)
|
||||
self.supported_audio_codecs = configuration.supported_audio_codecs
|
||||
|
||||
self.hf_indicators = {
|
||||
indicator: HfIndicatorState(indicator=indicator)
|
||||
@@ -821,7 +836,7 @@ class HfProtocol(utils.EventEmitter):
|
||||
cmd: str,
|
||||
timeout: float = 1.0,
|
||||
response_type: AtResponseType = AtResponseType.NONE,
|
||||
) -> None | AtResponse | list[AtResponse]:
|
||||
) -> Union[None, AtResponse, List[AtResponse]]:
|
||||
"""
|
||||
Sends an AT command and wait for the peer response.
|
||||
Wait for the AT responses sent by the peer, to the status code.
|
||||
@@ -838,7 +853,7 @@ class HfProtocol(utils.EventEmitter):
|
||||
async with self.command_lock:
|
||||
logger.debug(f">>> {cmd}")
|
||||
self.dlc.write(cmd + '\r')
|
||||
responses: list[AtResponse] = []
|
||||
responses: List[AtResponse] = []
|
||||
|
||||
while True:
|
||||
result = await asyncio.wait_for(
|
||||
@@ -1058,7 +1073,7 @@ class HfProtocol(utils.EventEmitter):
|
||||
# code, with the value indicating (call=0).
|
||||
await self.execute_command("AT+CHUP")
|
||||
|
||||
async def query_current_calls(self) -> list[CallInfo]:
|
||||
async def query_current_calls(self) -> List[CallInfo]:
|
||||
"""4.32.1 Query List of Current Calls in AG.
|
||||
|
||||
Return:
|
||||
@@ -1189,27 +1204,27 @@ class AgProtocol(utils.EventEmitter):
|
||||
EVENT_MICROPHONE_VOLUME = "microphone_volume"
|
||||
|
||||
supported_hf_features: int
|
||||
supported_hf_indicators: set[HfIndicator]
|
||||
supported_audio_codecs: list[AudioCodec]
|
||||
supported_hf_indicators: Set[HfIndicator]
|
||||
supported_audio_codecs: List[AudioCodec]
|
||||
|
||||
supported_ag_features: int
|
||||
supported_ag_call_hold_operations: list[CallHoldOperation]
|
||||
supported_ag_call_hold_operations: List[CallHoldOperation]
|
||||
|
||||
ag_indicators: list[AgIndicatorState]
|
||||
ag_indicators: List[AgIndicatorState]
|
||||
hf_indicators: collections.OrderedDict[HfIndicator, HfIndicatorState]
|
||||
|
||||
dlc: rfcomm.DLC
|
||||
|
||||
read_buffer: bytearray
|
||||
active_codec: AudioCodec
|
||||
calls: list[CallInfo]
|
||||
calls: List[CallInfo]
|
||||
|
||||
indicator_report_enabled: bool
|
||||
inband_ringtone_enabled: bool
|
||||
cme_error_enabled: bool
|
||||
cli_notification_enabled: bool
|
||||
call_waiting_enabled: bool
|
||||
_remained_slc_setup_features: set[HfFeature]
|
||||
_remained_slc_setup_features: Set[HfFeature]
|
||||
|
||||
def __init__(self, dlc: rfcomm.DLC, configuration: AgConfiguration) -> None:
|
||||
super().__init__()
|
||||
@@ -1352,7 +1367,7 @@ class AgProtocol(utils.EventEmitter):
|
||||
logger.warning(f'AG indicator {indicator} is disabled')
|
||||
|
||||
indicator_state.current_status = value
|
||||
self.send_response(f'+CIEV: {index + 1},{value}')
|
||||
self.send_response(f'+CIEV: {index+1},{value}')
|
||||
|
||||
async def negotiate_codec(self, codec: AudioCodec) -> None:
|
||||
"""Starts codec negotiation."""
|
||||
@@ -1412,13 +1427,13 @@ class AgProtocol(utils.EventEmitter):
|
||||
self.emit(self.EVENT_VOICE_RECOGNITION, VoiceRecognitionState(int(vrec)))
|
||||
|
||||
def _on_chld(self, operation_code: bytes) -> None:
|
||||
call_index: int | None = None
|
||||
call_index: Optional[int] = None
|
||||
if len(operation_code) > 1:
|
||||
call_index = int(operation_code[1:])
|
||||
operation_code = operation_code[:1] + b'x'
|
||||
try:
|
||||
operation = CallHoldOperation(operation_code.decode())
|
||||
except Exception:
|
||||
except:
|
||||
logger.error(f'Invalid operation: {operation_code.decode()}')
|
||||
self.send_cme_error(CmeError.OPERATION_NOT_SUPPORTED)
|
||||
return
|
||||
@@ -1482,8 +1497,8 @@ class AgProtocol(utils.EventEmitter):
|
||||
def _on_cmer(
|
||||
self,
|
||||
mode: bytes,
|
||||
keypad: bytes | None = None,
|
||||
display: bytes | None = None,
|
||||
keypad: Optional[bytes] = None,
|
||||
display: Optional[bytes] = None,
|
||||
indicator: bytes = b'',
|
||||
) -> None:
|
||||
if (
|
||||
@@ -1590,7 +1605,7 @@ class AgProtocol(utils.EventEmitter):
|
||||
|
||||
def _on_clcc(self) -> None:
|
||||
for call in self.calls:
|
||||
number_text = f',"{call.number}"' if call.number is not None else ''
|
||||
number_text = f',\"{call.number}\"' if call.number is not None else ''
|
||||
type_text = f',{call.type}' if call.type is not None else ''
|
||||
response = (
|
||||
f'+CLCC: {call.index}'
|
||||
@@ -1679,7 +1694,7 @@ def make_hf_sdp_records(
|
||||
rfcomm_channel: int,
|
||||
configuration: HfConfiguration,
|
||||
version: ProfileVersion = ProfileVersion.V1_8,
|
||||
) -> list[sdp.ServiceAttribute]:
|
||||
) -> List[sdp.ServiceAttribute]:
|
||||
"""
|
||||
Generates the SDP record for HFP Hands-Free support.
|
||||
|
||||
@@ -1765,7 +1780,7 @@ def make_ag_sdp_records(
|
||||
rfcomm_channel: int,
|
||||
configuration: AgConfiguration,
|
||||
version: ProfileVersion = ProfileVersion.V1_8,
|
||||
) -> list[sdp.ServiceAttribute]:
|
||||
) -> List[sdp.ServiceAttribute]:
|
||||
"""
|
||||
Generates the SDP record for HFP Audio-Gateway support.
|
||||
|
||||
@@ -1845,7 +1860,7 @@ def make_ag_sdp_records(
|
||||
|
||||
async def find_hf_sdp_record(
|
||||
connection: device.Connection,
|
||||
) -> tuple[int, ProfileVersion, HfSdpFeature] | None:
|
||||
) -> Optional[Tuple[int, ProfileVersion, HfSdpFeature]]:
|
||||
"""Searches a Hands-Free SDP record from remote device.
|
||||
|
||||
Args:
|
||||
@@ -1865,9 +1880,9 @@ async def find_hf_sdp_record(
|
||||
],
|
||||
)
|
||||
for attribute_lists in search_result:
|
||||
channel: int | None = None
|
||||
version: ProfileVersion | None = None
|
||||
features: HfSdpFeature | None = None
|
||||
channel: Optional[int] = None
|
||||
version: Optional[ProfileVersion] = None
|
||||
features: Optional[HfSdpFeature] = None
|
||||
for attribute in attribute_lists:
|
||||
# The layout is [[L2CAP_PROTOCOL], [RFCOMM_PROTOCOL, RFCOMM_CHANNEL]].
|
||||
if attribute.id == sdp.SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID:
|
||||
@@ -1897,7 +1912,7 @@ async def find_hf_sdp_record(
|
||||
|
||||
async def find_ag_sdp_record(
|
||||
connection: device.Connection,
|
||||
) -> tuple[int, ProfileVersion, AgSdpFeature] | None:
|
||||
) -> Optional[Tuple[int, ProfileVersion, AgSdpFeature]]:
|
||||
"""Searches an Audio-Gateway SDP record from remote device.
|
||||
|
||||
Args:
|
||||
@@ -1916,9 +1931,9 @@ async def find_ag_sdp_record(
|
||||
],
|
||||
)
|
||||
for attribute_lists in search_result:
|
||||
channel: int | None = None
|
||||
version: ProfileVersion | None = None
|
||||
features: AgSdpFeature | None = None
|
||||
channel: Optional[int] = None
|
||||
version: Optional[ProfileVersion] = None
|
||||
features: Optional[AgSdpFeature] = None
|
||||
for attribute in attribute_lists:
|
||||
# The layout is [[L2CAP_PROTOCOL], [RFCOMM_PROTOCOL, RFCOMM_CHANNEL]].
|
||||
if attribute.id == sdp.SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID:
|
||||
@@ -1995,7 +2010,7 @@ class EscoParameters:
|
||||
transmit_codec_frame_size: int = 60
|
||||
receive_codec_frame_size: int = 60
|
||||
|
||||
def asdict(self) -> dict[str, Any]:
|
||||
def asdict(self) -> Dict[str, Any]:
|
||||
# dataclasses.asdict() will recursively deep-copy the entire object,
|
||||
# which is expensive and breaks CodingFormat object, so let it simply copy here.
|
||||
return self.__dict__
|
||||
|
||||
@@ -16,20 +16,22 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
import logging
|
||||
import struct
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
import enum
|
||||
import struct
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional, Callable
|
||||
from typing_extensions import override
|
||||
|
||||
from bumble import device, l2cap, utils
|
||||
from bumble import l2cap
|
||||
from bumble import device
|
||||
from bumble import utils
|
||||
from bumble.core import InvalidStateError, ProtocolError
|
||||
from bumble.hci import Address
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -195,9 +197,9 @@ class SendHandshakeMessage(Message):
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class HID(ABC, utils.EventEmitter):
|
||||
l2cap_ctrl_channel: l2cap.ClassicChannel | None = None
|
||||
l2cap_intr_channel: l2cap.ClassicChannel | None = None
|
||||
connection: device.Connection | None = None
|
||||
l2cap_ctrl_channel: Optional[l2cap.ClassicChannel] = None
|
||||
l2cap_intr_channel: Optional[l2cap.ClassicChannel] = None
|
||||
connection: Optional[device.Connection] = None
|
||||
|
||||
EVENT_INTERRUPT_DATA = "interrupt_data"
|
||||
EVENT_CONTROL_DATA = "control_data"
|
||||
@@ -212,46 +214,38 @@ class HID(ABC, utils.EventEmitter):
|
||||
|
||||
def __init__(self, device: device.Device, role: Role) -> None:
|
||||
super().__init__()
|
||||
self.remote_device_bd_address: Address | None = None
|
||||
self.remote_device_bd_address: Optional[Address] = None
|
||||
self.device = device
|
||||
self.role = role
|
||||
|
||||
# Register ourselves with the L2CAP channel manager
|
||||
device.create_l2cap_server(
|
||||
l2cap.ClassicChannelSpec(HID_CONTROL_PSM), self.on_l2cap_connection
|
||||
)
|
||||
device.create_l2cap_server(
|
||||
l2cap.ClassicChannelSpec(HID_INTERRUPT_PSM), self.on_l2cap_connection
|
||||
)
|
||||
device.register_l2cap_server(HID_CONTROL_PSM, self.on_l2cap_connection)
|
||||
device.register_l2cap_server(HID_INTERRUPT_PSM, self.on_l2cap_connection)
|
||||
|
||||
device.on(device.EVENT_CONNECTION, self.on_device_connection)
|
||||
|
||||
async def connect_control_channel(self) -> None:
|
||||
if not self.connection:
|
||||
raise InvalidStateError("Connection is not established!")
|
||||
# Create a new L2CAP connection - control channel
|
||||
try:
|
||||
channel = await self.connection.create_l2cap_channel(
|
||||
l2cap.ClassicChannelSpec(HID_CONTROL_PSM)
|
||||
channel = await self.device.l2cap_channel_manager.connect(
|
||||
self.connection, HID_CONTROL_PSM
|
||||
)
|
||||
channel.sink = self.on_ctrl_pdu
|
||||
self.l2cap_ctrl_channel = channel
|
||||
except ProtocolError:
|
||||
logging.exception('L2CAP connection failed.')
|
||||
logging.exception(f'L2CAP connection failed.')
|
||||
raise
|
||||
|
||||
async def connect_interrupt_channel(self) -> None:
|
||||
if not self.connection:
|
||||
raise InvalidStateError("Connection is not established!")
|
||||
# Create a new L2CAP connection - interrupt channel
|
||||
try:
|
||||
channel = await self.connection.create_l2cap_channel(
|
||||
l2cap.ClassicChannelSpec(HID_INTERRUPT_PSM)
|
||||
channel = await self.device.l2cap_channel_manager.connect(
|
||||
self.connection, HID_INTERRUPT_PSM
|
||||
)
|
||||
channel.sink = self.on_intr_pdu
|
||||
self.l2cap_intr_channel = channel
|
||||
except ProtocolError:
|
||||
logging.exception('L2CAP connection failed.')
|
||||
logging.exception(f'L2CAP connection failed.')
|
||||
raise
|
||||
|
||||
async def disconnect_interrupt_channel(self) -> None:
|
||||
@@ -353,10 +347,10 @@ class Device(HID):
|
||||
data: bytes = b''
|
||||
status: int = 0
|
||||
|
||||
get_report_cb: Callable[[int, int, int], GetSetStatus] | None = None
|
||||
set_report_cb: Callable[[int, int, int, bytes], GetSetStatus] | None = None
|
||||
get_protocol_cb: Callable[[], GetSetStatus] | None = None
|
||||
set_protocol_cb: Callable[[int], GetSetStatus] | None = None
|
||||
get_report_cb: Optional[Callable[[int, int, int], GetSetStatus]] = None
|
||||
set_report_cb: Optional[Callable[[int, int, int, bytes], GetSetStatus]] = None
|
||||
get_protocol_cb: Optional[Callable[[], GetSetStatus]] = None
|
||||
set_protocol_cb: Optional[Callable[[int], GetSetStatus]] = None
|
||||
|
||||
def __init__(self, device: device.Device) -> None:
|
||||
super().__init__(device, HID.Role.DEVICE)
|
||||
|
||||
495
bumble/host.py
495
bumble/host.py
@@ -16,20 +16,37 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import collections
|
||||
import dataclasses
|
||||
import logging
|
||||
import struct
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
from bumble import drivers, hci, utils
|
||||
from typing import (
|
||||
Any,
|
||||
Awaitable,
|
||||
Callable,
|
||||
Deque,
|
||||
Dict,
|
||||
Optional,
|
||||
Set,
|
||||
cast,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
|
||||
from bumble.colors import color
|
||||
from bumble.core import ConnectionPHY, InvalidStateError, PhysicalTransport
|
||||
from bumble.l2cap import L2CAP_PDU
|
||||
from bumble.snoop import Snooper
|
||||
from bumble import drivers
|
||||
from bumble import hci
|
||||
from bumble.core import (
|
||||
PhysicalTransport,
|
||||
PhysicalTransport,
|
||||
ConnectionPHY,
|
||||
ConnectionParameters,
|
||||
)
|
||||
from bumble import utils
|
||||
from bumble.transport.common import TransportLostError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -58,11 +75,6 @@ class DataPacketQueue(utils.EventEmitter):
|
||||
|
||||
max_packet_size: int
|
||||
|
||||
class PerConnectionState:
|
||||
def __init__(self) -> None:
|
||||
self.in_flight = 0
|
||||
self.drained = asyncio.Event()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
max_packet_size: int,
|
||||
@@ -73,16 +85,11 @@ class DataPacketQueue(utils.EventEmitter):
|
||||
self.max_packet_size = max_packet_size
|
||||
self.max_in_flight = max_in_flight
|
||||
self._in_flight = 0 # Total number of packets in flight across all connections
|
||||
self._connection_state: dict[int, DataPacketQueue.PerConnectionState] = (
|
||||
collections.defaultdict(DataPacketQueue.PerConnectionState)
|
||||
)
|
||||
self._drained_per_connection: dict[int, asyncio.Event] = (
|
||||
collections.defaultdict(asyncio.Event)
|
||||
)
|
||||
self._in_flight_per_connection: dict[int, int] = collections.defaultdict(
|
||||
int
|
||||
) # Number of packets in flight per connection
|
||||
self._send = send
|
||||
self._packets: collections.deque[tuple[hci.HCI_Packet, int]] = (
|
||||
collections.deque()
|
||||
)
|
||||
self._packets: Deque[tuple[hci.HCI_Packet, int]] = collections.deque()
|
||||
self._queued = 0
|
||||
self._completed = 0
|
||||
|
||||
@@ -109,7 +116,8 @@ class DataPacketQueue(utils.EventEmitter):
|
||||
|
||||
if self._packets:
|
||||
logger.debug(
|
||||
f'{self._in_flight} packets in flight, {len(self._packets)} in queue'
|
||||
f'{self._in_flight} packets in flight, '
|
||||
f'{len(self._packets)} in queue'
|
||||
)
|
||||
|
||||
def flush(self, connection_handle: int) -> None:
|
||||
@@ -129,40 +137,36 @@ class DataPacketQueue(utils.EventEmitter):
|
||||
self._completed += flushed_count
|
||||
self._packets = collections.deque(packets_to_keep)
|
||||
|
||||
if connection_state := self._connection_state.pop(connection_handle, None):
|
||||
in_flight = connection_state.in_flight
|
||||
if connection_handle in self._in_flight_per_connection:
|
||||
in_flight = self._in_flight_per_connection[connection_handle]
|
||||
self._completed += in_flight
|
||||
self._in_flight -= in_flight
|
||||
connection_state.drained.set()
|
||||
del self._in_flight_per_connection[connection_handle]
|
||||
|
||||
def _check_queue(self) -> None:
|
||||
while self._packets and self._in_flight < self.max_in_flight:
|
||||
packet, connection_handle = self._packets.pop()
|
||||
self._send(packet)
|
||||
self._in_flight += 1
|
||||
connection_state = self._connection_state[connection_handle]
|
||||
connection_state.in_flight += 1
|
||||
connection_state.drained.clear()
|
||||
self._in_flight_per_connection[connection_handle] += 1
|
||||
|
||||
def on_packets_completed(self, packet_count: int, connection_handle: int) -> None:
|
||||
"""Mark one or more packets associated with a connection as completed."""
|
||||
if connection_handle not in self._connection_state:
|
||||
if connection_handle not in self._in_flight_per_connection:
|
||||
logger.warning(
|
||||
f'received completion for unknown connection {connection_handle}'
|
||||
)
|
||||
return
|
||||
|
||||
connection_state = self._connection_state[connection_handle]
|
||||
if packet_count <= connection_state.in_flight:
|
||||
connection_state.in_flight -= packet_count
|
||||
in_flight_for_connection = self._in_flight_per_connection[connection_handle]
|
||||
if packet_count <= in_flight_for_connection:
|
||||
self._in_flight_per_connection[connection_handle] -= packet_count
|
||||
else:
|
||||
logger.warning(
|
||||
f'{packet_count} completed for {connection_handle} '
|
||||
f'but only {connection_state.in_flight} in flight'
|
||||
f'but only {in_flight_for_connection} in flight'
|
||||
)
|
||||
connection_state.in_flight = 0
|
||||
if connection_state.in_flight == 0:
|
||||
connection_state.drained.set()
|
||||
self._in_flight_per_connection[connection_handle] = 0
|
||||
|
||||
if packet_count <= self._in_flight:
|
||||
self._in_flight -= packet_count
|
||||
@@ -177,13 +181,6 @@ class DataPacketQueue(utils.EventEmitter):
|
||||
self._check_queue()
|
||||
self.emit('flow')
|
||||
|
||||
async def drain(self, connection_handle: int) -> None:
|
||||
"""Wait until there are no pending packets for a connection."""
|
||||
if not (connection_state := self._connection_state.get(connection_handle)):
|
||||
raise ValueError('no such connection')
|
||||
|
||||
await connection_state.drained.wait()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Connection:
|
||||
@@ -199,7 +196,7 @@ class Connection:
|
||||
self.peer_address = peer_address
|
||||
self.assembler = hci.HCI_AclDataPacketAssembler(self.on_acl_pdu)
|
||||
self.transport = transport
|
||||
acl_packet_queue: DataPacketQueue | None = (
|
||||
acl_packet_queue: Optional[DataPacketQueue] = (
|
||||
host.le_acl_packet_queue
|
||||
if transport == PhysicalTransport.LE
|
||||
else host.acl_packet_queue
|
||||
@@ -237,23 +234,25 @@ class IsoLink:
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Host(utils.EventEmitter):
|
||||
connections: dict[int, Connection]
|
||||
cis_links: dict[int, IsoLink]
|
||||
bis_links: dict[int, IsoLink]
|
||||
sco_links: dict[int, ScoLink]
|
||||
connections: Dict[int, Connection]
|
||||
cis_links: Dict[int, IsoLink]
|
||||
bis_links: Dict[int, IsoLink]
|
||||
sco_links: Dict[int, ScoLink]
|
||||
bigs: dict[int, set[int]]
|
||||
acl_packet_queue: DataPacketQueue | None = None
|
||||
le_acl_packet_queue: DataPacketQueue | None = None
|
||||
iso_packet_queue: DataPacketQueue | None = None
|
||||
hci_sink: TransportSink | None = None
|
||||
hci_metadata: dict[str, Any]
|
||||
long_term_key_provider: Callable[[int, bytes, int], Awaitable[bytes | None]] | None
|
||||
link_key_provider: Callable[[hci.Address], Awaitable[bytes | None]] | None
|
||||
acl_packet_queue: Optional[DataPacketQueue] = None
|
||||
le_acl_packet_queue: Optional[DataPacketQueue] = None
|
||||
iso_packet_queue: Optional[DataPacketQueue] = None
|
||||
hci_sink: Optional[TransportSink] = None
|
||||
hci_metadata: Dict[str, Any]
|
||||
long_term_key_provider: Optional[
|
||||
Callable[[int, bytes, int], Awaitable[Optional[bytes]]]
|
||||
]
|
||||
link_key_provider: Optional[Callable[[hci.Address], Awaitable[Optional[bytes]]]]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
controller_source: TransportSource | None = None,
|
||||
controller_sink: TransportSink | None = None,
|
||||
controller_source: Optional[TransportSource] = None,
|
||||
controller_sink: Optional[TransportSink] = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
|
||||
@@ -265,7 +264,7 @@ class Host(utils.EventEmitter):
|
||||
self.sco_links = {} # SCO links, by connection handle
|
||||
self.bigs = {} # BIG Handle to BIS Handles
|
||||
self.pending_command = None
|
||||
self.pending_response: asyncio.Future[Any] | None = None
|
||||
self.pending_response: Optional[asyncio.Future[Any]] = None
|
||||
self.number_of_supported_advertising_sets = 0
|
||||
self.maximum_advertising_data_length = 31
|
||||
self.local_version = None
|
||||
@@ -278,7 +277,7 @@ class Host(utils.EventEmitter):
|
||||
self.long_term_key_provider = None
|
||||
self.link_key_provider = None
|
||||
self.pairing_io_capability_provider = None # Classic only
|
||||
self.snooper: Snooper | None = None
|
||||
self.snooper: Optional[Snooper] = None
|
||||
|
||||
# Connect to the source and sink if specified
|
||||
if controller_source:
|
||||
@@ -289,9 +288,9 @@ class Host(utils.EventEmitter):
|
||||
def find_connection_by_bd_addr(
|
||||
self,
|
||||
bd_addr: hci.Address,
|
||||
transport: int | None = None,
|
||||
transport: Optional[int] = None,
|
||||
check_address_type: bool = False,
|
||||
) -> Connection | None:
|
||||
) -> Optional[Connection]:
|
||||
for connection in self.connections.values():
|
||||
if bytes(connection.peer_address) == bytes(bd_addr):
|
||||
if (
|
||||
@@ -548,7 +547,7 @@ class Host(utils.EventEmitter):
|
||||
logger.debug(
|
||||
'HCI LE flow control: '
|
||||
f'le_acl_data_packet_length={le_acl_data_packet_length},'
|
||||
f'total_num_le_acl_data_packets={total_num_le_acl_data_packets},'
|
||||
f'total_num_le_acl_data_packets={total_num_le_acl_data_packets}'
|
||||
f'iso_data_packet_length={iso_data_packet_length},'
|
||||
f'total_num_iso_data_packets={total_num_iso_data_packets}'
|
||||
)
|
||||
@@ -631,7 +630,7 @@ class Host(utils.EventEmitter):
|
||||
)
|
||||
|
||||
@property
|
||||
def controller(self) -> TransportSink | None:
|
||||
def controller(self) -> Optional[TransportSink]:
|
||||
return self.hci_sink
|
||||
|
||||
@controller.setter
|
||||
@@ -640,7 +639,7 @@ class Host(utils.EventEmitter):
|
||||
if controller:
|
||||
self.set_packet_source(controller)
|
||||
|
||||
def set_packet_sink(self, sink: TransportSink | None) -> None:
|
||||
def set_packet_sink(self, sink: Optional[TransportSink]) -> None:
|
||||
self.hci_sink = sink
|
||||
|
||||
def set_packet_source(self, source: TransportSource) -> None:
|
||||
@@ -655,7 +654,7 @@ class Host(utils.EventEmitter):
|
||||
self.hci_sink.on_packet(bytes(packet))
|
||||
|
||||
async def send_command(
|
||||
self, command, check_result=False, response_timeout: int | None = None
|
||||
self, command, check_result=False, response_timeout: Optional[int] = None
|
||||
):
|
||||
# Wait until we can send (only one pending command at a time)
|
||||
async with self.command_semaphore:
|
||||
@@ -691,9 +690,11 @@ class Host(utils.EventEmitter):
|
||||
raise hci.HCI_Error(status)
|
||||
|
||||
return response
|
||||
except Exception:
|
||||
logger.exception(color("!!! Exception while sending command:", "red"))
|
||||
raise
|
||||
except Exception as error:
|
||||
logger.exception(
|
||||
f'{color("!!! Exception while sending command:", "red")} {error}'
|
||||
)
|
||||
raise error
|
||||
finally:
|
||||
self.pending_command = None
|
||||
self.pending_response = None
|
||||
@@ -705,7 +706,7 @@ class Host(utils.EventEmitter):
|
||||
|
||||
asyncio.create_task(send_command(command))
|
||||
|
||||
def send_acl_sdu(self, connection_handle: int, sdu: bytes) -> None:
|
||||
def send_l2cap_pdu(self, connection_handle: int, cid: int, pdu: bytes) -> None:
|
||||
if not (connection := self.connections.get(connection_handle)):
|
||||
logger.warning(f'connection 0x{connection_handle:04X} not found')
|
||||
return
|
||||
@@ -716,24 +717,27 @@ class Host(utils.EventEmitter):
|
||||
)
|
||||
return
|
||||
|
||||
# Create a PDU
|
||||
l2cap_pdu = bytes(L2CAP_PDU(cid, pdu))
|
||||
|
||||
# Send the data to the controller via ACL packets
|
||||
max_packet_size = packet_queue.max_packet_size
|
||||
for offset in range(0, len(sdu), max_packet_size):
|
||||
pdu = sdu[offset : offset + max_packet_size]
|
||||
bytes_remaining = len(l2cap_pdu)
|
||||
offset = 0
|
||||
pb_flag = 0
|
||||
while bytes_remaining:
|
||||
data_total_length = min(bytes_remaining, packet_queue.max_packet_size)
|
||||
acl_packet = hci.HCI_AclDataPacket(
|
||||
connection_handle=connection_handle,
|
||||
pb_flag=1 if offset > 0 else 0,
|
||||
pb_flag=pb_flag,
|
||||
bc_flag=0,
|
||||
data_total_length=len(pdu),
|
||||
data=pdu,
|
||||
)
|
||||
logger.debug(
|
||||
'>>> ACL packet enqueue: (Handle=0x%04X) %s', connection_handle, pdu
|
||||
data_total_length=data_total_length,
|
||||
data=l2cap_pdu[offset : offset + data_total_length],
|
||||
)
|
||||
logger.debug(f'>>> ACL packet enqueue: (CID={cid}) {acl_packet}')
|
||||
packet_queue.enqueue(acl_packet, connection_handle)
|
||||
|
||||
def send_l2cap_pdu(self, connection_handle: int, cid: int, pdu: bytes) -> None:
|
||||
self.send_acl_sdu(connection_handle, bytes(L2CAP_PDU(cid, pdu)))
|
||||
pb_flag = 1
|
||||
offset += data_total_length
|
||||
bytes_remaining -= data_total_length
|
||||
|
||||
def get_data_packet_queue(self, connection_handle: int) -> DataPacketQueue | None:
|
||||
if connection := self.connections.get(connection_handle):
|
||||
@@ -809,7 +813,7 @@ class Host(utils.EventEmitter):
|
||||
) != 0
|
||||
|
||||
@property
|
||||
def supported_commands(self) -> set[int]:
|
||||
def supported_commands(self) -> Set[int]:
|
||||
return set(
|
||||
op_code
|
||||
for op_code, mask in hci.HCI_SUPPORTED_COMMANDS_MASKS.items()
|
||||
@@ -832,8 +836,8 @@ class Host(utils.EventEmitter):
|
||||
def on_packet(self, packet: bytes) -> None:
|
||||
try:
|
||||
hci_packet = hci.HCI_Packet.from_bytes(packet)
|
||||
except Exception:
|
||||
logger.exception('!!! error parsing packet from bytes')
|
||||
except Exception as error:
|
||||
logger.warning(f'!!! error parsing packet from bytes: {error}')
|
||||
return
|
||||
|
||||
if self.ready or (
|
||||
@@ -897,14 +901,10 @@ class Host(utils.EventEmitter):
|
||||
def on_l2cap_pdu(self, connection: Connection, cid: int, pdu: bytes) -> None:
|
||||
self.emit('l2cap_pdu', connection.handle, cid, pdu)
|
||||
|
||||
def on_command_processed(
|
||||
self, event: hci.HCI_Command_Complete_Event | hci.HCI_Command_Status_Event
|
||||
):
|
||||
def on_command_processed(self, event):
|
||||
if self.pending_response:
|
||||
# Check that it is what we were expecting
|
||||
if self.pending_command is None:
|
||||
logger.warning('!!! pending_command is None ')
|
||||
elif self.pending_command.op_code != event.command_opcode:
|
||||
if self.pending_command.op_code != event.command_opcode:
|
||||
logger.warning(
|
||||
'!!! command result mismatch, expected '
|
||||
f'0x{self.pending_command.op_code:X} but got '
|
||||
@@ -918,10 +918,10 @@ class Host(utils.EventEmitter):
|
||||
############################################################
|
||||
# HCI handlers
|
||||
############################################################
|
||||
def on_hci_event(self, event: hci.HCI_Event):
|
||||
def on_hci_event(self, event):
|
||||
logger.warning(f'{color(f"--- Ignoring event {event}", "red")}')
|
||||
|
||||
def on_hci_command_complete_event(self, event: hci.HCI_Command_Complete_Event):
|
||||
def on_hci_command_complete_event(self, event):
|
||||
if event.command_opcode == 0:
|
||||
# This is used just for the Num_HCI_Command_Packets field, not related to
|
||||
# an actual command
|
||||
@@ -930,7 +930,7 @@ class Host(utils.EventEmitter):
|
||||
|
||||
return self.on_command_processed(event)
|
||||
|
||||
def on_hci_command_status_event(self, event: hci.HCI_Command_Status_Event):
|
||||
def on_hci_command_status_event(self, event):
|
||||
return self.on_command_processed(event)
|
||||
|
||||
def on_hci_number_of_completed_packets_event(
|
||||
@@ -950,7 +950,7 @@ class Host(utils.EventEmitter):
|
||||
)
|
||||
|
||||
# Classic only
|
||||
def on_hci_connection_request_event(self, event: hci.HCI_Connection_Request_Event):
|
||||
def on_hci_connection_request_event(self, event):
|
||||
# Notify the listeners
|
||||
self.emit(
|
||||
'connection_request',
|
||||
@@ -959,14 +959,7 @@ class Host(utils.EventEmitter):
|
||||
event.link_type,
|
||||
)
|
||||
|
||||
def on_hci_le_connection_complete_event(
|
||||
self,
|
||||
event: (
|
||||
hci.HCI_LE_Connection_Complete_Event
|
||||
| hci.HCI_LE_Enhanced_Connection_Complete_Event
|
||||
| hci.HCI_LE_Enhanced_Connection_Complete_V2_Event
|
||||
),
|
||||
):
|
||||
def on_hci_le_connection_complete_event(self, event):
|
||||
# Check if this is a cancellation
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
# Create/update the connection
|
||||
@@ -986,16 +979,20 @@ class Host(utils.EventEmitter):
|
||||
self.connections[event.connection_handle] = connection
|
||||
|
||||
# Notify the client
|
||||
connection_parameters = ConnectionParameters(
|
||||
event.connection_interval,
|
||||
event.peripheral_latency,
|
||||
event.supervision_timeout,
|
||||
)
|
||||
self.emit(
|
||||
'le_connection',
|
||||
'connection',
|
||||
event.connection_handle,
|
||||
PhysicalTransport.LE,
|
||||
event.peer_address,
|
||||
getattr(event, 'local_resolvable_private_address', None),
|
||||
getattr(event, 'peer_resolvable_private_address', None),
|
||||
hci.Role(event.role),
|
||||
event.connection_interval,
|
||||
event.peripheral_latency,
|
||||
event.supervision_timeout,
|
||||
connection_parameters,
|
||||
)
|
||||
else:
|
||||
logger.debug(f'### CONNECTION FAILED: {event.status}')
|
||||
@@ -1008,25 +1005,15 @@ class Host(utils.EventEmitter):
|
||||
event.status,
|
||||
)
|
||||
|
||||
def on_hci_le_enhanced_connection_complete_event(
|
||||
self,
|
||||
event: (
|
||||
hci.HCI_LE_Enhanced_Connection_Complete_Event
|
||||
| hci.HCI_LE_Enhanced_Connection_Complete_V2_Event
|
||||
),
|
||||
):
|
||||
def on_hci_le_enhanced_connection_complete_event(self, event):
|
||||
# Just use the same implementation as for the non-enhanced event for now
|
||||
self.on_hci_le_connection_complete_event(event)
|
||||
|
||||
def on_hci_le_enhanced_connection_complete_v2_event(
|
||||
self, event: hci.HCI_LE_Enhanced_Connection_Complete_V2_Event
|
||||
):
|
||||
def on_hci_le_enhanced_connection_complete_v2_event(self, event):
|
||||
# Just use the same implementation as for the v1 event for now
|
||||
self.on_hci_le_enhanced_connection_complete_event(event)
|
||||
|
||||
def on_hci_connection_complete_event(
|
||||
self, event: hci.HCI_Connection_Complete_Event
|
||||
):
|
||||
def on_hci_connection_complete_event(self, event):
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
# Create/update the connection
|
||||
logger.debug(
|
||||
@@ -1046,9 +1033,14 @@ class Host(utils.EventEmitter):
|
||||
|
||||
# Notify the client
|
||||
self.emit(
|
||||
'classic_connection',
|
||||
'connection',
|
||||
event.connection_handle,
|
||||
PhysicalTransport.BR_EDR,
|
||||
event.bd_addr,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
else:
|
||||
logger.debug(f'### BR/EDR CONNECTION FAILED: {event.status}')
|
||||
@@ -1061,9 +1053,7 @@ class Host(utils.EventEmitter):
|
||||
event.status,
|
||||
)
|
||||
|
||||
def on_hci_disconnection_complete_event(
|
||||
self, event: hci.HCI_Disconnection_Complete_Event
|
||||
):
|
||||
def on_hci_disconnection_complete_event(self, event):
|
||||
# Find the connection
|
||||
handle = event.connection_handle
|
||||
if (
|
||||
@@ -1102,30 +1092,27 @@ class Host(utils.EventEmitter):
|
||||
# Notify the listeners
|
||||
self.emit('disconnection_failure', handle, event.status)
|
||||
|
||||
def on_hci_le_connection_update_complete_event(
|
||||
self, event: hci.HCI_LE_Connection_Update_Complete_Event
|
||||
):
|
||||
def on_hci_le_connection_update_complete_event(self, event):
|
||||
if (connection := self.connections.get(event.connection_handle)) is None:
|
||||
logger.warning('!!! CONNECTION PARAMETERS UPDATE COMPLETE: unknown handle')
|
||||
return
|
||||
|
||||
# Notify the client
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
self.emit(
|
||||
'connection_parameters_update',
|
||||
connection.handle,
|
||||
connection_parameters = ConnectionParameters(
|
||||
event.connection_interval,
|
||||
event.peripheral_latency,
|
||||
event.supervision_timeout,
|
||||
)
|
||||
self.emit(
|
||||
'connection_parameters_update', connection.handle, connection_parameters
|
||||
)
|
||||
else:
|
||||
self.emit(
|
||||
'connection_parameters_update_failure', connection.handle, event.status
|
||||
)
|
||||
|
||||
def on_hci_le_phy_update_complete_event(
|
||||
self, event: hci.HCI_LE_PHY_Update_Complete_Event
|
||||
):
|
||||
def on_hci_le_phy_update_complete_event(self, event):
|
||||
if (connection := self.connections.get(event.connection_handle)) is None:
|
||||
logger.warning('!!! CONNECTION PHY UPDATE COMPLETE: unknown handle')
|
||||
return
|
||||
@@ -1140,24 +1127,14 @@ class Host(utils.EventEmitter):
|
||||
else:
|
||||
self.emit('connection_phy_update_failure', connection.handle, event.status)
|
||||
|
||||
def on_hci_le_advertising_report_event(
|
||||
self,
|
||||
event: (
|
||||
hci.HCI_LE_Advertising_Report_Event
|
||||
| hci.HCI_LE_Extended_Advertising_Report_Event
|
||||
),
|
||||
):
|
||||
def on_hci_le_advertising_report_event(self, event):
|
||||
for report in event.reports:
|
||||
self.emit('advertising_report', report)
|
||||
|
||||
def on_hci_le_extended_advertising_report_event(
|
||||
self, event: hci.HCI_LE_Extended_Advertising_Report_Event
|
||||
):
|
||||
def on_hci_le_extended_advertising_report_event(self, event):
|
||||
self.on_hci_le_advertising_report_event(event)
|
||||
|
||||
def on_hci_le_advertising_set_terminated_event(
|
||||
self, event: hci.HCI_LE_Advertising_Set_Terminated_Event
|
||||
):
|
||||
def on_hci_le_advertising_set_terminated_event(self, event):
|
||||
self.emit(
|
||||
'advertising_set_termination',
|
||||
event.status,
|
||||
@@ -1166,9 +1143,7 @@ class Host(utils.EventEmitter):
|
||||
event.num_completed_extended_advertising_events,
|
||||
)
|
||||
|
||||
def on_hci_le_periodic_advertising_sync_established_event(
|
||||
self, event: hci.HCI_LE_Periodic_Advertising_Sync_Established_Event
|
||||
):
|
||||
def on_hci_le_periodic_advertising_sync_established_event(self, event):
|
||||
self.emit(
|
||||
'periodic_advertising_sync_establishment',
|
||||
event.status,
|
||||
@@ -1180,22 +1155,16 @@ class Host(utils.EventEmitter):
|
||||
event.advertiser_clock_accuracy,
|
||||
)
|
||||
|
||||
def on_hci_le_periodic_advertising_sync_lost_event(
|
||||
self, event: hci.HCI_LE_Periodic_Advertising_Sync_Lost_Event
|
||||
):
|
||||
def on_hci_le_periodic_advertising_sync_lost_event(self, event):
|
||||
self.emit('periodic_advertising_sync_loss', event.sync_handle)
|
||||
|
||||
def on_hci_le_periodic_advertising_report_event(
|
||||
self, event: hci.HCI_LE_Periodic_Advertising_Report_Event
|
||||
):
|
||||
def on_hci_le_periodic_advertising_report_event(self, event):
|
||||
self.emit('periodic_advertising_report', event.sync_handle, event)
|
||||
|
||||
def on_hci_le_biginfo_advertising_report_event(
|
||||
self, event: hci.HCI_LE_BIGInfo_Advertising_Report_Event
|
||||
):
|
||||
def on_hci_le_biginfo_advertising_report_event(self, event):
|
||||
self.emit('biginfo_advertising_report', event.sync_handle, event)
|
||||
|
||||
def on_hci_le_cis_request_event(self, event: hci.HCI_LE_CIS_Request_Event):
|
||||
def on_hci_le_cis_request_event(self, event):
|
||||
self.emit(
|
||||
'cis_request',
|
||||
event.acl_connection_handle,
|
||||
@@ -1204,12 +1173,10 @@ class Host(utils.EventEmitter):
|
||||
event.cis_id,
|
||||
)
|
||||
|
||||
def on_hci_le_create_big_complete_event(
|
||||
self, event: hci.HCI_LE_Create_BIG_Complete_Event
|
||||
):
|
||||
def on_hci_le_create_big_complete_event(self, event):
|
||||
self.bigs[event.big_handle] = set(event.connection_handle)
|
||||
if self.iso_packet_queue is None:
|
||||
raise InvalidStateError("BIS established but ISO packets not supported")
|
||||
logger.warning("BIS established but ISO packets not supported")
|
||||
|
||||
for connection_handle in event.connection_handle:
|
||||
self.bis_links[connection_handle] = IsoLink(
|
||||
@@ -1232,13 +1199,8 @@ class Host(utils.EventEmitter):
|
||||
event.iso_interval,
|
||||
)
|
||||
|
||||
def on_hci_le_big_sync_established_event(
|
||||
self, event: hci.HCI_LE_BIG_Sync_Established_Event
|
||||
):
|
||||
def on_hci_le_big_sync_established_event(self, event):
|
||||
self.bigs[event.big_handle] = set(event.connection_handle)
|
||||
if self.iso_packet_queue is None:
|
||||
raise InvalidStateError("BIS established but ISO packets not supported")
|
||||
|
||||
for connection_handle in event.connection_handle:
|
||||
self.bis_links[connection_handle] = IsoLink(
|
||||
connection_handle, self.iso_packet_queue
|
||||
@@ -1258,19 +1220,15 @@ class Host(utils.EventEmitter):
|
||||
event.connection_handle,
|
||||
)
|
||||
|
||||
def on_hci_le_big_sync_lost_event(self, event: hci.HCI_LE_BIG_Sync_Lost_Event):
|
||||
def on_hci_le_big_sync_lost_event(self, event):
|
||||
self.remove_big(event.big_handle)
|
||||
self.emit('big_sync_lost', event.big_handle, event.reason)
|
||||
|
||||
def on_hci_le_terminate_big_complete_event(
|
||||
self, event: hci.HCI_LE_Terminate_BIG_Complete_Event
|
||||
):
|
||||
def on_hci_le_terminate_big_complete_event(self, event):
|
||||
self.remove_big(event.big_handle)
|
||||
self.emit('big_termination', event.reason, event.big_handle)
|
||||
|
||||
def on_hci_le_periodic_advertising_sync_transfer_received_event(
|
||||
self, event: hci.HCI_LE_Periodic_Advertising_Sync_Transfer_Received_Event
|
||||
):
|
||||
def on_hci_le_periodic_advertising_sync_transfer_received_event(self, event):
|
||||
self.emit(
|
||||
'periodic_advertising_sync_transfer',
|
||||
event.status,
|
||||
@@ -1283,9 +1241,7 @@ class Host(utils.EventEmitter):
|
||||
event.advertiser_clock_accuracy,
|
||||
)
|
||||
|
||||
def on_hci_le_periodic_advertising_sync_transfer_received_v2_event(
|
||||
self, event: hci.HCI_LE_Periodic_Advertising_Sync_Transfer_Received_V2_Event
|
||||
):
|
||||
def on_hci_le_periodic_advertising_sync_transfer_received_v2_event(self, event):
|
||||
self.emit(
|
||||
'periodic_advertising_sync_transfer',
|
||||
event.status,
|
||||
@@ -1298,40 +1254,21 @@ class Host(utils.EventEmitter):
|
||||
event.advertiser_clock_accuracy,
|
||||
)
|
||||
|
||||
def on_hci_le_cis_established_event(self, event: hci.HCI_LE_CIS_Established_Event):
|
||||
def on_hci_le_cis_established_event(self, event):
|
||||
# The remaining parameters are unused for now.
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
if self.iso_packet_queue is None:
|
||||
raise InvalidStateError("CIS established but ISO packets not supported")
|
||||
logger.warning("CIS established but ISO packets not supported")
|
||||
self.cis_links[event.connection_handle] = IsoLink(
|
||||
handle=event.connection_handle, packet_queue=self.iso_packet_queue
|
||||
)
|
||||
self.emit(
|
||||
'cis_establishment',
|
||||
event.connection_handle,
|
||||
event.cig_sync_delay,
|
||||
event.cis_sync_delay,
|
||||
event.transport_latency_c_to_p,
|
||||
event.transport_latency_p_to_c,
|
||||
event.phy_c_to_p,
|
||||
event.phy_p_to_c,
|
||||
event.nse,
|
||||
event.bn_c_to_p,
|
||||
event.bn_p_to_c,
|
||||
event.ft_c_to_p,
|
||||
event.ft_p_to_c,
|
||||
event.max_pdu_c_to_p,
|
||||
event.max_pdu_p_to_c,
|
||||
event.iso_interval,
|
||||
)
|
||||
self.emit('cis_establishment', event.connection_handle)
|
||||
else:
|
||||
self.emit(
|
||||
'cis_establishment_failure', event.connection_handle, event.status
|
||||
)
|
||||
|
||||
def on_hci_le_remote_connection_parameter_request_event(
|
||||
self, event: hci.HCI_LE_Remote_Connection_Parameter_Request_Event
|
||||
):
|
||||
def on_hci_le_remote_connection_parameter_request_event(self, event):
|
||||
if event.connection_handle not in self.connections:
|
||||
logger.warning('!!! REMOTE CONNECTION PARAMETER REQUEST: unknown handle')
|
||||
return
|
||||
@@ -1350,9 +1287,7 @@ class Host(utils.EventEmitter):
|
||||
)
|
||||
)
|
||||
|
||||
def on_hci_le_long_term_key_request_event(
|
||||
self, event: hci.HCI_LE_Long_Term_Key_Request_Event
|
||||
):
|
||||
def on_hci_le_long_term_key_request_event(self, event):
|
||||
if (connection := self.connections.get(event.connection_handle)) is None:
|
||||
logger.warning('!!! LE LONG TERM KEY REQUEST: unknown handle')
|
||||
return
|
||||
@@ -1386,13 +1321,12 @@ class Host(utils.EventEmitter):
|
||||
|
||||
asyncio.create_task(send_long_term_key())
|
||||
|
||||
def on_hci_synchronous_connection_complete_event(
|
||||
self, event: hci.HCI_Synchronous_Connection_Complete_Event
|
||||
):
|
||||
def on_hci_synchronous_connection_complete_event(self, event):
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
# Create/update the connection
|
||||
logger.debug(
|
||||
f'### SCO CONNECTION: [0x{event.connection_handle:04X}] {event.bd_addr}'
|
||||
f'### SCO CONNECTION: [0x{event.connection_handle:04X}] '
|
||||
f'{event.bd_addr}'
|
||||
)
|
||||
|
||||
self.sco_links[event.connection_handle] = ScoLink(
|
||||
@@ -1413,21 +1347,10 @@ class Host(utils.EventEmitter):
|
||||
# Notify the client
|
||||
self.emit('sco_connection_failure', event.bd_addr, event.status)
|
||||
|
||||
def on_hci_synchronous_connection_changed_event(
|
||||
self, event: hci.HCI_Synchronous_Connection_Changed_Event
|
||||
):
|
||||
def on_hci_synchronous_connection_changed_event(self, event):
|
||||
pass
|
||||
|
||||
def on_hci_mode_change_event(self, event: hci.HCI_Mode_Change_Event):
|
||||
self.emit(
|
||||
'mode_change',
|
||||
event.connection_handle,
|
||||
event.status,
|
||||
event.current_mode,
|
||||
event.interval,
|
||||
)
|
||||
|
||||
def on_hci_role_change_event(self, event: hci.HCI_Role_Change_Event):
|
||||
def on_hci_role_change_event(self, event):
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
logger.debug(
|
||||
f'role change for {event.bd_addr}: '
|
||||
@@ -1441,13 +1364,7 @@ class Host(utils.EventEmitter):
|
||||
)
|
||||
self.emit('role_change_failure', event.bd_addr, event.status)
|
||||
|
||||
def on_hci_le_data_length_change_event(
|
||||
self, event: hci.HCI_LE_Data_Length_Change_Event
|
||||
):
|
||||
if event.connection_handle not in self.connections:
|
||||
logger.warning('!!! DATA LENGTH CHANGE: unknown handle')
|
||||
return
|
||||
|
||||
def on_hci_le_data_length_change_event(self, event):
|
||||
self.emit(
|
||||
'connection_data_length_change',
|
||||
event.connection_handle,
|
||||
@@ -1457,9 +1374,7 @@ class Host(utils.EventEmitter):
|
||||
event.max_rx_time,
|
||||
)
|
||||
|
||||
def on_hci_authentication_complete_event(
|
||||
self, event: hci.HCI_Authentication_Complete_Event
|
||||
):
|
||||
def on_hci_authentication_complete_event(self, event):
|
||||
# Notify the client
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
self.emit('connection_authentication', event.connection_handle)
|
||||
@@ -1470,7 +1385,7 @@ class Host(utils.EventEmitter):
|
||||
event.status,
|
||||
)
|
||||
|
||||
def on_hci_encryption_change_event(self, event: hci.HCI_Encryption_Change_Event):
|
||||
def on_hci_encryption_change_event(self, event):
|
||||
# Notify the client
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
self.emit(
|
||||
@@ -1484,9 +1399,7 @@ class Host(utils.EventEmitter):
|
||||
'connection_encryption_failure', event.connection_handle, event.status
|
||||
)
|
||||
|
||||
def on_hci_encryption_change_v2_event(
|
||||
self, event: hci.HCI_Encryption_Change_V2_Event
|
||||
):
|
||||
def on_hci_encryption_change_v2_event(self, event):
|
||||
# Notify the client
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
self.emit(
|
||||
@@ -1500,9 +1413,7 @@ class Host(utils.EventEmitter):
|
||||
'connection_encryption_failure', event.connection_handle, event.status
|
||||
)
|
||||
|
||||
def on_hci_encryption_key_refresh_complete_event(
|
||||
self, event: hci.HCI_Encryption_Key_Refresh_Complete_Event
|
||||
):
|
||||
def on_hci_encryption_key_refresh_complete_event(self, event):
|
||||
# Notify the client
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
self.emit('connection_encryption_key_refresh', event.connection_handle)
|
||||
@@ -1513,7 +1424,7 @@ class Host(utils.EventEmitter):
|
||||
event.status,
|
||||
)
|
||||
|
||||
def on_hci_qos_setup_complete_event(self, event: hci.HCI_QOS_Setup_Complete_Event):
|
||||
def on_hci_qos_setup_complete_event(self, event):
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
self.emit(
|
||||
'connection_qos_setup', event.connection_handle, event.service_type
|
||||
@@ -1525,31 +1436,23 @@ class Host(utils.EventEmitter):
|
||||
event.status,
|
||||
)
|
||||
|
||||
def on_hci_link_supervision_timeout_changed_event(
|
||||
self, event: hci.HCI_Link_Supervision_Timeout_Changed_Event
|
||||
):
|
||||
def on_hci_link_supervision_timeout_changed_event(self, event):
|
||||
pass
|
||||
|
||||
def on_hci_max_slots_change_event(self, event: hci.HCI_Max_Slots_Change_Event):
|
||||
def on_hci_max_slots_change_event(self, event):
|
||||
pass
|
||||
|
||||
def on_hci_page_scan_repetition_mode_change_event(
|
||||
self, event: hci.HCI_Page_Scan_Repetition_Mode_Change_Event
|
||||
):
|
||||
def on_hci_page_scan_repetition_mode_change_event(self, event):
|
||||
pass
|
||||
|
||||
def on_hci_link_key_notification_event(
|
||||
self, event: hci.HCI_Link_Key_Notification_Event
|
||||
):
|
||||
def on_hci_link_key_notification_event(self, event):
|
||||
logger.debug(
|
||||
f'link key for {event.bd_addr}: {event.link_key.hex()}, '
|
||||
f'type={hci.HCI_Constant.link_key_type_name(event.key_type)}'
|
||||
)
|
||||
self.emit('link_key', event.bd_addr, event.link_key, event.key_type)
|
||||
|
||||
def on_hci_simple_pairing_complete_event(
|
||||
self, event: hci.HCI_Simple_Pairing_Complete_Event
|
||||
):
|
||||
def on_hci_simple_pairing_complete_event(self, event):
|
||||
logger.debug(
|
||||
f'simple pairing complete for {event.bd_addr}: '
|
||||
f'status={hci.HCI_Constant.status_name(event.status)}'
|
||||
@@ -1559,10 +1462,10 @@ class Host(utils.EventEmitter):
|
||||
else:
|
||||
self.emit('classic_pairing_failure', event.bd_addr, event.status)
|
||||
|
||||
def on_hci_pin_code_request_event(self, event: hci.HCI_PIN_Code_Request_Event):
|
||||
def on_hci_pin_code_request_event(self, event):
|
||||
self.emit('pin_code_request', event.bd_addr)
|
||||
|
||||
def on_hci_link_key_request_event(self, event: hci.HCI_Link_Key_Request_Event):
|
||||
def on_hci_link_key_request_event(self, event):
|
||||
async def send_link_key():
|
||||
if self.link_key_provider is None:
|
||||
logger.debug('no link key provider')
|
||||
@@ -1587,14 +1490,10 @@ class Host(utils.EventEmitter):
|
||||
|
||||
asyncio.create_task(send_link_key())
|
||||
|
||||
def on_hci_io_capability_request_event(
|
||||
self, event: hci.HCI_IO_Capability_Request_Event
|
||||
):
|
||||
def on_hci_io_capability_request_event(self, event):
|
||||
self.emit('authentication_io_capability_request', event.bd_addr)
|
||||
|
||||
def on_hci_io_capability_response_event(
|
||||
self, event: hci.HCI_IO_Capability_Response_Event
|
||||
):
|
||||
def on_hci_io_capability_response_event(self, event):
|
||||
self.emit(
|
||||
'authentication_io_capability_response',
|
||||
event.bd_addr,
|
||||
@@ -1602,47 +1501,35 @@ class Host(utils.EventEmitter):
|
||||
event.authentication_requirements,
|
||||
)
|
||||
|
||||
def on_hci_user_confirmation_request_event(
|
||||
self, event: hci.HCI_User_Confirmation_Request_Event
|
||||
):
|
||||
def on_hci_user_confirmation_request_event(self, event):
|
||||
self.emit(
|
||||
'authentication_user_confirmation_request',
|
||||
event.bd_addr,
|
||||
event.numeric_value,
|
||||
)
|
||||
|
||||
def on_hci_user_passkey_request_event(
|
||||
self, event: hci.HCI_User_Passkey_Request_Event
|
||||
):
|
||||
def on_hci_user_passkey_request_event(self, event):
|
||||
self.emit('authentication_user_passkey_request', event.bd_addr)
|
||||
|
||||
def on_hci_user_passkey_notification_event(
|
||||
self, event: hci.HCI_User_Passkey_Notification_Event
|
||||
):
|
||||
def on_hci_user_passkey_notification_event(self, event):
|
||||
self.emit(
|
||||
'authentication_user_passkey_notification', event.bd_addr, event.passkey
|
||||
)
|
||||
|
||||
def on_hci_inquiry_complete_event(self, _event: hci.HCI_Inquiry_Complete_Event):
|
||||
def on_hci_inquiry_complete_event(self, _event):
|
||||
self.emit('inquiry_complete')
|
||||
|
||||
def on_hci_inquiry_result_with_rssi_event(
|
||||
self, event: hci.HCI_Inquiry_Result_With_RSSI_Event
|
||||
):
|
||||
for bd_addr, class_of_device, rssi in zip(
|
||||
event.bd_addr, event.class_of_device, event.rssi
|
||||
):
|
||||
def on_hci_inquiry_result_with_rssi_event(self, event):
|
||||
for response in event.responses:
|
||||
self.emit(
|
||||
'inquiry_result',
|
||||
bd_addr,
|
||||
class_of_device,
|
||||
response.bd_addr,
|
||||
response.class_of_device,
|
||||
b'',
|
||||
rssi,
|
||||
response.rssi,
|
||||
)
|
||||
|
||||
def on_hci_extended_inquiry_result_event(
|
||||
self, event: hci.HCI_Extended_Inquiry_Result_Event
|
||||
):
|
||||
def on_hci_extended_inquiry_result_event(self, event):
|
||||
self.emit(
|
||||
'inquiry_result',
|
||||
event.bd_addr,
|
||||
@@ -1651,9 +1538,7 @@ class Host(utils.EventEmitter):
|
||||
event.rssi,
|
||||
)
|
||||
|
||||
def on_hci_remote_name_request_complete_event(
|
||||
self, event: hci.HCI_Remote_Name_Request_Complete_Event
|
||||
):
|
||||
def on_hci_remote_name_request_complete_event(self, event):
|
||||
if event.status != hci.HCI_SUCCESS:
|
||||
self.emit('remote_name_failure', event.bd_addr, event.status)
|
||||
else:
|
||||
@@ -1664,18 +1549,14 @@ class Host(utils.EventEmitter):
|
||||
|
||||
self.emit('remote_name', event.bd_addr, utf8_name)
|
||||
|
||||
def on_hci_remote_host_supported_features_notification_event(
|
||||
self, event: hci.HCI_Remote_Host_Supported_Features_Notification_Event
|
||||
):
|
||||
def on_hci_remote_host_supported_features_notification_event(self, event):
|
||||
self.emit(
|
||||
'remote_host_supported_features',
|
||||
event.bd_addr,
|
||||
event.host_supported_features,
|
||||
)
|
||||
|
||||
def on_hci_le_read_remote_features_complete_event(
|
||||
self, event: hci.HCI_LE_Read_Remote_Features_Complete_Event
|
||||
):
|
||||
def on_hci_le_read_remote_features_complete_event(self, event):
|
||||
if event.status != hci.HCI_SUCCESS:
|
||||
self.emit(
|
||||
'le_remote_features_failure', event.connection_handle, event.status
|
||||
@@ -1687,45 +1568,23 @@ class Host(utils.EventEmitter):
|
||||
int.from_bytes(event.le_features, 'little'),
|
||||
)
|
||||
|
||||
def on_hci_le_cs_read_remote_supported_capabilities_complete_event(
|
||||
self, event: hci.HCI_LE_CS_Read_Remote_Supported_Capabilities_Complete_Event
|
||||
):
|
||||
def on_hci_le_cs_read_remote_supported_capabilities_complete_event(self, event):
|
||||
self.emit('cs_remote_supported_capabilities', event)
|
||||
|
||||
def on_hci_le_cs_security_enable_complete_event(
|
||||
self, event: hci.HCI_LE_CS_Security_Enable_Complete_Event
|
||||
):
|
||||
def on_hci_le_cs_security_enable_complete_event(self, event):
|
||||
self.emit('cs_security', event)
|
||||
|
||||
def on_hci_le_cs_config_complete_event(
|
||||
self, event: hci.HCI_LE_CS_Config_Complete_Event
|
||||
):
|
||||
def on_hci_le_cs_config_complete_event(self, event):
|
||||
self.emit('cs_config', event)
|
||||
|
||||
def on_hci_le_cs_procedure_enable_complete_event(
|
||||
self, event: hci.HCI_LE_CS_Procedure_Enable_Complete_Event
|
||||
):
|
||||
def on_hci_le_cs_procedure_enable_complete_event(self, event):
|
||||
self.emit('cs_procedure', event)
|
||||
|
||||
def on_hci_le_cs_subevent_result_event(
|
||||
self, event: hci.HCI_LE_CS_Subevent_Result_Event
|
||||
):
|
||||
def on_hci_le_cs_subevent_result_event(self, event):
|
||||
self.emit('cs_subevent_result', event)
|
||||
|
||||
def on_hci_le_cs_subevent_result_continue_event(
|
||||
self, event: hci.HCI_LE_CS_Subevent_Result_Continue_Event
|
||||
):
|
||||
def on_hci_le_cs_subevent_result_continue_event(self, event):
|
||||
self.emit('cs_subevent_result_continue', event)
|
||||
|
||||
def on_hci_le_subrate_change_event(self, event: hci.HCI_LE_Subrate_Change_Event):
|
||||
self.emit(
|
||||
'le_subrate_change',
|
||||
event.connection_handle,
|
||||
event.subrate_factor,
|
||||
event.peripheral_latency,
|
||||
event.continuation_number,
|
||||
event.supervision_timeout,
|
||||
)
|
||||
|
||||
def on_hci_vendor_event(self, event: hci.HCI_Vendor_Event):
|
||||
def on_hci_vendor_event(self, event):
|
||||
self.emit('vendor_event', event)
|
||||
|
||||
@@ -21,18 +21,16 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import json
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type, Any
|
||||
from typing_extensions import Self
|
||||
|
||||
from bumble import hci
|
||||
from bumble.colors import color
|
||||
from bumble import hci
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bumble.device import Device
|
||||
@@ -51,8 +49,8 @@ class PairingKeys:
|
||||
class Key:
|
||||
value: bytes
|
||||
authenticated: bool = False
|
||||
ediv: int | None = None
|
||||
rand: bytes | None = None
|
||||
ediv: Optional[int] = None
|
||||
rand: Optional[bytes] = None
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, key_dict: dict[str, Any]) -> PairingKeys.Key:
|
||||
@@ -74,17 +72,17 @@ class PairingKeys:
|
||||
|
||||
return key_dict
|
||||
|
||||
address_type: hci.AddressType | None = None
|
||||
ltk: Key | None = None
|
||||
ltk_central: Key | None = None
|
||||
ltk_peripheral: Key | None = None
|
||||
irk: Key | None = None
|
||||
csrk: Key | None = None
|
||||
link_key: Key | None = None # Classic
|
||||
link_key_type: int | None = None # Classic
|
||||
address_type: Optional[hci.AddressType] = None
|
||||
ltk: Optional[Key] = None
|
||||
ltk_central: Optional[Key] = None
|
||||
ltk_peripheral: Optional[Key] = None
|
||||
irk: Optional[Key] = None
|
||||
csrk: Optional[Key] = None
|
||||
link_key: Optional[Key] = None # Classic
|
||||
link_key_type: Optional[int] = None # Classic
|
||||
|
||||
@classmethod
|
||||
def key_from_dict(cls, keys_dict: dict[str, Any], key_name: str) -> Key | None:
|
||||
def key_from_dict(cls, keys_dict: dict[str, Any], key_name: str) -> Optional[Key]:
|
||||
key_dict = keys_dict.get(key_name)
|
||||
if key_dict is None:
|
||||
return None
|
||||
@@ -156,10 +154,10 @@ class KeyStore:
|
||||
async def update(self, name: str, keys: PairingKeys) -> None:
|
||||
pass
|
||||
|
||||
async def get(self, _name: str) -> PairingKeys | None:
|
||||
async def get(self, _name: str) -> Optional[PairingKeys]:
|
||||
return None
|
||||
|
||||
async def get_all(self) -> list[tuple[str, PairingKeys]]:
|
||||
async def get_all(self) -> List[Tuple[str, PairingKeys]]:
|
||||
return []
|
||||
|
||||
async def delete_all(self) -> None:
|
||||
@@ -274,7 +272,7 @@ class JsonKeyStore(KeyStore):
|
||||
|
||||
@classmethod
|
||||
def from_device(
|
||||
cls: type[Self], device: Device, filename: str | None = None
|
||||
cls: Type[Self], device: Device, filename: Optional[str] = None
|
||||
) -> Self:
|
||||
if not filename:
|
||||
# Extract the filename from the config if there is one
|
||||
@@ -297,7 +295,7 @@ class JsonKeyStore(KeyStore):
|
||||
# Try to open the file, without failing. If the file does not exist, it
|
||||
# will be created upon saving.
|
||||
try:
|
||||
with open(self.filename, encoding='utf-8') as json_file:
|
||||
with open(self.filename, 'r', encoding='utf-8') as json_file:
|
||||
db = json.load(json_file)
|
||||
except FileNotFoundError:
|
||||
db = {}
|
||||
@@ -348,7 +346,7 @@ class JsonKeyStore(KeyStore):
|
||||
key_map.clear()
|
||||
await self.save(db)
|
||||
|
||||
async def get(self, name: str) -> PairingKeys | None:
|
||||
async def get(self, name: str) -> Optional[PairingKeys]:
|
||||
_, key_map = await self.load()
|
||||
if name not in key_map:
|
||||
return None
|
||||
@@ -358,7 +356,7 @@ class JsonKeyStore(KeyStore):
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class MemoryKeyStore(KeyStore):
|
||||
all_keys: dict[str, PairingKeys]
|
||||
all_keys: Dict[str, PairingKeys]
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.all_keys = {}
|
||||
@@ -370,8 +368,8 @@ class MemoryKeyStore(KeyStore):
|
||||
async def update(self, name: str, keys: PairingKeys) -> None:
|
||||
self.all_keys[name] = keys
|
||||
|
||||
async def get(self, name: str) -> PairingKeys | None:
|
||||
async def get(self, name: str) -> Optional[PairingKeys]:
|
||||
return self.all_keys.get(name)
|
||||
|
||||
async def get_all(self) -> list[tuple[str, PairingKeys]]:
|
||||
async def get_all(self) -> List[Tuple[str, PairingKeys]]:
|
||||
return list(self.all_keys.items())
|
||||
|
||||
2099
bumble/l2cap.py
2099
bumble/l2cap.py
File diff suppressed because it is too large
Load Diff
621
bumble/link.py
621
bumble/link.py
@@ -11,20 +11,32 @@
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
import asyncio
|
||||
from functools import partial
|
||||
|
||||
from bumble import core, hci, ll, lmp
|
||||
from bumble.core import (
|
||||
PhysicalTransport,
|
||||
InvalidStateError,
|
||||
)
|
||||
from bumble.colors import color
|
||||
from bumble.hci import (
|
||||
Address,
|
||||
Role,
|
||||
HCI_SUCCESS,
|
||||
HCI_CONNECTION_ACCEPT_TIMEOUT_ERROR,
|
||||
HCI_CONNECTION_TIMEOUT_ERROR,
|
||||
HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR,
|
||||
HCI_PAGE_TIMEOUT_ERROR,
|
||||
HCI_Connection_Complete_Event,
|
||||
)
|
||||
from bumble import controller
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bumble import controller
|
||||
from typing import Optional, Set
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -32,6 +44,18 @@ if TYPE_CHECKING:
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Utils
|
||||
# -----------------------------------------------------------------------------
|
||||
def parse_parameters(params_str):
|
||||
result = {}
|
||||
for param_str in params_str.split(','):
|
||||
if '=' in param_str:
|
||||
key, value = param_str.split('=')
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# TODO: add more support for various LL exchanges
|
||||
# (see Vol 6, Part B - 2.4 DATA CHANNEL PDU)
|
||||
@@ -41,38 +65,41 @@ class LocalLink:
|
||||
Link bus for controllers to communicate with each other
|
||||
'''
|
||||
|
||||
controllers: set[controller.Controller]
|
||||
controllers: Set[controller.Controller]
|
||||
|
||||
def __init__(self):
|
||||
self.controllers = set()
|
||||
self.pending_connection = None
|
||||
self.pending_classic_connection = None
|
||||
|
||||
############################################################
|
||||
# Common utils
|
||||
############################################################
|
||||
|
||||
def add_controller(self, controller: controller.Controller):
|
||||
def add_controller(self, controller):
|
||||
logger.debug(f'new controller: {controller}')
|
||||
self.controllers.add(controller)
|
||||
|
||||
def remove_controller(self, controller: controller.Controller):
|
||||
def remove_controller(self, controller):
|
||||
self.controllers.remove(controller)
|
||||
|
||||
def find_le_controller(self, address: hci.Address) -> controller.Controller | None:
|
||||
def find_controller(self, address):
|
||||
for controller in self.controllers:
|
||||
for connection in controller.le_connections.values():
|
||||
if connection.self_address == address:
|
||||
return controller
|
||||
if controller.random_address == address:
|
||||
return controller
|
||||
return None
|
||||
|
||||
def find_classic_controller(
|
||||
self, address: hci.Address
|
||||
) -> controller.Controller | None:
|
||||
self, address: Address
|
||||
) -> Optional[controller.Controller]:
|
||||
for controller in self.controllers:
|
||||
if controller.public_address == address:
|
||||
return controller
|
||||
return None
|
||||
|
||||
def get_pending_connection(self):
|
||||
return self.pending_connection
|
||||
|
||||
############################################################
|
||||
# LE handlers
|
||||
############################################################
|
||||
@@ -80,70 +107,538 @@ class LocalLink:
|
||||
def on_address_changed(self, controller):
|
||||
pass
|
||||
|
||||
def send_acl_data(
|
||||
self,
|
||||
sender_controller: controller.Controller,
|
||||
destination_address: hci.Address,
|
||||
transport: core.PhysicalTransport,
|
||||
data: bytes,
|
||||
):
|
||||
def send_advertising_data(self, sender_address, data):
|
||||
# Send the advertising data to all controllers, except the sender
|
||||
for controller in self.controllers:
|
||||
if controller.random_address != sender_address:
|
||||
controller.on_link_advertising_data(sender_address, data)
|
||||
|
||||
def send_acl_data(self, sender_controller, destination_address, transport, data):
|
||||
# Send the data to the first controller with a matching address
|
||||
if transport == core.PhysicalTransport.LE:
|
||||
destination_controller = self.find_le_controller(destination_address)
|
||||
if transport == PhysicalTransport.LE:
|
||||
destination_controller = self.find_controller(destination_address)
|
||||
source_address = sender_controller.random_address
|
||||
elif transport == core.PhysicalTransport.BR_EDR:
|
||||
elif transport == PhysicalTransport.BR_EDR:
|
||||
destination_controller = self.find_classic_controller(destination_address)
|
||||
source_address = sender_controller.public_address
|
||||
else:
|
||||
raise ValueError("unsupported transport type")
|
||||
|
||||
if destination_controller is not None:
|
||||
asyncio.get_running_loop().call_soon(
|
||||
lambda: destination_controller.on_link_acl_data(
|
||||
source_address, transport, data
|
||||
)
|
||||
)
|
||||
destination_controller.on_link_acl_data(source_address, transport, data)
|
||||
|
||||
def send_advertising_pdu(
|
||||
self,
|
||||
sender_controller: controller.Controller,
|
||||
packet: ll.AdvertisingPdu,
|
||||
):
|
||||
loop = asyncio.get_running_loop()
|
||||
for c in self.controllers:
|
||||
if c != sender_controller:
|
||||
loop.call_soon(c.on_ll_advertising_pdu, packet)
|
||||
def on_connection_complete(self):
|
||||
# Check that we expect this call
|
||||
if not self.pending_connection:
|
||||
logger.warning('on_connection_complete with no pending connection')
|
||||
return
|
||||
|
||||
def send_ll_control_pdu(
|
||||
self,
|
||||
sender_address: hci.Address,
|
||||
receiver_address: hci.Address,
|
||||
packet: ll.ControlPdu,
|
||||
):
|
||||
if not (receiver_controller := self.find_le_controller(receiver_address)):
|
||||
raise core.InvalidArgumentError(
|
||||
f"Unable to find controller for address {receiver_address}"
|
||||
central_address, le_create_connection_command = self.pending_connection
|
||||
self.pending_connection = None
|
||||
|
||||
# Find the controller that initiated the connection
|
||||
if not (central_controller := self.find_controller(central_address)):
|
||||
logger.warning('!!! Initiating controller not found')
|
||||
return
|
||||
|
||||
# Connect to the first controller with a matching address
|
||||
if peripheral_controller := self.find_controller(
|
||||
le_create_connection_command.peer_address
|
||||
):
|
||||
central_controller.on_link_peripheral_connection_complete(
|
||||
le_create_connection_command, HCI_SUCCESS
|
||||
)
|
||||
asyncio.get_running_loop().call_soon(
|
||||
lambda: receiver_controller.on_ll_control_pdu(sender_address, packet)
|
||||
peripheral_controller.on_link_central_connected(central_address)
|
||||
return
|
||||
|
||||
# No peripheral found
|
||||
central_controller.on_link_peripheral_connection_complete(
|
||||
le_create_connection_command, HCI_CONNECTION_ACCEPT_TIMEOUT_ERROR
|
||||
)
|
||||
|
||||
def connect(self, central_address, le_create_connection_command):
|
||||
logger.debug(
|
||||
f'$$$ CONNECTION {central_address} -> '
|
||||
f'{le_create_connection_command.peer_address}'
|
||||
)
|
||||
self.pending_connection = (central_address, le_create_connection_command)
|
||||
asyncio.get_running_loop().call_soon(self.on_connection_complete)
|
||||
|
||||
def on_disconnection_complete(
|
||||
self, central_address, peripheral_address, disconnect_command
|
||||
):
|
||||
# Find the controller that initiated the disconnection
|
||||
if not (central_controller := self.find_controller(central_address)):
|
||||
logger.warning('!!! Initiating controller not found')
|
||||
return
|
||||
|
||||
# Disconnect from the first controller with a matching address
|
||||
if peripheral_controller := self.find_controller(peripheral_address):
|
||||
peripheral_controller.on_link_central_disconnected(
|
||||
central_address, disconnect_command.reason
|
||||
)
|
||||
|
||||
central_controller.on_link_peripheral_disconnection_complete(
|
||||
disconnect_command, HCI_SUCCESS
|
||||
)
|
||||
|
||||
def disconnect(self, central_address, peripheral_address, disconnect_command):
|
||||
logger.debug(
|
||||
f'$$$ DISCONNECTION {central_address} -> '
|
||||
f'{peripheral_address}: reason = {disconnect_command.reason}'
|
||||
)
|
||||
args = [central_address, peripheral_address, disconnect_command]
|
||||
asyncio.get_running_loop().call_soon(self.on_disconnection_complete, *args)
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def on_connection_encrypted(
|
||||
self, central_address, peripheral_address, rand, ediv, ltk
|
||||
):
|
||||
logger.debug(f'*** ENCRYPTION {central_address} -> {peripheral_address}')
|
||||
|
||||
if central_controller := self.find_controller(central_address):
|
||||
central_controller.on_link_encrypted(peripheral_address, rand, ediv, ltk)
|
||||
|
||||
if peripheral_controller := self.find_controller(peripheral_address):
|
||||
peripheral_controller.on_link_encrypted(central_address, rand, ediv, ltk)
|
||||
|
||||
def create_cis(
|
||||
self,
|
||||
central_controller: controller.Controller,
|
||||
peripheral_address: Address,
|
||||
cig_id: int,
|
||||
cis_id: int,
|
||||
) -> None:
|
||||
logger.debug(
|
||||
f'$$$ CIS Request {central_controller.random_address} -> {peripheral_address}'
|
||||
)
|
||||
if peripheral_controller := self.find_controller(peripheral_address):
|
||||
asyncio.get_running_loop().call_soon(
|
||||
peripheral_controller.on_link_cis_request,
|
||||
central_controller.random_address,
|
||||
cig_id,
|
||||
cis_id,
|
||||
)
|
||||
|
||||
def accept_cis(
|
||||
self,
|
||||
peripheral_controller: controller.Controller,
|
||||
central_address: Address,
|
||||
cig_id: int,
|
||||
cis_id: int,
|
||||
) -> None:
|
||||
logger.debug(
|
||||
f'$$$ CIS Accept {peripheral_controller.random_address} -> {central_address}'
|
||||
)
|
||||
if central_controller := self.find_controller(central_address):
|
||||
asyncio.get_running_loop().call_soon(
|
||||
central_controller.on_link_cis_established, cig_id, cis_id
|
||||
)
|
||||
asyncio.get_running_loop().call_soon(
|
||||
peripheral_controller.on_link_cis_established, cig_id, cis_id
|
||||
)
|
||||
|
||||
def disconnect_cis(
|
||||
self,
|
||||
initiator_controller: controller.Controller,
|
||||
peer_address: Address,
|
||||
cig_id: int,
|
||||
cis_id: int,
|
||||
) -> None:
|
||||
logger.debug(
|
||||
f'$$$ CIS Disconnect {initiator_controller.random_address} -> {peer_address}'
|
||||
)
|
||||
if peer_controller := self.find_controller(peer_address):
|
||||
asyncio.get_running_loop().call_soon(
|
||||
initiator_controller.on_link_cis_disconnected, cig_id, cis_id
|
||||
)
|
||||
asyncio.get_running_loop().call_soon(
|
||||
peer_controller.on_link_cis_disconnected, cig_id, cis_id
|
||||
)
|
||||
|
||||
############################################################
|
||||
# Classic handlers
|
||||
############################################################
|
||||
|
||||
def send_lmp_packet(
|
||||
self,
|
||||
sender_controller: controller.Controller,
|
||||
receiver_address: hci.Address,
|
||||
packet: lmp.Packet,
|
||||
):
|
||||
if not (receiver_controller := self.find_classic_controller(receiver_address)):
|
||||
raise core.InvalidArgumentError(
|
||||
f"Unable to find controller for address {receiver_address}"
|
||||
def classic_connect(self, initiator_controller, responder_address):
|
||||
logger.debug(
|
||||
f'[Classic] {initiator_controller.public_address} connects to {responder_address}'
|
||||
)
|
||||
responder_controller = self.find_classic_controller(responder_address)
|
||||
if responder_controller is None:
|
||||
initiator_controller.on_classic_connection_complete(
|
||||
responder_address, HCI_PAGE_TIMEOUT_ERROR
|
||||
)
|
||||
asyncio.get_running_loop().call_soon(
|
||||
lambda: receiver_controller.on_lmp_packet(
|
||||
sender_controller.public_address, packet
|
||||
return
|
||||
self.pending_classic_connection = (initiator_controller, responder_controller)
|
||||
|
||||
responder_controller.on_classic_connection_request(
|
||||
initiator_controller.public_address,
|
||||
HCI_Connection_Complete_Event.ACL_LINK_TYPE,
|
||||
)
|
||||
|
||||
def classic_accept_connection(
|
||||
self, responder_controller, initiator_address, responder_role
|
||||
):
|
||||
logger.debug(
|
||||
f'[Classic] {responder_controller.public_address} accepts to connect {initiator_address}'
|
||||
)
|
||||
initiator_controller = self.find_classic_controller(initiator_address)
|
||||
if initiator_controller is None:
|
||||
responder_controller.on_classic_connection_complete(
|
||||
responder_controller.public_address, HCI_PAGE_TIMEOUT_ERROR
|
||||
)
|
||||
return
|
||||
|
||||
async def task():
|
||||
if responder_role != Role.PERIPHERAL:
|
||||
initiator_controller.on_classic_role_change(
|
||||
responder_controller.public_address, int(not (responder_role))
|
||||
)
|
||||
initiator_controller.on_classic_connection_complete(
|
||||
responder_controller.public_address, HCI_SUCCESS
|
||||
)
|
||||
|
||||
asyncio.create_task(task())
|
||||
responder_controller.on_classic_role_change(
|
||||
initiator_controller.public_address, responder_role
|
||||
)
|
||||
responder_controller.on_classic_connection_complete(
|
||||
initiator_controller.public_address, HCI_SUCCESS
|
||||
)
|
||||
self.pending_classic_connection = None
|
||||
|
||||
def classic_disconnect(self, initiator_controller, responder_address, reason):
|
||||
logger.debug(
|
||||
f'[Classic] {initiator_controller.public_address} disconnects {responder_address}'
|
||||
)
|
||||
responder_controller = self.find_classic_controller(responder_address)
|
||||
|
||||
async def task():
|
||||
initiator_controller.on_classic_disconnected(responder_address, reason)
|
||||
|
||||
asyncio.create_task(task())
|
||||
responder_controller.on_classic_disconnected(
|
||||
initiator_controller.public_address, reason
|
||||
)
|
||||
|
||||
def classic_switch_role(
|
||||
self, initiator_controller, responder_address, initiator_new_role
|
||||
):
|
||||
responder_controller = self.find_classic_controller(responder_address)
|
||||
if responder_controller is None:
|
||||
return
|
||||
|
||||
async def task():
|
||||
initiator_controller.on_classic_role_change(
|
||||
responder_address, initiator_new_role
|
||||
)
|
||||
|
||||
asyncio.create_task(task())
|
||||
responder_controller.on_classic_role_change(
|
||||
initiator_controller.public_address, int(not (initiator_new_role))
|
||||
)
|
||||
|
||||
def classic_sco_connect(
|
||||
self,
|
||||
initiator_controller: controller.Controller,
|
||||
responder_address: Address,
|
||||
link_type: int,
|
||||
):
|
||||
logger.debug(
|
||||
f'[Classic] {initiator_controller.public_address} connects SCO to {responder_address}'
|
||||
)
|
||||
responder_controller = self.find_classic_controller(responder_address)
|
||||
# Initiator controller should handle it.
|
||||
assert responder_controller
|
||||
|
||||
responder_controller.on_classic_connection_request(
|
||||
initiator_controller.public_address,
|
||||
link_type,
|
||||
)
|
||||
|
||||
def classic_accept_sco_connection(
|
||||
self,
|
||||
responder_controller: controller.Controller,
|
||||
initiator_address: Address,
|
||||
link_type: int,
|
||||
):
|
||||
logger.debug(
|
||||
f'[Classic] {responder_controller.public_address} accepts to connect SCO {initiator_address}'
|
||||
)
|
||||
initiator_controller = self.find_classic_controller(initiator_address)
|
||||
if initiator_controller is None:
|
||||
responder_controller.on_classic_sco_connection_complete(
|
||||
responder_controller.public_address,
|
||||
HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR,
|
||||
link_type,
|
||||
)
|
||||
return
|
||||
|
||||
async def task():
|
||||
initiator_controller.on_classic_sco_connection_complete(
|
||||
responder_controller.public_address, HCI_SUCCESS, link_type
|
||||
)
|
||||
|
||||
asyncio.create_task(task())
|
||||
responder_controller.on_classic_sco_connection_complete(
|
||||
initiator_controller.public_address, HCI_SUCCESS, link_type
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class RemoteLink:
|
||||
'''
|
||||
A Link implementation that communicates with other virtual controllers via a
|
||||
WebSocket relay
|
||||
'''
|
||||
|
||||
def __init__(self, uri):
|
||||
self.controller = None
|
||||
self.uri = uri
|
||||
self.execution_queue = asyncio.Queue()
|
||||
self.websocket = asyncio.get_running_loop().create_future()
|
||||
self.rpc_result = None
|
||||
self.pending_connection = None
|
||||
self.central_connections = set() # List of addresses that we have connected to
|
||||
self.peripheral_connections = (
|
||||
set()
|
||||
) # List of addresses that have connected to us
|
||||
|
||||
# Connect and run asynchronously
|
||||
asyncio.create_task(self.run_connection())
|
||||
asyncio.create_task(self.run_executor_loop())
|
||||
|
||||
def add_controller(self, controller):
|
||||
if self.controller:
|
||||
raise InvalidStateError('controller already set')
|
||||
self.controller = controller
|
||||
|
||||
def remove_controller(self, controller):
|
||||
if self.controller != controller:
|
||||
raise InvalidStateError('controller mismatch')
|
||||
self.controller = None
|
||||
|
||||
def get_pending_connection(self):
|
||||
return self.pending_connection
|
||||
|
||||
def get_pending_classic_connection(self):
|
||||
return self.pending_classic_connection
|
||||
|
||||
async def wait_until_connected(self):
|
||||
await self.websocket
|
||||
|
||||
def execute(self, async_function):
|
||||
self.execution_queue.put_nowait(async_function())
|
||||
|
||||
async def run_executor_loop(self):
|
||||
logger.debug('executor loop starting')
|
||||
while True:
|
||||
item = await self.execution_queue.get()
|
||||
try:
|
||||
await item
|
||||
except Exception as error:
|
||||
logger.warning(
|
||||
f'{color("!!! Exception in async handler:", "red")} {error}'
|
||||
)
|
||||
|
||||
async def run_connection(self):
|
||||
import websockets # lazy import
|
||||
|
||||
# Connect to the relay
|
||||
logger.debug(f'connecting to {self.uri}')
|
||||
# pylint: disable-next=no-member
|
||||
websocket = await websockets.connect(self.uri)
|
||||
self.websocket.set_result(websocket)
|
||||
logger.debug(f'connected to {self.uri}')
|
||||
|
||||
while True:
|
||||
message = await websocket.recv()
|
||||
logger.debug(f'received message: {message}')
|
||||
keyword, *payload = message.split(':', 1)
|
||||
|
||||
handler_name = f'on_{keyword}_received'
|
||||
handler = getattr(self, handler_name, None)
|
||||
if handler:
|
||||
await handler(payload[0] if payload else None)
|
||||
|
||||
def close(self):
|
||||
if self.websocket.done():
|
||||
logger.debug('closing websocket')
|
||||
websocket = self.websocket.result()
|
||||
asyncio.create_task(websocket.close())
|
||||
|
||||
async def on_result_received(self, result):
|
||||
if self.rpc_result:
|
||||
self.rpc_result.set_result(result)
|
||||
|
||||
async def on_left_received(self, address):
|
||||
if address in self.central_connections:
|
||||
self.controller.on_link_peripheral_disconnected(Address(address))
|
||||
self.central_connections.remove(address)
|
||||
|
||||
if address in self.peripheral_connections:
|
||||
self.controller.on_link_central_disconnected(
|
||||
address, HCI_CONNECTION_TIMEOUT_ERROR
|
||||
)
|
||||
self.peripheral_connections.remove(address)
|
||||
|
||||
async def on_unreachable_received(self, target):
|
||||
await self.on_left_received(target)
|
||||
|
||||
async def on_message_received(self, message):
|
||||
sender, *payload = message.split('/', 1)
|
||||
if payload:
|
||||
keyword, *payload = payload[0].split(':', 1)
|
||||
handler_name = f'on_{keyword}_message_received'
|
||||
handler = getattr(self, handler_name, None)
|
||||
if handler:
|
||||
await handler(sender, payload[0] if payload else None)
|
||||
|
||||
async def on_advertisement_message_received(self, sender, advertisement):
|
||||
try:
|
||||
self.controller.on_link_advertising_data(
|
||||
Address(sender), bytes.fromhex(advertisement)
|
||||
)
|
||||
except Exception:
|
||||
logger.exception('exception')
|
||||
|
||||
async def on_acl_message_received(self, sender, acl_data):
|
||||
try:
|
||||
self.controller.on_link_acl_data(Address(sender), bytes.fromhex(acl_data))
|
||||
except Exception:
|
||||
logger.exception('exception')
|
||||
|
||||
async def on_connect_message_received(self, sender, _):
|
||||
# Remember the connection
|
||||
self.peripheral_connections.add(sender)
|
||||
|
||||
# Notify the controller
|
||||
logger.debug(f'connection from central {sender}')
|
||||
self.controller.on_link_central_connected(Address(sender))
|
||||
|
||||
# Accept the connection by responding to it
|
||||
await self.send_targeted_message(sender, 'connected')
|
||||
|
||||
async def on_connected_message_received(self, sender, _):
|
||||
if not self.pending_connection:
|
||||
logger.warning('received a connection ack, but no connection is pending')
|
||||
return
|
||||
|
||||
# Remember the connection
|
||||
self.central_connections.add(sender)
|
||||
|
||||
# Notify the controller
|
||||
logger.debug(f'connected to peripheral {self.pending_connection.peer_address}')
|
||||
self.controller.on_link_peripheral_connection_complete(
|
||||
self.pending_connection, HCI_SUCCESS
|
||||
)
|
||||
|
||||
async def on_disconnect_message_received(self, sender, message):
|
||||
# Notify the controller
|
||||
params = parse_parameters(message)
|
||||
reason = int(params.get('reason', str(HCI_CONNECTION_TIMEOUT_ERROR)))
|
||||
self.controller.on_link_central_disconnected(Address(sender), reason)
|
||||
|
||||
# Forget the connection
|
||||
if sender in self.peripheral_connections:
|
||||
self.peripheral_connections.remove(sender)
|
||||
|
||||
async def on_encrypted_message_received(self, sender, _):
|
||||
# TODO parse params to get real args
|
||||
self.controller.on_link_encrypted(Address(sender), bytes(8), 0, bytes(16))
|
||||
|
||||
async def send_rpc_command(self, command):
|
||||
# Ensure we have a connection
|
||||
websocket = await self.websocket
|
||||
|
||||
# Create a future value to hold the eventual result
|
||||
assert self.rpc_result is None
|
||||
self.rpc_result = asyncio.get_running_loop().create_future()
|
||||
|
||||
# Send the command
|
||||
await websocket.send(command)
|
||||
|
||||
# Wait for the result
|
||||
rpc_result = await self.rpc_result
|
||||
self.rpc_result = None
|
||||
logger.debug(f'rpc_result: {rpc_result}')
|
||||
|
||||
# TODO: parse the result
|
||||
|
||||
async def send_targeted_message(self, target, message):
|
||||
# Ensure we have a connection
|
||||
websocket = await self.websocket
|
||||
|
||||
# Send the message
|
||||
await websocket.send(f'@{target} {message}')
|
||||
|
||||
async def notify_address_changed(self):
|
||||
await self.send_rpc_command(f'/set-address {self.controller.random_address}')
|
||||
|
||||
def on_address_changed(self, controller):
|
||||
logger.info(f'address changed for {controller}: {controller.random_address}')
|
||||
|
||||
# Notify the relay of the change
|
||||
self.execute(self.notify_address_changed)
|
||||
|
||||
async def send_advertising_data_to_relay(self, data):
|
||||
await self.send_targeted_message('*', f'advertisement:{data.hex()}')
|
||||
|
||||
def send_advertising_data(self, _, data):
|
||||
self.execute(partial(self.send_advertising_data_to_relay, data))
|
||||
|
||||
async def send_acl_data_to_relay(self, peer_address, data):
|
||||
await self.send_targeted_message(peer_address, f'acl:{data.hex()}')
|
||||
|
||||
def send_acl_data(self, _, peer_address, _transport, data):
|
||||
# TODO: handle different transport
|
||||
self.execute(partial(self.send_acl_data_to_relay, peer_address, data))
|
||||
|
||||
async def send_connection_request_to_relay(self, peer_address):
|
||||
await self.send_targeted_message(peer_address, 'connect')
|
||||
|
||||
def connect(self, _, le_create_connection_command):
|
||||
if self.pending_connection:
|
||||
logger.warning('connection already pending')
|
||||
return
|
||||
self.pending_connection = le_create_connection_command
|
||||
self.execute(
|
||||
partial(
|
||||
self.send_connection_request_to_relay,
|
||||
str(le_create_connection_command.peer_address),
|
||||
)
|
||||
)
|
||||
|
||||
def on_disconnection_complete(self, disconnect_command):
|
||||
self.controller.on_link_peripheral_disconnection_complete(
|
||||
disconnect_command, HCI_SUCCESS
|
||||
)
|
||||
|
||||
def disconnect(self, central_address, peripheral_address, disconnect_command):
|
||||
logger.debug(
|
||||
f'disconnect {central_address} -> '
|
||||
f'{peripheral_address}: reason = {disconnect_command.reason}'
|
||||
)
|
||||
self.execute(
|
||||
partial(
|
||||
self.send_targeted_message,
|
||||
peripheral_address,
|
||||
f'disconnect:reason={disconnect_command.reason}',
|
||||
)
|
||||
)
|
||||
asyncio.get_running_loop().call_soon(
|
||||
self.on_disconnection_complete, disconnect_command
|
||||
)
|
||||
|
||||
def on_connection_encrypted(self, _, peripheral_address, rand, ediv, ltk):
|
||||
asyncio.get_running_loop().call_soon(
|
||||
self.controller.on_link_encrypted, peripheral_address, rand, ediv, ltk
|
||||
)
|
||||
self.execute(
|
||||
partial(
|
||||
self.send_targeted_message,
|
||||
peripheral_address,
|
||||
f'encrypted:ltk={ltk.hex()}',
|
||||
)
|
||||
)
|
||||
|
||||
200
bumble/ll.py
200
bumble/ll.py
@@ -1,200 +0,0 @@
|
||||
# Copyright 2021-2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
from typing import ClassVar
|
||||
|
||||
from bumble import hci
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Advertising PDU
|
||||
# -----------------------------------------------------------------------------
|
||||
class AdvertisingPdu:
|
||||
"""Base Advertising Physical Channel PDU class.
|
||||
|
||||
See Core Spec 6.0, Volume 6, Part B, 2.3. Advertising physical channel PDU.
|
||||
|
||||
Currently these messages don't really follow the LL spec, because LL protocol is
|
||||
context-aware and we don't have real physical transport.
|
||||
"""
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ConnectInd(AdvertisingPdu):
|
||||
initiator_address: hci.Address
|
||||
advertiser_address: hci.Address
|
||||
interval: int
|
||||
latency: int
|
||||
timeout: int
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class AdvInd(AdvertisingPdu):
|
||||
advertiser_address: hci.Address
|
||||
data: bytes
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class AdvDirectInd(AdvertisingPdu):
|
||||
advertiser_address: hci.Address
|
||||
target_address: hci.Address
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class AdvNonConnInd(AdvertisingPdu):
|
||||
advertiser_address: hci.Address
|
||||
data: bytes
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class AdvExtInd(AdvertisingPdu):
|
||||
advertiser_address: hci.Address
|
||||
data: bytes
|
||||
|
||||
target_address: hci.Address | None = None
|
||||
adi: int | None = None
|
||||
tx_power: int | None = None
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# LL Control PDU
|
||||
# -----------------------------------------------------------------------------
|
||||
class ControlPdu:
|
||||
"""Base LL Control PDU Class.
|
||||
|
||||
See Core Spec 6.0, Volume 6, Part B, 2.4.2. LL Control PDU.
|
||||
|
||||
Currently these messages don't really follow the LL spec, because LL protocol is
|
||||
context-aware and we don't have real physical transport.
|
||||
"""
|
||||
|
||||
class Opcode(hci.SpecableEnum):
|
||||
LL_CONNECTION_UPDATE_IND = 0x00
|
||||
LL_CHANNEL_MAP_IND = 0x01
|
||||
LL_TERMINATE_IND = 0x02
|
||||
LL_ENC_REQ = 0x03
|
||||
LL_ENC_RSP = 0x04
|
||||
LL_START_ENC_REQ = 0x05
|
||||
LL_START_ENC_RSP = 0x06
|
||||
LL_UNKNOWN_RSP = 0x07
|
||||
LL_FEATURE_REQ = 0x08
|
||||
LL_FEATURE_RSP = 0x09
|
||||
LL_PAUSE_ENC_REQ = 0x0A
|
||||
LL_PAUSE_ENC_RSP = 0x0B
|
||||
LL_VERSION_IND = 0x0C
|
||||
LL_REJECT_IND = 0x0D
|
||||
LL_PERIPHERAL_FEATURE_REQ = 0x0E
|
||||
LL_CONNECTION_PARAM_REQ = 0x0F
|
||||
LL_CONNECTION_PARAM_RSP = 0x10
|
||||
LL_REJECT_EXT_IND = 0x11
|
||||
LL_PING_REQ = 0x12
|
||||
LL_PING_RSP = 0x13
|
||||
LL_LENGTH_REQ = 0x14
|
||||
LL_LENGTH_RSP = 0x15
|
||||
LL_PHY_REQ = 0x16
|
||||
LL_PHY_RSP = 0x17
|
||||
LL_PHY_UPDATE_IND = 0x18
|
||||
LL_MIN_USED_CHANNELS_IND = 0x19
|
||||
LL_CTE_REQ = 0x1A
|
||||
LL_CTE_RSP = 0x1B
|
||||
LL_PERIODIC_SYNC_IND = 0x1C
|
||||
LL_CLOCK_ACCURACY_REQ = 0x1D
|
||||
LL_CLOCK_ACCURACY_RSP = 0x1E
|
||||
LL_CIS_REQ = 0x1F
|
||||
LL_CIS_RSP = 0x20
|
||||
LL_CIS_IND = 0x21
|
||||
LL_CIS_TERMINATE_IND = 0x22
|
||||
LL_POWER_CONTROL_REQ = 0x23
|
||||
LL_POWER_CONTROL_RSP = 0x24
|
||||
LL_POWER_CHANGE_IND = 0x25
|
||||
LL_SUBRATE_REQ = 0x26
|
||||
LL_SUBRATE_IND = 0x27
|
||||
LL_CHANNEL_REPORTING_IND = 0x28
|
||||
LL_CHANNEL_STATUS_IND = 0x29
|
||||
LL_PERIODIC_SYNC_WR_IND = 0x2A
|
||||
LL_FEATURE_EXT_REQ = 0x2B
|
||||
LL_FEATURE_EXT_RSP = 0x2C
|
||||
LL_CS_SEC_RSP = 0x2D
|
||||
LL_CS_CAPABILITIES_REQ = 0x2E
|
||||
LL_CS_CAPABILITIES_RSP = 0x2F
|
||||
LL_CS_CONFIG_REQ = 0x30
|
||||
LL_CS_CONFIG_RSP = 0x31
|
||||
LL_CS_REQ = 0x32
|
||||
LL_CS_RSP = 0x33
|
||||
LL_CS_IND = 0x34
|
||||
LL_CS_TERMINATE_REQ = 0x35
|
||||
LL_CS_FAE_REQ = 0x36
|
||||
LL_CS_FAE_RSP = 0x37
|
||||
LL_CS_CHANNEL_MAP_IND = 0x38
|
||||
LL_CS_SEC_REQ = 0x39
|
||||
LL_CS_TERMINATE_RSP = 0x3A
|
||||
LL_FRAME_SPACE_REQ = 0x3B
|
||||
LL_FRAME_SPACE_RSP = 0x3C
|
||||
|
||||
opcode: ClassVar[Opcode]
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class TerminateInd(ControlPdu):
|
||||
opcode = ControlPdu.Opcode.LL_TERMINATE_IND
|
||||
|
||||
error_code: int
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class EncReq(ControlPdu):
|
||||
opcode = ControlPdu.Opcode.LL_ENC_REQ
|
||||
|
||||
rand: bytes
|
||||
ediv: int
|
||||
ltk: bytes
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class CisReq(ControlPdu):
|
||||
opcode = ControlPdu.Opcode.LL_CIS_REQ
|
||||
|
||||
cig_id: int
|
||||
cis_id: int
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class CisRsp(ControlPdu):
|
||||
opcode = ControlPdu.Opcode.LL_CIS_REQ
|
||||
|
||||
cig_id: int
|
||||
cis_id: int
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class CisInd(ControlPdu):
|
||||
opcode = ControlPdu.Opcode.LL_CIS_REQ
|
||||
|
||||
cig_id: int
|
||||
cis_id: int
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class CisTerminateInd(ControlPdu):
|
||||
opcode = ControlPdu.Opcode.LL_CIS_TERMINATE_IND
|
||||
|
||||
cig_id: int
|
||||
cis_id: int
|
||||
error_code: int
|
||||
324
bumble/lmp.py
324
bumble/lmp.py
@@ -1,324 +0,0 @@
|
||||
# Copyright 2021-2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TypeVar
|
||||
|
||||
from bumble import hci, utils
|
||||
|
||||
|
||||
class Opcode(utils.OpenIntEnum):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 2, Part C - 5.1 PDU summary.
|
||||
|
||||
Follow the alphabetical order defined there.
|
||||
'''
|
||||
|
||||
# fmt: off
|
||||
LMP_ACCEPTED = 3
|
||||
LMP_ACCEPTED_EXT = 127 << 8 + 1
|
||||
LMP_AU_RAND = 11
|
||||
LMP_AUTO_RATE = 35
|
||||
LMP_CHANNEL_CLASSIFICATION = 127 << 8 + 17
|
||||
LMP_CHANNEL_CLASSIFICATION_REQ = 127 << 8 + 16
|
||||
LMP_CLK_ADJ = 127 << 8 + 5
|
||||
LMP_CLK_ADJ_ACK = 127 << 8 + 6
|
||||
LMP_CLK_ADJ_REQ = 127 << 8 + 7
|
||||
LMP_CLKOFFSET_REQ = 5
|
||||
LMP_CLKOFFSET_RES = 6
|
||||
LMP_COMB_KEY = 9
|
||||
LMP_DECR_POWER_REQ = 32
|
||||
LMP_DETACH = 7
|
||||
LMP_DHKEY_CHECK = 65
|
||||
LMP_ENCAPSULATED_HEADER = 61
|
||||
LMP_ENCAPSULATED_PAYLOAD = 62
|
||||
LMP_ENCRYPTION_KEY_SIZE_MASK_REQ= 58
|
||||
LMP_ENCRYPTION_KEY_SIZE_MASK_RES= 59
|
||||
LMP_ENCRYPTION_KEY_SIZE_REQ = 16
|
||||
LMP_ENCRYPTION_MODE_REQ = 15
|
||||
LMP_ESCO_LINK_REQ = 127 << 8 + 12
|
||||
LMP_FEATURES_REQ = 39
|
||||
LMP_FEATURES_REQ_EXT = 127 << 8 + 3
|
||||
LMP_FEATURES_RES = 40
|
||||
LMP_FEATURES_RES_EXT = 127 << 8 + 4
|
||||
LMP_HOLD = 20
|
||||
LMP_HOLD_REQ = 21
|
||||
LMP_HOST_CONNECTION_REQ = 51
|
||||
LMP_IN_RAND = 8
|
||||
LMP_INCR_POWER_REQ = 31
|
||||
LMP_IO_CAPABILITY_REQ = 127 << 8 + 25
|
||||
LMP_IO_CAPABILITY_RES = 127 << 8 + 26
|
||||
LMP_KEYPRESS_NOTIFICATION = 127 << 8 + 30
|
||||
LMP_MAX_POWER = 33
|
||||
LMP_MAX_SLOT = 45
|
||||
LMP_MAX_SLOT_REQ = 46
|
||||
LMP_MIN_POWER = 34
|
||||
LMP_NAME_REQ = 1
|
||||
LMP_NAME_RES = 2
|
||||
LMP_NOT_ACCEPTED = 4
|
||||
LMP_NOT_ACCEPTED_EXT = 127 << 8 + 2
|
||||
LMP_NUMERIC_COMPARISON_FAILED = 127 << 8 + 27
|
||||
LMP_OOB_FAILED = 127 << 8 + 29
|
||||
LMP_PACKET_TYPE_TABLE_REQ = 127 << 8 + 11
|
||||
LMP_PAGE_MODE_REQ = 53
|
||||
LMP_PAGE_SCAN_MODE_REQ = 54
|
||||
LMP_PASSKEY_FAILED = 127 << 8 + 28
|
||||
LMP_PAUSE_ENCRYPTION_AES_REQ = 66
|
||||
LMP_PAUSE_ENCRYPTION_REQ = 127 << 8 + 23
|
||||
LMP_PING_REQ = 127 << 8 + 33
|
||||
LMP_PING_RES = 127 << 8 + 34
|
||||
LMP_POWER_CONTROL_REQ = 127 << 8 + 31
|
||||
LMP_POWER_CONTROL_RES = 127 << 8 + 32
|
||||
LMP_PREFERRED_RATE = 36
|
||||
LMP_QUALITY_OF_SERVICE = 41
|
||||
LMP_QUALITY_OF_SERVICE_REQ = 42
|
||||
LMP_REMOVE_ESCO_LINK_REQ = 127 << 8 + 13
|
||||
LMP_REMOVE_SCO_LINK_REQ = 44
|
||||
LMP_RESUME_ENCRYPTION_REQ = 127 << 8 + 24
|
||||
LMP_SAM_DEFINE_MAP = 127 << 8 + 36
|
||||
LMP_SAM_SET_TYPE0 = 127 << 8 + 35
|
||||
LMP_SAM_SWITCH = 127 << 8 + 37
|
||||
LMP_SCO_LINK_REQ = 43
|
||||
LMP_SET_AFH = 60
|
||||
LMP_SETUP_COMPLETE = 49
|
||||
LMP_SIMPLE_PAIRING_CONFIRM = 63
|
||||
LMP_SIMPLE_PAIRING_NUMBER = 64
|
||||
LMP_SLOT_OFFSET = 52
|
||||
LMP_SNIFF_REQ = 23
|
||||
LMP_SNIFF_SUBRATING_REQ = 127 << 8 + 21
|
||||
LMP_SNIFF_SUBRATING_RES = 127 << 8 + 22
|
||||
LMP_SRES = 12
|
||||
LMP_START_ENCRYPTION_REQ = 17
|
||||
LMP_STOP_ENCRYPTION_REQ = 18
|
||||
LMP_SUPERVISION_TIMEOUT = 55
|
||||
LMP_SWITCH_REQ = 19
|
||||
LMP_TEMP_KEY = 14
|
||||
LMP_TEMP_RAND = 13
|
||||
LMP_TEST_ACTIVATE = 56
|
||||
LMP_TEST_CONTROL = 57
|
||||
LMP_TIMING_ACCURACY_REQ = 47
|
||||
LMP_TIMING_ACCURACY_RES = 48
|
||||
LMP_UNIT_KEY = 10
|
||||
LMP_UNSNIFF_REQ = 24
|
||||
LMP_USE_SEMI_PERMANENT_KEY = 50
|
||||
LMP_VERSION_REQ = 37
|
||||
LMP_VERSION_RES = 38
|
||||
# fmt: on
|
||||
|
||||
@classmethod
|
||||
def parse_from(cls, data: bytes, offset: int = 0) -> tuple[int, Opcode]:
|
||||
opcode = data[offset]
|
||||
if opcode in (124, 127):
|
||||
opcode = struct.unpack('>H', data)[0]
|
||||
return offset + 2, Opcode(opcode)
|
||||
return offset + 1, Opcode(opcode)
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
if self.value >> 8:
|
||||
return struct.pack('>H', self.value)
|
||||
return bytes([self.value])
|
||||
|
||||
@classmethod
|
||||
def type_metadata(cls):
|
||||
return hci.metadata(
|
||||
{
|
||||
'serializer': bytes,
|
||||
'parser': lambda data, offset: (Opcode.parse_from(data, offset)),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class Packet:
|
||||
'''
|
||||
See Bluetooth spec @ Vol 2, Part C - 5.1 PDU summary
|
||||
'''
|
||||
|
||||
subclasses: dict[int, type[Packet]] = {}
|
||||
opcode: Opcode
|
||||
fields: hci.Fields = ()
|
||||
_payload: bytes = b''
|
||||
|
||||
_Packet = TypeVar("_Packet", bound="Packet")
|
||||
|
||||
@classmethod
|
||||
def subclass(cls, subclass: type[_Packet]) -> type[_Packet]:
|
||||
# Register a factory for this class
|
||||
cls.subclasses[subclass.opcode] = subclass
|
||||
subclass.fields = hci.HCI_Object.fields_from_dataclass(subclass)
|
||||
|
||||
return subclass
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data: bytes) -> Packet:
|
||||
offset, opcode = Opcode.parse_from(data)
|
||||
if not (subclass := cls.subclasses.get(opcode)):
|
||||
instance = Packet()
|
||||
instance.opcode = opcode
|
||||
else:
|
||||
instance = subclass(
|
||||
**hci.HCI_Object.dict_from_bytes(data, offset, subclass.fields)
|
||||
)
|
||||
instance.payload = data[offset:]
|
||||
return instance
|
||||
|
||||
@property
|
||||
def payload(self) -> bytes:
|
||||
if self._payload is None:
|
||||
self._payload = hci.HCI_Object.dict_to_bytes(self.__dict__, self.fields)
|
||||
return self._payload
|
||||
|
||||
@payload.setter
|
||||
def payload(self, value: bytes) -> None:
|
||||
self._payload = value
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
return bytes(self.opcode) + self.payload
|
||||
|
||||
|
||||
@Packet.subclass
|
||||
@dataclass
|
||||
class LmpAccepted(Packet):
|
||||
opcode = Opcode.LMP_ACCEPTED
|
||||
|
||||
response_opcode: Opcode = field(metadata=Opcode.type_metadata())
|
||||
|
||||
|
||||
@Packet.subclass
|
||||
@dataclass
|
||||
class LmpNotAccepted(Packet):
|
||||
opcode = Opcode.LMP_NOT_ACCEPTED
|
||||
|
||||
response_opcode: Opcode = field(metadata=Opcode.type_metadata())
|
||||
error_code: int = field(metadata=hci.metadata(1))
|
||||
|
||||
|
||||
@Packet.subclass
|
||||
@dataclass
|
||||
class LmpAcceptedExt(Packet):
|
||||
opcode = Opcode.LMP_ACCEPTED_EXT
|
||||
|
||||
response_opcode: Opcode = field(metadata=Opcode.type_metadata())
|
||||
|
||||
|
||||
@Packet.subclass
|
||||
@dataclass
|
||||
class LmpNotAcceptedExt(Packet):
|
||||
opcode = Opcode.LMP_NOT_ACCEPTED_EXT
|
||||
|
||||
response_opcode: Opcode = field(metadata=Opcode.type_metadata())
|
||||
error_code: int = field(metadata=hci.metadata(1))
|
||||
|
||||
|
||||
@Packet.subclass
|
||||
@dataclass
|
||||
class LmpAuRand(Packet):
|
||||
opcode = Opcode.LMP_AU_RAND
|
||||
|
||||
random_number: bytes = field(metadata=hci.metadata(16))
|
||||
|
||||
|
||||
@Packet.subclass
|
||||
@dataclass
|
||||
class LmpDetach(Packet):
|
||||
opcode = Opcode.LMP_DETACH
|
||||
|
||||
error_code: int = field(metadata=hci.metadata(1))
|
||||
|
||||
|
||||
@Packet.subclass
|
||||
@dataclass
|
||||
class LmpEscoLinkReq(Packet):
|
||||
opcode = Opcode.LMP_ESCO_LINK_REQ
|
||||
|
||||
esco_handle: int = field(metadata=hci.metadata(1))
|
||||
esco_lt_addr: int = field(metadata=hci.metadata(1))
|
||||
timing_control_flags: int = field(metadata=hci.metadata(1))
|
||||
d_esco: int = field(metadata=hci.metadata(1))
|
||||
t_esco: int = field(metadata=hci.metadata(1))
|
||||
w_esco: int = field(metadata=hci.metadata(1))
|
||||
esco_packet_type_c_to_p: int = field(metadata=hci.metadata(1))
|
||||
esco_packet_type_p_to_c: int = field(metadata=hci.metadata(1))
|
||||
packet_length_c_to_p: int = field(metadata=hci.metadata(2))
|
||||
packet_length_p_to_c: int = field(metadata=hci.metadata(2))
|
||||
air_mode: int = field(metadata=hci.metadata(1))
|
||||
negotiation_state: int = field(metadata=hci.metadata(1))
|
||||
|
||||
|
||||
@Packet.subclass
|
||||
@dataclass
|
||||
class LmpHostConnectionReq(Packet):
|
||||
opcode = Opcode.LMP_HOST_CONNECTION_REQ
|
||||
|
||||
|
||||
@Packet.subclass
|
||||
@dataclass
|
||||
class LmpRemoveEscoLinkReq(Packet):
|
||||
opcode = Opcode.LMP_REMOVE_ESCO_LINK_REQ
|
||||
|
||||
esco_handle: int = field(metadata=hci.metadata(1))
|
||||
error_code: int = field(metadata=hci.metadata(1))
|
||||
|
||||
|
||||
@Packet.subclass
|
||||
@dataclass
|
||||
class LmpRemoveScoLinkReq(Packet):
|
||||
opcode = Opcode.LMP_REMOVE_SCO_LINK_REQ
|
||||
|
||||
sco_handle: int = field(metadata=hci.metadata(1))
|
||||
error_code: int = field(metadata=hci.metadata(1))
|
||||
|
||||
|
||||
@Packet.subclass
|
||||
@dataclass
|
||||
class LmpScoLinkReq(Packet):
|
||||
opcode = Opcode.LMP_SCO_LINK_REQ
|
||||
|
||||
sco_handle: int = field(metadata=hci.metadata(1))
|
||||
timing_control_flags: int = field(metadata=hci.metadata(1))
|
||||
d_sco: int = field(metadata=hci.metadata(1))
|
||||
t_sco: int = field(metadata=hci.metadata(1))
|
||||
sco_packet: int = field(metadata=hci.metadata(1))
|
||||
air_mode: int = field(metadata=hci.metadata(1))
|
||||
|
||||
|
||||
@Packet.subclass
|
||||
@dataclass
|
||||
class LmpSwitchReq(Packet):
|
||||
opcode = Opcode.LMP_SWITCH_REQ
|
||||
|
||||
switch_instant: int = field(metadata=hci.metadata(4), default=0)
|
||||
|
||||
|
||||
@Packet.subclass
|
||||
@dataclass
|
||||
class LmpNameReq(Packet):
|
||||
opcode = Opcode.LMP_NAME_REQ
|
||||
|
||||
name_offset: int = field(metadata=hci.metadata(2))
|
||||
|
||||
|
||||
@Packet.subclass
|
||||
@dataclass
|
||||
class LmpNameRes(Packet):
|
||||
opcode = Opcode.LMP_NAME_RES
|
||||
|
||||
name_offset: int = field(metadata=hci.metadata(2))
|
||||
name_length: int = field(metadata=hci.metadata(3))
|
||||
name_fregment: bytes = field(metadata=hci.metadata('*'))
|
||||
@@ -1,65 +0,0 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import functools
|
||||
import logging
|
||||
import os
|
||||
|
||||
from bumble import colors
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class ColorFormatter(logging.Formatter):
|
||||
_colorizers = {
|
||||
logging.DEBUG: functools.partial(colors.color, fg="white"),
|
||||
logging.INFO: functools.partial(colors.color, fg="green"),
|
||||
logging.WARNING: functools.partial(colors.color, fg="yellow"),
|
||||
logging.ERROR: functools.partial(colors.color, fg="red"),
|
||||
logging.CRITICAL: functools.partial(colors.color, fg="black", bg="red"),
|
||||
}
|
||||
|
||||
_formatters = {
|
||||
level: logging.Formatter(
|
||||
fmt=colorizer("{asctime}.{msecs:03.0f} {levelname:.1} {name}: ")
|
||||
+ "{message}",
|
||||
datefmt="%H:%M:%S",
|
||||
style="{",
|
||||
)
|
||||
for level, colorizer in _colorizers.items()
|
||||
}
|
||||
|
||||
def format(self, record: logging.LogRecord) -> str:
|
||||
return self._formatters[record.levelno].format(record)
|
||||
|
||||
|
||||
def setup_basic_logging(default_level: str = "INFO") -> None:
|
||||
"""
|
||||
Set up basic logging with logging.basicConfig, configured with a simple formatter
|
||||
that prints out the date and log level in color.
|
||||
If the BUMBLE_LOGLEVEL environment variable is set to the name of a log level, it
|
||||
is used. Otherwise the default_level argument is used.
|
||||
|
||||
Args:
|
||||
default_level: default logging level
|
||||
|
||||
"""
|
||||
handler = logging.StreamHandler()
|
||||
handler.setFormatter(ColorFormatter())
|
||||
logging.basicConfig(
|
||||
level=os.environ.get("BUMBLE_LOGLEVEL", default_level).upper(),
|
||||
handlers=[handler],
|
||||
)
|
||||
@@ -16,27 +16,32 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
import secrets
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from bumble import hci
|
||||
from bumble.core import AdvertisingData, LeRole
|
||||
from bumble.hci import (
|
||||
Address,
|
||||
HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
|
||||
HCI_DISPLAY_ONLY_IO_CAPABILITY,
|
||||
HCI_DISPLAY_YES_NO_IO_CAPABILITY,
|
||||
HCI_KEYBOARD_ONLY_IO_CAPABILITY,
|
||||
)
|
||||
from bumble.smp import (
|
||||
SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
|
||||
SMP_KEYBOARD_ONLY_IO_CAPABILITY,
|
||||
SMP_DISPLAY_ONLY_IO_CAPABILITY,
|
||||
SMP_DISPLAY_YES_NO_IO_CAPABILITY,
|
||||
SMP_KEYBOARD_DISPLAY_IO_CAPABILITY,
|
||||
SMP_ENC_KEY_DISTRIBUTION_FLAG,
|
||||
SMP_ID_KEY_DISTRIBUTION_FLAG,
|
||||
SMP_KEYBOARD_DISPLAY_IO_CAPABILITY,
|
||||
SMP_KEYBOARD_ONLY_IO_CAPABILITY,
|
||||
SMP_LINK_KEY_DISTRIBUTION_FLAG,
|
||||
SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
|
||||
SMP_SIGN_KEY_DISTRIBUTION_FLAG,
|
||||
SMP_LINK_KEY_DISTRIBUTION_FLAG,
|
||||
OobContext,
|
||||
OobLegacyContext,
|
||||
OobSharedData,
|
||||
)
|
||||
from bumble.core import AdvertisingData, LeRole
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -44,19 +49,19 @@ from bumble.smp import (
|
||||
class OobData:
|
||||
"""OOB data that can be sent from one device to another."""
|
||||
|
||||
address: hci.Address | None = None
|
||||
role: LeRole | None = None
|
||||
shared_data: OobSharedData | None = None
|
||||
legacy_context: OobLegacyContext | None = None
|
||||
address: Optional[Address] = None
|
||||
role: Optional[LeRole] = None
|
||||
shared_data: Optional[OobSharedData] = None
|
||||
legacy_context: Optional[OobLegacyContext] = None
|
||||
|
||||
@classmethod
|
||||
def from_ad(cls, ad: AdvertisingData) -> OobData:
|
||||
instance = cls()
|
||||
shared_data_c: bytes | None = None
|
||||
shared_data_r: bytes | None = None
|
||||
shared_data_c: Optional[bytes] = None
|
||||
shared_data_r: Optional[bytes] = None
|
||||
for ad_type, ad_data in ad.ad_structures:
|
||||
if ad_type == AdvertisingData.LE_BLUETOOTH_DEVICE_ADDRESS:
|
||||
instance.address = hci.Address(ad_data)
|
||||
instance.address = Address(ad_data)
|
||||
elif ad_type == AdvertisingData.LE_ROLE:
|
||||
instance.role = LeRole(ad_data[0])
|
||||
elif ad_type == AdvertisingData.LE_SECURE_CONNECTIONS_CONFIRMATION_VALUE:
|
||||
@@ -124,11 +129,11 @@ class PairingDelegate:
|
||||
# Default mapping from abstract to Classic I/O capabilities.
|
||||
# Subclasses may override this if they prefer a different mapping.
|
||||
CLASSIC_IO_CAPABILITIES_MAP = {
|
||||
NO_OUTPUT_NO_INPUT: hci.IoCapability.NO_INPUT_NO_OUTPUT,
|
||||
KEYBOARD_INPUT_ONLY: hci.IoCapability.KEYBOARD_ONLY,
|
||||
DISPLAY_OUTPUT_ONLY: hci.IoCapability.DISPLAY_ONLY,
|
||||
DISPLAY_OUTPUT_AND_YES_NO_INPUT: hci.IoCapability.DISPLAY_YES_NO,
|
||||
DISPLAY_OUTPUT_AND_KEYBOARD_INPUT: hci.IoCapability.DISPLAY_YES_NO,
|
||||
NO_OUTPUT_NO_INPUT: HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
|
||||
KEYBOARD_INPUT_ONLY: HCI_KEYBOARD_ONLY_IO_CAPABILITY,
|
||||
DISPLAY_OUTPUT_ONLY: HCI_DISPLAY_ONLY_IO_CAPABILITY,
|
||||
DISPLAY_OUTPUT_AND_YES_NO_INPUT: HCI_DISPLAY_YES_NO_IO_CAPABILITY,
|
||||
DISPLAY_OUTPUT_AND_KEYBOARD_INPUT: HCI_DISPLAY_YES_NO_IO_CAPABILITY,
|
||||
}
|
||||
|
||||
io_capability: IoCapability
|
||||
@@ -154,7 +159,7 @@ class PairingDelegate:
|
||||
|
||||
# pylint: disable=line-too-long
|
||||
return self.CLASSIC_IO_CAPABILITIES_MAP.get(
|
||||
self.io_capability, hci.IoCapability.NO_INPUT_NO_OUTPUT
|
||||
self.io_capability, HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -180,14 +185,14 @@ class PairingDelegate:
|
||||
"""Compare two numbers."""
|
||||
return True
|
||||
|
||||
async def get_number(self) -> int | None:
|
||||
async def get_number(self) -> Optional[int]:
|
||||
"""
|
||||
Return an optional number as an answer to a passkey request.
|
||||
Returning `None` will result in a negative reply.
|
||||
"""
|
||||
return 0
|
||||
|
||||
async def get_string(self, max_length: int) -> str | None:
|
||||
async def get_string(self, max_length: int) -> Optional[str]:
|
||||
"""
|
||||
Return a string whose utf-8 encoding is up to max_length bytes.
|
||||
"""
|
||||
@@ -200,7 +205,7 @@ class PairingDelegate:
|
||||
# [LE only]
|
||||
async def key_distribution_response(
|
||||
self, peer_initiator_key_distribution: int, peer_responder_key_distribution: int
|
||||
) -> tuple[int, int]:
|
||||
) -> Tuple[int, int]:
|
||||
"""
|
||||
Return the key distribution response in an SMP protocol context.
|
||||
|
||||
@@ -217,39 +222,31 @@ class PairingDelegate:
|
||||
),
|
||||
)
|
||||
|
||||
async def generate_passkey(self) -> int:
|
||||
"""
|
||||
Return a passkey value between 0 and 999999 (inclusive).
|
||||
"""
|
||||
|
||||
# By default, generate a random passkey.
|
||||
return secrets.randbelow(1000000)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class PairingConfig:
|
||||
"""Configuration for the Pairing protocol."""
|
||||
|
||||
class AddressType(enum.IntEnum):
|
||||
PUBLIC = hci.Address.PUBLIC_DEVICE_ADDRESS
|
||||
RANDOM = hci.Address.RANDOM_DEVICE_ADDRESS
|
||||
PUBLIC = Address.PUBLIC_DEVICE_ADDRESS
|
||||
RANDOM = Address.RANDOM_DEVICE_ADDRESS
|
||||
|
||||
@dataclass
|
||||
class OobConfig:
|
||||
"""Config for OOB pairing."""
|
||||
|
||||
our_context: OobContext | None
|
||||
peer_data: OobSharedData | None
|
||||
legacy_context: OobLegacyContext | None
|
||||
our_context: Optional[OobContext]
|
||||
peer_data: Optional[OobSharedData]
|
||||
legacy_context: Optional[OobLegacyContext]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
sc: bool = True,
|
||||
mitm: bool = True,
|
||||
bonding: bool = True,
|
||||
delegate: PairingDelegate | None = None,
|
||||
identity_address_type: AddressType | None = None,
|
||||
oob: OobConfig | None = None,
|
||||
delegate: Optional[PairingDelegate] = None,
|
||||
identity_address_type: Optional[AddressType] = None,
|
||||
oob: Optional[OobConfig] = None,
|
||||
) -> None:
|
||||
self.sc = sc
|
||||
self.mitm = mitm
|
||||
|
||||
@@ -19,22 +19,21 @@ This module implement the Pandora Bluetooth test APIs for the Bumble stack.
|
||||
|
||||
__version__ = "0.0.1"
|
||||
|
||||
from collections.abc import Callable
|
||||
|
||||
import grpc
|
||||
import grpc.aio
|
||||
from pandora.host_grpc_aio import add_HostServicer_to_server
|
||||
from pandora.l2cap_grpc_aio import add_L2CAPServicer_to_server
|
||||
from pandora.security_grpc_aio import (
|
||||
add_SecurityServicer_to_server,
|
||||
add_SecurityStorageServicer_to_server,
|
||||
)
|
||||
|
||||
from bumble.pandora.config import Config
|
||||
from bumble.pandora.device import PandoraDevice
|
||||
from bumble.pandora.host import HostService
|
||||
from bumble.pandora.l2cap import L2CAPService
|
||||
from bumble.pandora.security import SecurityService, SecurityStorageService
|
||||
from pandora.host_grpc_aio import add_HostServicer_to_server
|
||||
from pandora.l2cap_grpc_aio import add_L2CAPServicer_to_server
|
||||
from pandora.security_grpc_aio import (
|
||||
add_SecurityServicer_to_server,
|
||||
add_SecurityStorageServicer_to_server,
|
||||
)
|
||||
from typing import Callable, List, Optional
|
||||
|
||||
# public symbols
|
||||
__all__ = [
|
||||
@@ -46,11 +45,11 @@ __all__ = [
|
||||
|
||||
|
||||
# Add servicers hooks.
|
||||
_SERVICERS_HOOKS: list[Callable[[PandoraDevice, Config, grpc.aio.Server], None]] = []
|
||||
_SERVICERS_HOOKS: List[Callable[[PandoraDevice, Config, grpc.aio.Server], None]] = []
|
||||
|
||||
|
||||
def register_servicer_hook(
|
||||
hook: Callable[[PandoraDevice, Config, grpc.aio.Server], None],
|
||||
hook: Callable[[PandoraDevice, Config, grpc.aio.Server], None]
|
||||
) -> None:
|
||||
_SERVICERS_HOOKS.append(hook)
|
||||
|
||||
@@ -58,7 +57,7 @@ def register_servicer_hook(
|
||||
async def serve(
|
||||
bumble: PandoraDevice,
|
||||
config: Config = Config(),
|
||||
grpc_server: grpc.aio.Server | None = None,
|
||||
grpc_server: Optional[grpc.aio.Server] = None,
|
||||
port: int = 0,
|
||||
) -> None:
|
||||
# initialize a gRPC server if not provided.
|
||||
|
||||
@@ -13,11 +13,9 @@
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from bumble.pairing import PairingConfig, PairingDelegate
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -34,7 +32,7 @@ class Config:
|
||||
PairingDelegate.DEFAULT_KEY_DISTRIBUTION
|
||||
)
|
||||
|
||||
def load_from_dict(self, config: dict[str, Any]) -> None:
|
||||
def load_from_dict(self, config: Dict[str, Any]) -> None:
|
||||
io_capability_name: str = config.get(
|
||||
'io_capability', 'no_output_no_input'
|
||||
).upper()
|
||||
|
||||
@@ -15,9 +15,6 @@
|
||||
"""Generic & dependency free Bumble (reference) device."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from bumble import transport
|
||||
from bumble.core import (
|
||||
BT_GENERIC_AUDIO_SERVICE,
|
||||
@@ -35,6 +32,8 @@ from bumble.sdp import (
|
||||
DataElement,
|
||||
ServiceAttribute,
|
||||
)
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
|
||||
# Default rootcanal HCI TCP address
|
||||
ROOTCANAL_HCI_ADDRESS = "localhost:6402"
|
||||
@@ -50,13 +49,13 @@ class PandoraDevice:
|
||||
|
||||
# Bumble device instance & configuration.
|
||||
device: Device
|
||||
config: dict[str, Any]
|
||||
config: Dict[str, Any]
|
||||
|
||||
# HCI transport name & instance.
|
||||
_hci_name: str
|
||||
_hci: transport.Transport | None # type: ignore[name-defined]
|
||||
_hci: Optional[transport.Transport] # type: ignore[name-defined]
|
||||
|
||||
def __init__(self, config: dict[str, Any]) -> None:
|
||||
def __init__(self, config: Dict[str, Any]) -> None:
|
||||
self.config = config
|
||||
self.device = _make_device(config)
|
||||
self._hci_name = config.get(
|
||||
@@ -74,9 +73,7 @@ class PandoraDevice:
|
||||
|
||||
# open HCI transport & set device host.
|
||||
self._hci = await transport.open_transport(self._hci_name)
|
||||
self.device.host = Host(
|
||||
controller_source=self._hci.source, controller_sink=self._hci.sink
|
||||
) # type: ignore[no-untyped-call]
|
||||
self.device.host = Host(controller_source=self._hci.source, controller_sink=self._hci.sink) # type: ignore[no-untyped-call]
|
||||
|
||||
# power-on.
|
||||
await self.device.power_on()
|
||||
@@ -98,14 +95,14 @@ class PandoraDevice:
|
||||
await self.close()
|
||||
await self.open()
|
||||
|
||||
def info(self) -> dict[str, str] | None:
|
||||
def info(self) -> Optional[Dict[str, str]]:
|
||||
return {
|
||||
'public_bd_address': str(self.device.public_address),
|
||||
'random_address': str(self.device.random_address),
|
||||
}
|
||||
|
||||
|
||||
def _make_device(config: dict[str, Any]) -> Device:
|
||||
def _make_device(config: Dict[str, Any]) -> Device:
|
||||
"""Initialize an idle Bumble device instance."""
|
||||
|
||||
# initialize bumble device.
|
||||
@@ -120,7 +117,7 @@ def _make_device(config: dict[str, Any]) -> Device:
|
||||
|
||||
|
||||
# TODO(b/267540823): remove when Pandora A2dp is supported
|
||||
def _make_sdp_records(rfcomm_channel: int) -> dict[int, list[ServiceAttribute]]:
|
||||
def _make_sdp_records(rfcomm_channel: int) -> Dict[int, List[ServiceAttribute]]:
|
||||
return {
|
||||
0x00010001: [
|
||||
ServiceAttribute(
|
||||
|
||||
@@ -13,26 +13,51 @@
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import struct
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import cast
|
||||
|
||||
import bumble.device
|
||||
import grpc
|
||||
import grpc.aio
|
||||
from google.protobuf import (
|
||||
any_pb2, # pytype: disable=pyi-error
|
||||
empty_pb2, # pytype: disable=pyi-error
|
||||
import logging
|
||||
import struct
|
||||
|
||||
import bumble.utils
|
||||
from bumble.pandora import utils
|
||||
from bumble.pandora.config import Config
|
||||
from bumble.core import (
|
||||
PhysicalTransport,
|
||||
UUID,
|
||||
AdvertisingData,
|
||||
Appearance,
|
||||
ConnectionError,
|
||||
)
|
||||
from pandora import host_pb2
|
||||
from bumble.device import (
|
||||
DEVICE_DEFAULT_SCAN_INTERVAL,
|
||||
DEVICE_DEFAULT_SCAN_WINDOW,
|
||||
Advertisement,
|
||||
AdvertisingParameters,
|
||||
AdvertisingEventProperties,
|
||||
AdvertisingType,
|
||||
Device,
|
||||
)
|
||||
from bumble.gatt import Service
|
||||
from bumble.hci import (
|
||||
HCI_CONNECTION_ALREADY_EXISTS_ERROR,
|
||||
HCI_PAGE_TIMEOUT_ERROR,
|
||||
HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR,
|
||||
Address,
|
||||
Phy,
|
||||
Role,
|
||||
OwnAddressType,
|
||||
)
|
||||
from google.protobuf import any_pb2 # pytype: disable=pyi-error
|
||||
from google.protobuf import empty_pb2 # pytype: disable=pyi-error
|
||||
from pandora.host_grpc_aio import HostServicer
|
||||
from pandora import host_pb2
|
||||
from pandora.host_pb2 import (
|
||||
DISCOVERABLE_GENERAL,
|
||||
DISCOVERABLE_LIMITED,
|
||||
NOT_CONNECTABLE,
|
||||
NOT_DISCOVERABLE,
|
||||
DISCOVERABLE_LIMITED,
|
||||
DISCOVERABLE_GENERAL,
|
||||
PRIMARY_1M,
|
||||
PRIMARY_CODED,
|
||||
SECONDARY_1M,
|
||||
@@ -48,6 +73,7 @@ from pandora.host_pb2 import (
|
||||
ConnectResponse,
|
||||
DataTypes,
|
||||
DisconnectRequest,
|
||||
DiscoverabilityMode,
|
||||
InquiryResponse,
|
||||
PrimaryPhy,
|
||||
ReadLocalAddressResponse,
|
||||
@@ -60,39 +86,9 @@ from pandora.host_pb2 import (
|
||||
WaitConnectionResponse,
|
||||
WaitDisconnectionRequest,
|
||||
)
|
||||
from typing import AsyncGenerator, Dict, List, Optional, Set, Tuple, cast
|
||||
|
||||
import bumble.device
|
||||
import bumble.utils
|
||||
from bumble.core import (
|
||||
UUID,
|
||||
AdvertisingData,
|
||||
Appearance,
|
||||
ConnectionError,
|
||||
PhysicalTransport,
|
||||
)
|
||||
from bumble.device import (
|
||||
DEVICE_DEFAULT_SCAN_INTERVAL,
|
||||
DEVICE_DEFAULT_SCAN_WINDOW,
|
||||
Advertisement,
|
||||
AdvertisingEventProperties,
|
||||
AdvertisingParameters,
|
||||
AdvertisingType,
|
||||
Device,
|
||||
)
|
||||
from bumble.gatt import Service
|
||||
from bumble.hci import (
|
||||
HCI_CONNECTION_ALREADY_EXISTS_ERROR,
|
||||
HCI_PAGE_TIMEOUT_ERROR,
|
||||
HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR,
|
||||
Address,
|
||||
OwnAddressType,
|
||||
Phy,
|
||||
Role,
|
||||
)
|
||||
from bumble.pandora import utils
|
||||
from bumble.pandora.config import Config
|
||||
|
||||
PRIMARY_PHY_MAP: dict[int, PrimaryPhy] = {
|
||||
PRIMARY_PHY_MAP: Dict[int, PrimaryPhy] = {
|
||||
# Default value reported by Bumble for legacy Advertising reports.
|
||||
# FIXME(uael): `None` might be a better value, but Bumble need to change accordingly.
|
||||
0: PRIMARY_1M,
|
||||
@@ -100,26 +96,26 @@ PRIMARY_PHY_MAP: dict[int, PrimaryPhy] = {
|
||||
3: PRIMARY_CODED,
|
||||
}
|
||||
|
||||
SECONDARY_PHY_MAP: dict[int, SecondaryPhy] = {
|
||||
SECONDARY_PHY_MAP: Dict[int, SecondaryPhy] = {
|
||||
0: SECONDARY_NONE,
|
||||
1: SECONDARY_1M,
|
||||
2: SECONDARY_2M,
|
||||
3: SECONDARY_CODED,
|
||||
}
|
||||
|
||||
PRIMARY_PHY_TO_BUMBLE_PHY_MAP: dict[PrimaryPhy, Phy] = {
|
||||
PRIMARY_PHY_TO_BUMBLE_PHY_MAP: Dict[PrimaryPhy, Phy] = {
|
||||
PRIMARY_1M: Phy.LE_1M,
|
||||
PRIMARY_CODED: Phy.LE_CODED,
|
||||
}
|
||||
|
||||
SECONDARY_PHY_TO_BUMBLE_PHY_MAP: dict[SecondaryPhy, Phy] = {
|
||||
SECONDARY_PHY_TO_BUMBLE_PHY_MAP: Dict[SecondaryPhy, Phy] = {
|
||||
SECONDARY_NONE: Phy.LE_1M,
|
||||
SECONDARY_1M: Phy.LE_1M,
|
||||
SECONDARY_2M: Phy.LE_2M,
|
||||
SECONDARY_CODED: Phy.LE_CODED,
|
||||
}
|
||||
|
||||
OWN_ADDRESS_MAP: dict[host_pb2.OwnAddressType, OwnAddressType] = {
|
||||
OWN_ADDRESS_MAP: Dict[host_pb2.OwnAddressType, OwnAddressType] = {
|
||||
host_pb2.PUBLIC: OwnAddressType.PUBLIC,
|
||||
host_pb2.RANDOM: OwnAddressType.RANDOM,
|
||||
host_pb2.RESOLVABLE_OR_PUBLIC: OwnAddressType.RESOLVABLE_OR_PUBLIC,
|
||||
@@ -128,7 +124,7 @@ OWN_ADDRESS_MAP: dict[host_pb2.OwnAddressType, OwnAddressType] = {
|
||||
|
||||
|
||||
class HostService(HostServicer):
|
||||
waited_connections: set[int]
|
||||
waited_connections: Set[int]
|
||||
|
||||
def __init__(
|
||||
self, grpc_server: grpc.aio.Server, device: Device, config: Config
|
||||
@@ -305,9 +301,7 @@ class HostService(HostServicer):
|
||||
await disconnection_future
|
||||
self.log.debug("Disconnected")
|
||||
finally:
|
||||
connection.remove_listener(
|
||||
connection.EVENT_DISCONNECTION, on_disconnection
|
||||
) # type: ignore
|
||||
connection.remove_listener(connection.EVENT_DISCONNECTION, on_disconnection) # type: ignore
|
||||
|
||||
return empty_pb2.Empty()
|
||||
|
||||
@@ -544,7 +538,7 @@ class HostService(HostServicer):
|
||||
await bumble.utils.cancel_on_event(
|
||||
self.device, 'flush', self.device.stop_advertising()
|
||||
)
|
||||
except Exception:
|
||||
except:
|
||||
pass
|
||||
|
||||
@utils.rpc
|
||||
@@ -614,7 +608,7 @@ class HostService(HostServicer):
|
||||
await bumble.utils.cancel_on_event(
|
||||
self.device, 'flush', self.device.stop_scanning()
|
||||
)
|
||||
except Exception:
|
||||
except:
|
||||
pass
|
||||
|
||||
@utils.rpc
|
||||
@@ -624,7 +618,7 @@ class HostService(HostServicer):
|
||||
self.log.debug('Inquiry')
|
||||
|
||||
inquiry_queue: asyncio.Queue[
|
||||
tuple[Address, int, AdvertisingData, int] | None
|
||||
Optional[Tuple[Address, int, AdvertisingData, int]]
|
||||
] = asyncio.Queue()
|
||||
complete_handler = self.device.on(
|
||||
self.device.EVENT_INQUIRY_COMPLETE, lambda: inquiry_queue.put_nowait(None)
|
||||
@@ -649,18 +643,14 @@ class HostService(HostServicer):
|
||||
)
|
||||
|
||||
finally:
|
||||
self.device.remove_listener(
|
||||
self.device.EVENT_INQUIRY_COMPLETE, complete_handler
|
||||
) # type: ignore
|
||||
self.device.remove_listener(
|
||||
self.device.EVENT_INQUIRY_RESULT, result_handler
|
||||
) # type: ignore
|
||||
self.device.remove_listener(self.device.EVENT_INQUIRY_COMPLETE, complete_handler) # type: ignore
|
||||
self.device.remove_listener(self.device.EVENT_INQUIRY_RESULT, result_handler) # type: ignore
|
||||
try:
|
||||
self.log.debug('Stop inquiry')
|
||||
await bumble.utils.cancel_on_event(
|
||||
self.device, 'flush', self.device.stop_discovery()
|
||||
)
|
||||
except Exception:
|
||||
except:
|
||||
pass
|
||||
|
||||
@utils.rpc
|
||||
@@ -680,10 +670,10 @@ class HostService(HostServicer):
|
||||
return empty_pb2.Empty()
|
||||
|
||||
def unpack_data_types(self, dt: DataTypes) -> AdvertisingData:
|
||||
ad_structures: list[tuple[int, bytes]] = []
|
||||
ad_structures: List[Tuple[int, bytes]] = []
|
||||
|
||||
uuids: list[str]
|
||||
datas: dict[str, bytes]
|
||||
uuids: List[str]
|
||||
datas: Dict[str, bytes]
|
||||
|
||||
def uuid128_from_str(uuid: str) -> bytes:
|
||||
"""Decode a 128-bit uuid encoded as XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
|
||||
@@ -897,50 +887,50 @@ class HostService(HostServicer):
|
||||
|
||||
def pack_data_types(self, ad: AdvertisingData) -> DataTypes:
|
||||
dt = DataTypes()
|
||||
uuids: list[UUID]
|
||||
uuids: List[UUID]
|
||||
s: str
|
||||
i: int
|
||||
ij: tuple[int, int]
|
||||
uuid_data: tuple[UUID, bytes]
|
||||
ij: Tuple[int, int]
|
||||
uuid_data: Tuple[UUID, bytes]
|
||||
data: bytes
|
||||
|
||||
if uuids := cast(
|
||||
list[UUID],
|
||||
List[UUID],
|
||||
ad.get(AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS),
|
||||
):
|
||||
dt.incomplete_service_class_uuids16.extend(
|
||||
list(map(lambda x: x.to_hex_str('-'), uuids))
|
||||
)
|
||||
if uuids := cast(
|
||||
list[UUID],
|
||||
List[UUID],
|
||||
ad.get(AdvertisingData.COMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS),
|
||||
):
|
||||
dt.complete_service_class_uuids16.extend(
|
||||
list(map(lambda x: x.to_hex_str('-'), uuids))
|
||||
)
|
||||
if uuids := cast(
|
||||
list[UUID],
|
||||
List[UUID],
|
||||
ad.get(AdvertisingData.INCOMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS),
|
||||
):
|
||||
dt.incomplete_service_class_uuids32.extend(
|
||||
list(map(lambda x: x.to_hex_str('-'), uuids))
|
||||
)
|
||||
if uuids := cast(
|
||||
list[UUID],
|
||||
List[UUID],
|
||||
ad.get(AdvertisingData.COMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS),
|
||||
):
|
||||
dt.complete_service_class_uuids32.extend(
|
||||
list(map(lambda x: x.to_hex_str('-'), uuids))
|
||||
)
|
||||
if uuids := cast(
|
||||
list[UUID],
|
||||
List[UUID],
|
||||
ad.get(AdvertisingData.INCOMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS),
|
||||
):
|
||||
dt.incomplete_service_class_uuids128.extend(
|
||||
list(map(lambda x: x.to_hex_str('-'), uuids))
|
||||
)
|
||||
if uuids := cast(
|
||||
list[UUID],
|
||||
List[UUID],
|
||||
ad.get(AdvertisingData.COMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS),
|
||||
):
|
||||
dt.complete_service_class_uuids128.extend(
|
||||
@@ -955,42 +945,42 @@ class HostService(HostServicer):
|
||||
if i := cast(int, ad.get(AdvertisingData.CLASS_OF_DEVICE)):
|
||||
dt.class_of_device = i
|
||||
if ij := cast(
|
||||
tuple[int, int],
|
||||
Tuple[int, int],
|
||||
ad.get(AdvertisingData.PERIPHERAL_CONNECTION_INTERVAL_RANGE),
|
||||
):
|
||||
dt.peripheral_connection_interval_min = ij[0]
|
||||
dt.peripheral_connection_interval_max = ij[1]
|
||||
if uuids := cast(
|
||||
list[UUID],
|
||||
List[UUID],
|
||||
ad.get(AdvertisingData.LIST_OF_16_BIT_SERVICE_SOLICITATION_UUIDS),
|
||||
):
|
||||
dt.service_solicitation_uuids16.extend(
|
||||
list(map(lambda x: x.to_hex_str('-'), uuids))
|
||||
)
|
||||
if uuids := cast(
|
||||
list[UUID],
|
||||
List[UUID],
|
||||
ad.get(AdvertisingData.LIST_OF_32_BIT_SERVICE_SOLICITATION_UUIDS),
|
||||
):
|
||||
dt.service_solicitation_uuids32.extend(
|
||||
list(map(lambda x: x.to_hex_str('-'), uuids))
|
||||
)
|
||||
if uuids := cast(
|
||||
list[UUID],
|
||||
List[UUID],
|
||||
ad.get(AdvertisingData.LIST_OF_128_BIT_SERVICE_SOLICITATION_UUIDS),
|
||||
):
|
||||
dt.service_solicitation_uuids128.extend(
|
||||
list(map(lambda x: x.to_hex_str('-'), uuids))
|
||||
)
|
||||
if uuid_data := cast(
|
||||
tuple[UUID, bytes], ad.get(AdvertisingData.SERVICE_DATA_16_BIT_UUID)
|
||||
Tuple[UUID, bytes], ad.get(AdvertisingData.SERVICE_DATA_16_BIT_UUID)
|
||||
):
|
||||
dt.service_data_uuid16[uuid_data[0].to_hex_str('-')] = uuid_data[1]
|
||||
if uuid_data := cast(
|
||||
tuple[UUID, bytes], ad.get(AdvertisingData.SERVICE_DATA_32_BIT_UUID)
|
||||
Tuple[UUID, bytes], ad.get(AdvertisingData.SERVICE_DATA_32_BIT_UUID)
|
||||
):
|
||||
dt.service_data_uuid32[uuid_data[0].to_hex_str('-')] = uuid_data[1]
|
||||
if uuid_data := cast(
|
||||
tuple[UUID, bytes], ad.get(AdvertisingData.SERVICE_DATA_128_BIT_UUID)
|
||||
Tuple[UUID, bytes], ad.get(AdvertisingData.SERVICE_DATA_128_BIT_UUID)
|
||||
):
|
||||
dt.service_data_uuid128[uuid_data[0].to_hex_str('-')] = uuid_data[1]
|
||||
if data := cast(bytes, ad.get(AdvertisingData.PUBLIC_TARGET_ADDRESS, raw=True)):
|
||||
|
||||
@@ -12,21 +12,31 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import grpc
|
||||
import json
|
||||
import logging
|
||||
from asyncio import Future
|
||||
from asyncio import Queue as AsyncQueue
|
||||
from collections.abc import AsyncGenerator
|
||||
from dataclasses import dataclass
|
||||
|
||||
import grpc
|
||||
from asyncio import Queue as AsyncQueue, Future
|
||||
|
||||
from bumble.pandora import utils
|
||||
from bumble.pandora.config import Config
|
||||
from bumble.core import OutOfResourcesError, InvalidArgumentError
|
||||
from bumble.device import Device
|
||||
from bumble.l2cap import (
|
||||
ClassicChannel,
|
||||
ClassicChannelServer,
|
||||
ClassicChannelSpec,
|
||||
LeCreditBasedChannel,
|
||||
LeCreditBasedChannelServer,
|
||||
LeCreditBasedChannelSpec,
|
||||
)
|
||||
from google.protobuf import any_pb2, empty_pb2 # pytype: disable=pyi-error
|
||||
from pandora.l2cap_grpc_aio import L2CAPServicer # pytype: disable=pyi-error
|
||||
from pandora.l2cap_pb2 import (
|
||||
from pandora.l2cap_pb2 import ( # pytype: disable=pyi-error
|
||||
COMMAND_NOT_UNDERSTOOD,
|
||||
INVALID_CID_IN_REQUEST,
|
||||
Channel as PandoraChannel,
|
||||
ConnectRequest,
|
||||
ConnectResponse,
|
||||
CreditBasedChannelRequest,
|
||||
@@ -41,22 +51,10 @@ from pandora.l2cap_pb2 import (
|
||||
WaitDisconnectionRequest,
|
||||
WaitDisconnectionResponse,
|
||||
)
|
||||
from pandora.l2cap_pb2 import Channel as PandoraChannel # pytype: disable=pyi-error
|
||||
from typing import AsyncGenerator, Dict, Optional, Union
|
||||
from dataclasses import dataclass
|
||||
|
||||
from bumble.core import InvalidArgumentError, OutOfResourcesError
|
||||
from bumble.device import Device
|
||||
from bumble.l2cap import (
|
||||
ClassicChannel,
|
||||
ClassicChannelServer,
|
||||
ClassicChannelSpec,
|
||||
LeCreditBasedChannel,
|
||||
LeCreditBasedChannelServer,
|
||||
LeCreditBasedChannelSpec,
|
||||
)
|
||||
from bumble.pandora import utils
|
||||
from bumble.pandora.config import Config
|
||||
|
||||
L2capChannel = ClassicChannel | LeCreditBasedChannel
|
||||
L2capChannel = Union[ClassicChannel, LeCreditBasedChannel]
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -72,7 +70,7 @@ class L2CAPService(L2CAPServicer):
|
||||
)
|
||||
self.device = device
|
||||
self.config = config
|
||||
self.channels: dict[bytes, ChannelContext] = {}
|
||||
self.channels: Dict[bytes, ChannelContext] = {}
|
||||
|
||||
def register_event(self, l2cap_channel: L2capChannel) -> ChannelContext:
|
||||
close_future = asyncio.get_running_loop().create_future()
|
||||
@@ -107,8 +105,10 @@ class L2CAPService(L2CAPServicer):
|
||||
oneof = request.WhichOneof('type')
|
||||
self.log.debug(f'WaitConnection channel request type: {oneof}.')
|
||||
channel_type = getattr(request, oneof)
|
||||
spec: ClassicChannelSpec | LeCreditBasedChannelSpec | None = None
|
||||
l2cap_server: ClassicChannelServer | LeCreditBasedChannelServer | None = None
|
||||
spec: Optional[Union[ClassicChannelSpec, LeCreditBasedChannelSpec]] = None
|
||||
l2cap_server: Optional[
|
||||
Union[ClassicChannelServer, LeCreditBasedChannelServer]
|
||||
] = None
|
||||
if isinstance(channel_type, CreditBasedChannelRequest):
|
||||
spec = LeCreditBasedChannelSpec(
|
||||
psm=channel_type.spsm,
|
||||
@@ -215,7 +215,7 @@ class L2CAPService(L2CAPServicer):
|
||||
oneof = request.WhichOneof('type')
|
||||
self.log.debug(f'Channel request type: {oneof}.')
|
||||
channel_type = getattr(request, oneof)
|
||||
spec: ClassicChannelSpec | LeCreditBasedChannelSpec | None = None
|
||||
spec: Optional[Union[ClassicChannelSpec, LeCreditBasedChannelSpec]] = None
|
||||
if isinstance(channel_type, CreditBasedChannelRequest):
|
||||
spec = LeCreditBasedChannelSpec(
|
||||
psm=channel_type.spsm,
|
||||
|
||||
@@ -13,19 +13,27 @@
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import logging
|
||||
from collections.abc import AsyncGenerator, AsyncIterator, Awaitable, Callable
|
||||
from typing import Any
|
||||
|
||||
from collections.abc import Awaitable
|
||||
import grpc
|
||||
from google.protobuf import (
|
||||
any_pb2, # pytype: disable=pyi-error
|
||||
empty_pb2, # pytype: disable=pyi-error
|
||||
wrappers_pb2, # pytype: disable=pyi-error
|
||||
import logging
|
||||
|
||||
from bumble.pandora import utils
|
||||
from bumble.pandora.config import Config
|
||||
from bumble import hci
|
||||
from bumble.core import (
|
||||
PhysicalTransport,
|
||||
ProtocolError,
|
||||
InvalidArgumentError,
|
||||
)
|
||||
import bumble.utils
|
||||
from bumble.device import Connection as BumbleConnection, Device
|
||||
from bumble.hci import HCI_Error, Role
|
||||
from bumble.pairing import PairingConfig, PairingDelegate as BasePairingDelegate
|
||||
from google.protobuf import any_pb2 # pytype: disable=pyi-error
|
||||
from google.protobuf import empty_pb2 # pytype: disable=pyi-error
|
||||
from google.protobuf import wrappers_pb2 # pytype: disable=pyi-error
|
||||
from pandora.host_pb2 import Connection
|
||||
from pandora.security_grpc_aio import SecurityServicer, SecurityStorageServicer
|
||||
from pandora.security_pb2 import (
|
||||
@@ -49,24 +57,14 @@ from pandora.security_pb2 import (
|
||||
WaitSecurityRequest,
|
||||
WaitSecurityResponse,
|
||||
)
|
||||
|
||||
import bumble.utils
|
||||
from bumble import hci
|
||||
from bumble.core import InvalidArgumentError, PhysicalTransport, ProtocolError
|
||||
from bumble.device import Connection as BumbleConnection
|
||||
from bumble.device import Device
|
||||
from bumble.hci import HCI_Error, Role
|
||||
from bumble.pairing import PairingConfig
|
||||
from bumble.pairing import PairingDelegate as BasePairingDelegate
|
||||
from bumble.pandora import utils
|
||||
from bumble.pandora.config import Config
|
||||
from typing import Any, AsyncGenerator, AsyncIterator, Callable, Dict, Optional, Union
|
||||
|
||||
|
||||
class PairingDelegate(BasePairingDelegate):
|
||||
def __init__(
|
||||
self,
|
||||
connection: BumbleConnection,
|
||||
service: SecurityService,
|
||||
service: "SecurityService",
|
||||
io_capability: BasePairingDelegate.IoCapability = BasePairingDelegate.NO_OUTPUT_NO_INPUT,
|
||||
local_initiator_key_distribution: BasePairingDelegate.KeyDistribution = BasePairingDelegate.DEFAULT_KEY_DISTRIBUTION,
|
||||
local_responder_key_distribution: BasePairingDelegate.KeyDistribution = BasePairingDelegate.DEFAULT_KEY_DISTRIBUTION,
|
||||
@@ -132,7 +130,7 @@ class PairingDelegate(BasePairingDelegate):
|
||||
assert answer.answer_variant() == 'confirm' and answer.confirm is not None
|
||||
return answer.confirm
|
||||
|
||||
async def get_number(self) -> int | None:
|
||||
async def get_number(self) -> Optional[int]:
|
||||
self.log.debug(
|
||||
f"Pairing event: `passkey_entry_request` (io_capability: {self.io_capability})"
|
||||
)
|
||||
@@ -149,7 +147,7 @@ class PairingDelegate(BasePairingDelegate):
|
||||
assert answer.answer_variant() == 'passkey'
|
||||
return answer.passkey
|
||||
|
||||
async def get_string(self, max_length: int) -> str | None:
|
||||
async def get_string(self, max_length: int) -> Optional[str]:
|
||||
self.log.debug(
|
||||
f"Pairing event: `pin_code_request` (io_capability: {self.io_capability})"
|
||||
)
|
||||
@@ -197,8 +195,8 @@ class SecurityService(SecurityServicer):
|
||||
self.log = utils.BumbleServerLoggerAdapter(
|
||||
logging.getLogger(), {'service_name': 'Security', 'device': device}
|
||||
)
|
||||
self.event_queue: asyncio.Queue[PairingEvent] | None = None
|
||||
self.event_answer: AsyncIterator[PairingEventAnswer] | None = None
|
||||
self.event_queue: Optional[asyncio.Queue[PairingEvent]] = None
|
||||
self.event_answer: Optional[AsyncIterator[PairingEventAnswer]] = None
|
||||
self.device = device
|
||||
self.config = config
|
||||
|
||||
@@ -233,7 +231,7 @@ class SecurityService(SecurityServicer):
|
||||
if level == LEVEL2:
|
||||
return connection.encryption != 0 and connection.authenticated
|
||||
|
||||
link_key_type: int | None = None
|
||||
link_key_type: Optional[int] = None
|
||||
if (keystore := connection.device.keystore) and (
|
||||
keys := await keystore.get(str(connection.peer_address))
|
||||
):
|
||||
@@ -246,16 +244,16 @@ class SecurityService(SecurityServicer):
|
||||
and connection.authenticated
|
||||
and link_key_type
|
||||
in (
|
||||
hci.LinkKeyType.AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_192,
|
||||
hci.LinkKeyType.AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256,
|
||||
hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_192_TYPE,
|
||||
hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE,
|
||||
)
|
||||
)
|
||||
if level == LEVEL4:
|
||||
return (
|
||||
connection.encryption == hci.HCI_Encryption_Change_Event.Enabled.AES_CCM
|
||||
connection.encryption == hci.HCI_Encryption_Change_Event.AES_CCM
|
||||
and connection.authenticated
|
||||
and link_key_type
|
||||
== hci.LinkKeyType.AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256
|
||||
== hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE
|
||||
)
|
||||
raise InvalidArgumentError(f"Unexpected level {level}")
|
||||
|
||||
@@ -412,8 +410,8 @@ class SecurityService(SecurityServicer):
|
||||
wait_for_security: asyncio.Future[str] = (
|
||||
asyncio.get_running_loop().create_future()
|
||||
)
|
||||
authenticate_task: asyncio.Future[None] | None = None
|
||||
pair_task: asyncio.Future[None] | None = None
|
||||
authenticate_task: Optional[asyncio.Future[None]] = None
|
||||
pair_task: Optional[asyncio.Future[None]] = None
|
||||
|
||||
async def authenticate() -> None:
|
||||
if (encryption := connection.encryption) != 0:
|
||||
@@ -457,9 +455,9 @@ class SecurityService(SecurityServicer):
|
||||
|
||||
def pair(*_: Any) -> None:
|
||||
if self.need_pairing(connection, level):
|
||||
bumble.utils.AsyncRunner.spawn(connection.pair())
|
||||
pair_task = asyncio.create_task(connection.pair())
|
||||
|
||||
listeners: dict[str, Callable[..., None | Awaitable[None]]] = {
|
||||
listeners: Dict[str, Callable[..., Union[None, Awaitable[None]]]] = {
|
||||
'disconnection': set_failure('connection_died'),
|
||||
'pairing_failure': set_failure('pairing_failure'),
|
||||
'connection_authentication_failure': set_failure('authentication_failure'),
|
||||
@@ -502,7 +500,7 @@ class SecurityService(SecurityServicer):
|
||||
return WaitSecurityResponse(**kwargs)
|
||||
|
||||
async def reached_security_level(
|
||||
self, connection: BumbleConnection, level: SecurityLevel | LESecurityLevel
|
||||
self, connection: BumbleConnection, level: Union[SecurityLevel, LESecurityLevel]
|
||||
) -> bool:
|
||||
self.log.debug(
|
||||
str(
|
||||
|
||||
@@ -13,21 +13,18 @@
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import functools
|
||||
import grpc
|
||||
import inspect
|
||||
import logging
|
||||
from collections.abc import Generator, MutableMapping
|
||||
from typing import Any
|
||||
|
||||
import grpc
|
||||
from google.protobuf.message import Message # pytype: disable=pyi-error
|
||||
|
||||
from bumble.device import Device
|
||||
from bumble.hci import Address, AddressType
|
||||
from google.protobuf.message import Message # pytype: disable=pyi-error
|
||||
from typing import Any, Dict, Generator, MutableMapping, Optional, Tuple
|
||||
|
||||
ADDRESS_TYPES: dict[str, AddressType] = {
|
||||
ADDRESS_TYPES: Dict[str, AddressType] = {
|
||||
"public": Address.PUBLIC_DEVICE_ADDRESS,
|
||||
"random": Address.RANDOM_DEVICE_ADDRESS,
|
||||
"public_identity": Address.PUBLIC_IDENTITY_ADDRESS,
|
||||
@@ -35,7 +32,7 @@ ADDRESS_TYPES: dict[str, AddressType] = {
|
||||
}
|
||||
|
||||
|
||||
def address_from_request(request: Message, field: str | None) -> Address:
|
||||
def address_from_request(request: Message, field: Optional[str]) -> Address:
|
||||
if field is None:
|
||||
return Address.ANY
|
||||
return Address(bytes(reversed(getattr(request, field))), ADDRESS_TYPES[field])
|
||||
@@ -46,7 +43,7 @@ class BumbleServerLoggerAdapter(logging.LoggerAdapter): # type: ignore
|
||||
|
||||
def process(
|
||||
self, msg: str, kwargs: MutableMapping[str, Any]
|
||||
) -> tuple[str, MutableMapping[str, Any]]:
|
||||
) -> Tuple[str, MutableMapping[str, Any]]:
|
||||
assert self.extra
|
||||
service_name = self.extra['service_name']
|
||||
assert isinstance(service_name, str)
|
||||
@@ -96,7 +93,8 @@ def rpc(func: Any) -> Any:
|
||||
@functools.wraps(func)
|
||||
def gen_wrapper(self: Any, request: Any, context: grpc.ServicerContext) -> Any:
|
||||
with exception_to_rpc_error(context):
|
||||
yield from func(self, request, context)
|
||||
for v in func(self, request, context):
|
||||
yield v
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(self: Any, request: Any, context: grpc.ServicerContext) -> Any:
|
||||
|
||||
@@ -18,26 +18,26 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import struct
|
||||
from dataclasses import dataclass
|
||||
|
||||
from bumble import utils
|
||||
from bumble.att import ATT_Error
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from bumble.device import Connection
|
||||
from bumble.att import ATT_Error
|
||||
from bumble.gatt import (
|
||||
GATT_AUDIO_INPUT_CONTROL_POINT_CHARACTERISTIC,
|
||||
GATT_AUDIO_INPUT_CONTROL_SERVICE,
|
||||
GATT_AUDIO_INPUT_DESCRIPTION_CHARACTERISTIC,
|
||||
GATT_AUDIO_INPUT_STATE_CHARACTERISTIC,
|
||||
GATT_AUDIO_INPUT_STATUS_CHARACTERISTIC,
|
||||
GATT_AUDIO_INPUT_TYPE_CHARACTERISTIC,
|
||||
GATT_GAIN_SETTINGS_ATTRIBUTE_CHARACTERISTIC,
|
||||
Attribute,
|
||||
Characteristic,
|
||||
CharacteristicValue,
|
||||
TemplateService,
|
||||
CharacteristicValue,
|
||||
GATT_AUDIO_INPUT_CONTROL_SERVICE,
|
||||
GATT_AUDIO_INPUT_STATE_CHARACTERISTIC,
|
||||
GATT_GAIN_SETTINGS_ATTRIBUTE_CHARACTERISTIC,
|
||||
GATT_AUDIO_INPUT_TYPE_CHARACTERISTIC,
|
||||
GATT_AUDIO_INPUT_STATUS_CHARACTERISTIC,
|
||||
GATT_AUDIO_INPUT_CONTROL_POINT_CHARACTERISTIC,
|
||||
GATT_AUDIO_INPUT_DESCRIPTION_CHARACTERISTIC,
|
||||
)
|
||||
from bumble.gatt_adapters import (
|
||||
CharacteristicProxy,
|
||||
@@ -48,6 +48,7 @@ from bumble.gatt_adapters import (
|
||||
UTF8CharacteristicProxyAdapter,
|
||||
)
|
||||
from bumble.gatt_client import ProfileServiceProxy, ServiceProxy
|
||||
from bumble import utils
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -128,7 +129,7 @@ class AudioInputState:
|
||||
mute: Mute = Mute.NOT_MUTED
|
||||
gain_mode: GainMode = GainMode.MANUAL
|
||||
change_counter: int = 0
|
||||
attribute: Attribute | None = None
|
||||
attribute: Optional[Attribute] = None
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
return bytes(
|
||||
@@ -197,7 +198,9 @@ class AudioInputControlPoint:
|
||||
audio_input_state: AudioInputState
|
||||
gain_settings_properties: GainSettingsProperties
|
||||
|
||||
async def on_write(self, connection: Connection, value: bytes) -> None:
|
||||
async def on_write(self, connection: Optional[Connection], value: bytes) -> None:
|
||||
assert connection
|
||||
|
||||
opcode = AudioInputControlPointOpCode(value[0])
|
||||
|
||||
if opcode == AudioInputControlPointOpCode.SET_GAIN_SETTING:
|
||||
@@ -315,12 +318,13 @@ class AudioInputDescription:
|
||||
'''
|
||||
|
||||
audio_input_description: str = "Bluetooth"
|
||||
attribute: Attribute | None = None
|
||||
attribute: Optional[Attribute] = None
|
||||
|
||||
def on_read(self, _connection: Connection) -> str:
|
||||
def on_read(self, _connection: Optional[Connection]) -> str:
|
||||
return self.audio_input_description
|
||||
|
||||
async def on_write(self, connection: Connection, value: str) -> None:
|
||||
async def on_write(self, connection: Optional[Connection], value: str) -> None:
|
||||
assert connection
|
||||
assert self.attribute
|
||||
|
||||
self.audio_input_description = value
|
||||
@@ -338,11 +342,11 @@ class AICSService(TemplateService):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
audio_input_state: AudioInputState | None = None,
|
||||
gain_settings_properties: GainSettingsProperties | None = None,
|
||||
audio_input_state: Optional[AudioInputState] = None,
|
||||
gain_settings_properties: Optional[GainSettingsProperties] = None,
|
||||
audio_input_type: str = "local",
|
||||
audio_input_status: AudioInputStatus | None = None,
|
||||
audio_input_description: AudioInputDescription | None = None,
|
||||
audio_input_status: Optional[AudioInputStatus] = None,
|
||||
audio_input_description: Optional[AudioInputDescription] = None,
|
||||
):
|
||||
self.audio_input_state = (
|
||||
AudioInputState() if audio_input_state is None else audio_input_state
|
||||
|
||||
@@ -1,401 +0,0 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
Apple Media Service (AMS).
|
||||
"""
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import enum
|
||||
import logging
|
||||
from collections.abc import Iterable
|
||||
|
||||
from bumble import utils
|
||||
from bumble.device import Peer
|
||||
from bumble.gatt import (
|
||||
GATT_AMS_ENTITY_ATTRIBUTE_CHARACTERISTIC,
|
||||
GATT_AMS_ENTITY_UPDATE_CHARACTERISTIC,
|
||||
GATT_AMS_REMOTE_COMMAND_CHARACTERISTIC,
|
||||
GATT_AMS_SERVICE,
|
||||
Characteristic,
|
||||
TemplateService,
|
||||
)
|
||||
from bumble.gatt_client import CharacteristicProxy, ProfileServiceProxy, ServiceProxy
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Protocol
|
||||
# -----------------------------------------------------------------------------
|
||||
class RemoteCommandId(utils.OpenIntEnum):
|
||||
PLAY = 0
|
||||
PAUSE = 1
|
||||
TOGGLE_PLAY_PAUSE = 2
|
||||
NEXT_TRACK = 3
|
||||
PREVIOUS_TRACK = 4
|
||||
VOLUME_UP = 5
|
||||
VOLUME_DOWN = 6
|
||||
ADVANCE_REPEAT_MODE = 7
|
||||
ADVANCE_SHUFFLE_MODE = 8
|
||||
SKIP_FORWARD = 9
|
||||
SKIP_BACKWARD = 10
|
||||
LIKE_TRACK = 11
|
||||
DISLIKE_TRACK = 12
|
||||
BOOKMARK_TRACK = 13
|
||||
|
||||
|
||||
class EntityId(utils.OpenIntEnum):
|
||||
PLAYER = 0
|
||||
QUEUE = 1
|
||||
TRACK = 2
|
||||
|
||||
|
||||
class ActionId(utils.OpenIntEnum):
|
||||
POSITIVE = 0
|
||||
NEGATIVE = 1
|
||||
|
||||
|
||||
class EntityUpdateFlags(enum.IntFlag):
|
||||
TRUNCATED = 1
|
||||
|
||||
|
||||
class PlayerAttributeId(utils.OpenIntEnum):
|
||||
NAME = 0
|
||||
PLAYBACK_INFO = 1
|
||||
VOLUME = 2
|
||||
|
||||
|
||||
class QueueAttributeId(utils.OpenIntEnum):
|
||||
INDEX = 0
|
||||
COUNT = 1
|
||||
SHUFFLE_MODE = 2
|
||||
REPEAT_MODE = 3
|
||||
|
||||
|
||||
class ShuffleMode(utils.OpenIntEnum):
|
||||
OFF = 0
|
||||
ONE = 1
|
||||
ALL = 2
|
||||
|
||||
|
||||
class RepeatMode(utils.OpenIntEnum):
|
||||
OFF = 0
|
||||
ONE = 1
|
||||
ALL = 2
|
||||
|
||||
|
||||
class TrackAttributeId(utils.OpenIntEnum):
|
||||
ARTIST = 0
|
||||
ALBUM = 1
|
||||
TITLE = 2
|
||||
DURATION = 3
|
||||
|
||||
|
||||
class PlaybackState(utils.OpenIntEnum):
|
||||
PAUSED = 0
|
||||
PLAYING = 1
|
||||
REWINDING = 2
|
||||
FAST_FORWARDING = 3
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class PlaybackInfo:
|
||||
playback_state: PlaybackState = PlaybackState.PAUSED
|
||||
playback_rate: float = 1.0
|
||||
elapsed_time: float = 0.0
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# GATT Server-side
|
||||
# -----------------------------------------------------------------------------
|
||||
class Ams(TemplateService):
|
||||
UUID = GATT_AMS_SERVICE
|
||||
|
||||
remote_command_characteristic: Characteristic
|
||||
entity_update_characteristic: Characteristic
|
||||
entity_attribute_characteristic: Characteristic
|
||||
|
||||
def __init__(self) -> None:
|
||||
# TODO not the final implementation
|
||||
self.remote_command_characteristic = Characteristic(
|
||||
GATT_AMS_REMOTE_COMMAND_CHARACTERISTIC,
|
||||
Characteristic.Properties.NOTIFY
|
||||
| Characteristic.Properties.WRITE_WITHOUT_RESPONSE,
|
||||
Characteristic.Permissions.WRITEABLE,
|
||||
)
|
||||
|
||||
# TODO not the final implementation
|
||||
self.entity_update_characteristic = Characteristic(
|
||||
GATT_AMS_ENTITY_UPDATE_CHARACTERISTIC,
|
||||
Characteristic.Properties.NOTIFY | Characteristic.Properties.WRITE,
|
||||
Characteristic.Permissions.WRITEABLE,
|
||||
)
|
||||
|
||||
# TODO not the final implementation
|
||||
self.entity_attribute_characteristic = Characteristic(
|
||||
GATT_AMS_ENTITY_ATTRIBUTE_CHARACTERISTIC,
|
||||
Characteristic.Properties.READ
|
||||
| Characteristic.Properties.WRITE_WITHOUT_RESPONSE,
|
||||
Characteristic.Permissions.WRITEABLE | Characteristic.Permissions.READABLE,
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
[
|
||||
self.remote_command_characteristic,
|
||||
self.entity_update_characteristic,
|
||||
self.entity_attribute_characteristic,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# GATT Client-side
|
||||
# -----------------------------------------------------------------------------
|
||||
class AmsProxy(ProfileServiceProxy):
|
||||
SERVICE_CLASS = Ams
|
||||
|
||||
# NOTE: these don't use adapters, because the format for write and notifications
|
||||
# are different.
|
||||
remote_command: CharacteristicProxy[bytes]
|
||||
entity_update: CharacteristicProxy[bytes]
|
||||
entity_attribute: CharacteristicProxy[bytes]
|
||||
|
||||
def __init__(self, service_proxy: ServiceProxy):
|
||||
self.remote_command = service_proxy.get_required_characteristic_by_uuid(
|
||||
GATT_AMS_REMOTE_COMMAND_CHARACTERISTIC
|
||||
)
|
||||
|
||||
self.entity_update = service_proxy.get_required_characteristic_by_uuid(
|
||||
GATT_AMS_ENTITY_UPDATE_CHARACTERISTIC
|
||||
)
|
||||
|
||||
self.entity_attribute = service_proxy.get_required_characteristic_by_uuid(
|
||||
GATT_AMS_ENTITY_ATTRIBUTE_CHARACTERISTIC
|
||||
)
|
||||
|
||||
|
||||
class AmsClient(utils.EventEmitter):
|
||||
EVENT_SUPPORTED_COMMANDS = "supported_commands"
|
||||
EVENT_PLAYER_NAME = "player_name"
|
||||
EVENT_PLAYER_PLAYBACK_INFO = "player_playback_info"
|
||||
EVENT_PLAYER_VOLUME = "player_volume"
|
||||
EVENT_QUEUE_COUNT = "queue_count"
|
||||
EVENT_QUEUE_INDEX = "queue_index"
|
||||
EVENT_QUEUE_SHUFFLE_MODE = "queue_shuffle_mode"
|
||||
EVENT_QUEUE_REPEAT_MODE = "queue_repeat_mode"
|
||||
EVENT_TRACK_ARTIST = "track_artist"
|
||||
EVENT_TRACK_ALBUM = "track_album"
|
||||
EVENT_TRACK_TITLE = "track_title"
|
||||
EVENT_TRACK_DURATION = "track_duration"
|
||||
|
||||
supported_commands: set[RemoteCommandId]
|
||||
player_name: str = ""
|
||||
player_playback_info: PlaybackInfo = PlaybackInfo(PlaybackState.PAUSED, 0.0, 0.0)
|
||||
player_volume: float = 1.0
|
||||
queue_count: int = 0
|
||||
queue_index: int = 0
|
||||
queue_shuffle_mode: ShuffleMode = ShuffleMode.OFF
|
||||
queue_repeat_mode: RepeatMode = RepeatMode.OFF
|
||||
track_artist: str = ""
|
||||
track_album: str = ""
|
||||
track_title: str = ""
|
||||
track_duration: float = 0.0
|
||||
|
||||
def __init__(self, ams_proxy: AmsProxy) -> None:
|
||||
super().__init__()
|
||||
self._ams_proxy = ams_proxy
|
||||
self._started = False
|
||||
self._read_attribute_semaphore = asyncio.Semaphore()
|
||||
self.supported_commands = set()
|
||||
|
||||
@classmethod
|
||||
async def for_peer(cls, peer: Peer) -> AmsClient | None:
|
||||
ams_proxy = await peer.discover_service_and_create_proxy(AmsProxy)
|
||||
if ams_proxy is None:
|
||||
return None
|
||||
return cls(ams_proxy)
|
||||
|
||||
async def start(self) -> None:
|
||||
logger.debug("subscribing to remote command characteristic")
|
||||
await self._ams_proxy.remote_command.subscribe(
|
||||
self._on_remote_command_notification
|
||||
)
|
||||
|
||||
logger.debug("subscribing to entity update characteristic")
|
||||
await self._ams_proxy.entity_update.subscribe(
|
||||
lambda data: utils.AsyncRunner.spawn(
|
||||
self._on_entity_update_notification(data)
|
||||
)
|
||||
)
|
||||
|
||||
self._started = True
|
||||
|
||||
async def stop(self) -> None:
|
||||
await self._ams_proxy.remote_command.unsubscribe(
|
||||
self._on_remote_command_notification
|
||||
)
|
||||
await self._ams_proxy.entity_update.unsubscribe(
|
||||
self._on_entity_update_notification
|
||||
)
|
||||
self._started = False
|
||||
|
||||
async def observe(
|
||||
self,
|
||||
entity: EntityId,
|
||||
attributes: Iterable[PlayerAttributeId | QueueAttributeId | TrackAttributeId],
|
||||
) -> None:
|
||||
await self._ams_proxy.entity_update.write_value(
|
||||
bytes([entity] + list(attributes)), with_response=True
|
||||
)
|
||||
|
||||
async def command(self, command: RemoteCommandId) -> None:
|
||||
await self._ams_proxy.remote_command.write_value(
|
||||
bytes([command]), with_response=True
|
||||
)
|
||||
|
||||
async def play(self) -> None:
|
||||
await self.command(RemoteCommandId.PLAY)
|
||||
|
||||
async def pause(self) -> None:
|
||||
await self.command(RemoteCommandId.PAUSE)
|
||||
|
||||
async def toggle_play_pause(self) -> None:
|
||||
await self.command(RemoteCommandId.TOGGLE_PLAY_PAUSE)
|
||||
|
||||
async def next_track(self) -> None:
|
||||
await self.command(RemoteCommandId.NEXT_TRACK)
|
||||
|
||||
async def previous_track(self) -> None:
|
||||
await self.command(RemoteCommandId.PREVIOUS_TRACK)
|
||||
|
||||
async def volume_up(self) -> None:
|
||||
await self.command(RemoteCommandId.VOLUME_UP)
|
||||
|
||||
async def volume_down(self) -> None:
|
||||
await self.command(RemoteCommandId.VOLUME_DOWN)
|
||||
|
||||
async def advance_repeat_mode(self) -> None:
|
||||
await self.command(RemoteCommandId.ADVANCE_REPEAT_MODE)
|
||||
|
||||
async def advance_shuffle_mode(self) -> None:
|
||||
await self.command(RemoteCommandId.ADVANCE_SHUFFLE_MODE)
|
||||
|
||||
async def skip_forward(self) -> None:
|
||||
await self.command(RemoteCommandId.SKIP_FORWARD)
|
||||
|
||||
async def skip_backward(self) -> None:
|
||||
await self.command(RemoteCommandId.SKIP_BACKWARD)
|
||||
|
||||
async def like_track(self) -> None:
|
||||
await self.command(RemoteCommandId.LIKE_TRACK)
|
||||
|
||||
async def dislike_track(self) -> None:
|
||||
await self.command(RemoteCommandId.DISLIKE_TRACK)
|
||||
|
||||
async def bookmark_track(self) -> None:
|
||||
await self.command(RemoteCommandId.BOOKMARK_TRACK)
|
||||
|
||||
def _on_remote_command_notification(self, data: bytes) -> None:
|
||||
supported_commands = [RemoteCommandId(command) for command in data]
|
||||
logger.debug(
|
||||
f"supported commands: {[command.name for command in supported_commands]}"
|
||||
)
|
||||
for command in supported_commands:
|
||||
self.supported_commands.add(command)
|
||||
|
||||
self.emit(self.EVENT_SUPPORTED_COMMANDS)
|
||||
|
||||
async def _on_entity_update_notification(self, data: bytes) -> None:
|
||||
entity = EntityId(data[0])
|
||||
flags = EntityUpdateFlags(data[2])
|
||||
value = data[3:]
|
||||
|
||||
if flags & EntityUpdateFlags.TRUNCATED:
|
||||
logger.debug("truncated attribute, fetching full value")
|
||||
|
||||
# Write the entity and attribute we're interested in
|
||||
# (protected by a semaphore, so that we only read one attribute at a time)
|
||||
async with self._read_attribute_semaphore:
|
||||
await self._ams_proxy.entity_attribute.write_value(
|
||||
data[:2], with_response=True
|
||||
)
|
||||
value = await self._ams_proxy.entity_attribute.read_value()
|
||||
|
||||
if entity == EntityId.PLAYER:
|
||||
player_attribute = PlayerAttributeId(data[1])
|
||||
if player_attribute == PlayerAttributeId.NAME:
|
||||
self.player_name = value.decode()
|
||||
self.emit(self.EVENT_PLAYER_NAME)
|
||||
elif player_attribute == PlayerAttributeId.PLAYBACK_INFO:
|
||||
playback_state_str, playback_rate_str, elapsed_time_str = (
|
||||
value.decode().split(",")
|
||||
)
|
||||
self.player_playback_info = PlaybackInfo(
|
||||
PlaybackState(int(playback_state_str)),
|
||||
float(playback_rate_str),
|
||||
float(elapsed_time_str),
|
||||
)
|
||||
self.emit(self.EVENT_PLAYER_PLAYBACK_INFO)
|
||||
elif player_attribute == PlayerAttributeId.VOLUME:
|
||||
self.player_volume = float(value.decode())
|
||||
self.emit(self.EVENT_PLAYER_VOLUME)
|
||||
else:
|
||||
logger.warning(f"received unknown player attribute {player_attribute}")
|
||||
|
||||
elif entity == EntityId.QUEUE:
|
||||
queue_attribute = QueueAttributeId(data[1])
|
||||
if queue_attribute == QueueAttributeId.COUNT:
|
||||
self.queue_count = int(value)
|
||||
self.emit(self.EVENT_QUEUE_COUNT)
|
||||
elif queue_attribute == QueueAttributeId.INDEX:
|
||||
self.queue_index = int(value)
|
||||
self.emit(self.EVENT_QUEUE_INDEX)
|
||||
elif queue_attribute == QueueAttributeId.REPEAT_MODE:
|
||||
self.queue_repeat_mode = RepeatMode(int(value))
|
||||
self.emit(self.EVENT_QUEUE_REPEAT_MODE)
|
||||
elif queue_attribute == QueueAttributeId.SHUFFLE_MODE:
|
||||
self.queue_shuffle_mode = ShuffleMode(int(value))
|
||||
self.emit(self.EVENT_QUEUE_SHUFFLE_MODE)
|
||||
else:
|
||||
logger.warning(f"received unknown queue attribute {queue_attribute}")
|
||||
|
||||
elif entity == EntityId.TRACK:
|
||||
track_attribute = TrackAttributeId(data[1])
|
||||
if track_attribute == TrackAttributeId.ARTIST:
|
||||
self.track_artist = value.decode()
|
||||
self.emit(self.EVENT_TRACK_ARTIST)
|
||||
elif track_attribute == TrackAttributeId.ALBUM:
|
||||
self.track_album = value.decode()
|
||||
self.emit(self.EVENT_TRACK_ALBUM)
|
||||
elif track_attribute == TrackAttributeId.TITLE:
|
||||
self.track_title = value.decode()
|
||||
self.emit(self.EVENT_TRACK_TITLE)
|
||||
elif track_attribute == TrackAttributeId.DURATION:
|
||||
self.track_duration = float(value.decode())
|
||||
self.emit(self.EVENT_TRACK_DURATION)
|
||||
else:
|
||||
logger.warning(f"received unknown track attribute {track_attribute}")
|
||||
|
||||
else:
|
||||
logger.warning(f"received unknown attribute ID {data[1]}")
|
||||
@@ -20,28 +20,29 @@ Apple Notification Center Service (ANCS).
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import datetime
|
||||
import enum
|
||||
import logging
|
||||
import struct
|
||||
from collections.abc import Sequence
|
||||
from typing import Optional, Sequence, Union
|
||||
|
||||
|
||||
from bumble import utils
|
||||
from bumble.att import ATT_Error
|
||||
from bumble.device import Peer
|
||||
from bumble.gatt import (
|
||||
Characteristic,
|
||||
GATT_ANCS_SERVICE,
|
||||
GATT_ANCS_NOTIFICATION_SOURCE_CHARACTERISTIC,
|
||||
GATT_ANCS_CONTROL_POINT_CHARACTERISTIC,
|
||||
GATT_ANCS_DATA_SOURCE_CHARACTERISTIC,
|
||||
GATT_ANCS_NOTIFICATION_SOURCE_CHARACTERISTIC,
|
||||
GATT_ANCS_SERVICE,
|
||||
Characteristic,
|
||||
TemplateService,
|
||||
)
|
||||
from bumble.gatt_adapters import SerializableCharacteristicProxyAdapter
|
||||
from bumble.gatt_client import CharacteristicProxy, ProfileServiceProxy, ServiceProxy
|
||||
from bumble.gatt_adapters import SerializableCharacteristicProxyAdapter
|
||||
from bumble import utils
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
@@ -116,7 +117,7 @@ class NotificationAttributeId(utils.OpenIntEnum):
|
||||
@dataclasses.dataclass
|
||||
class NotificationAttribute:
|
||||
attribute_id: NotificationAttributeId
|
||||
value: str | int | datetime.datetime
|
||||
value: Union[str, int, datetime.datetime]
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
@@ -242,10 +243,10 @@ class AncsProxy(ProfileServiceProxy):
|
||||
|
||||
|
||||
class AncsClient(utils.EventEmitter):
|
||||
_expected_response_command_id: CommandId | None
|
||||
_expected_response_notification_uid: int | None
|
||||
_expected_response_app_identifier: str | None
|
||||
_expected_app_identifier: str | None
|
||||
_expected_response_command_id: Optional[CommandId]
|
||||
_expected_response_notification_uid: Optional[int]
|
||||
_expected_response_app_identifier: Optional[str]
|
||||
_expected_app_identifier: Optional[str]
|
||||
_expected_response_tuples: int
|
||||
_response_accumulator: bytes
|
||||
|
||||
@@ -255,12 +256,12 @@ class AncsClient(utils.EventEmitter):
|
||||
super().__init__()
|
||||
self._ancs_proxy = ancs_proxy
|
||||
self._command_semaphore = asyncio.Semaphore()
|
||||
self._response: asyncio.Future | None = None
|
||||
self._response: Optional[asyncio.Future] = None
|
||||
self._reset_response()
|
||||
self._started = False
|
||||
|
||||
@classmethod
|
||||
async def for_peer(cls, peer: Peer) -> AncsClient | None:
|
||||
async def for_peer(cls, peer: Peer) -> Optional[AncsClient]:
|
||||
ancs_proxy = await peer.discover_service_and_create_proxy(AncsProxy)
|
||||
if ancs_proxy is None:
|
||||
return None
|
||||
@@ -316,7 +317,7 @@ class AncsClient(utils.EventEmitter):
|
||||
# Not enough data yet.
|
||||
return
|
||||
|
||||
attributes: list[NotificationAttribute | AppAttribute] = []
|
||||
attributes: list[Union[NotificationAttribute, AppAttribute]] = []
|
||||
|
||||
if command_id == CommandId.GET_NOTIFICATION_ATTRIBUTES:
|
||||
(notification_uid,) = struct.unpack_from(
|
||||
@@ -342,7 +343,7 @@ class AncsClient(utils.EventEmitter):
|
||||
str_value = attribute_data[3 : 3 + attribute_data_length].decode(
|
||||
"utf-8"
|
||||
)
|
||||
value: str | int | datetime.datetime
|
||||
value: Union[str, int, datetime.datetime]
|
||||
if attribute_id == NotificationAttributeId.MESSAGE_SIZE:
|
||||
value = int(str_value)
|
||||
elif attribute_id == NotificationAttributeId.DATE:
|
||||
@@ -415,7 +416,7 @@ class AncsClient(utils.EventEmitter):
|
||||
self,
|
||||
notification_uid: int,
|
||||
attributes: Sequence[
|
||||
NotificationAttributeId | tuple[NotificationAttributeId, int]
|
||||
Union[NotificationAttributeId, tuple[NotificationAttributeId, int]]
|
||||
],
|
||||
) -> list[NotificationAttribute]:
|
||||
if not self._started:
|
||||
|
||||
@@ -19,16 +19,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
import functools
|
||||
import logging
|
||||
import struct
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, TypeVar
|
||||
from typing import Any, Dict, List, Optional, Sequence, Tuple, Type, Union
|
||||
|
||||
from bumble import colors, device, gatt, gatt_client, hci, utils
|
||||
from bumble.profiles import le_audio
|
||||
from bumble import utils
|
||||
from bumble import colors
|
||||
from bumble.profiles.bap import CodecSpecificConfiguration
|
||||
from bumble.profiles import le_audio
|
||||
from bumble import device
|
||||
from bumble import gatt
|
||||
from bumble import gatt_client
|
||||
from bumble import hci
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -46,11 +48,11 @@ class ASE_Operation:
|
||||
See Audio Stream Control Service - 5 ASE Control operations.
|
||||
'''
|
||||
|
||||
classes: dict[int, type[ASE_Operation]] = {}
|
||||
op_code: Opcode
|
||||
classes: Dict[int, Type[ASE_Operation]] = {}
|
||||
op_code: int
|
||||
name: str
|
||||
fields: Sequence[Any] | None = None
|
||||
ase_id: Sequence[int]
|
||||
fields: Optional[Sequence[Any]] = None
|
||||
ase_id: List[int]
|
||||
|
||||
class Opcode(enum.IntEnum):
|
||||
# fmt: off
|
||||
@@ -63,30 +65,51 @@ class ASE_Operation:
|
||||
UPDATE_METADATA = 0x07
|
||||
RELEASE = 0x08
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, pdu: bytes) -> ASE_Operation:
|
||||
@staticmethod
|
||||
def from_bytes(pdu: bytes) -> ASE_Operation:
|
||||
op_code = pdu[0]
|
||||
|
||||
clazz = ASE_Operation.classes[op_code]
|
||||
return clazz(
|
||||
**hci.HCI_Object.dict_from_bytes(pdu, offset=1, fields=clazz.fields)
|
||||
)
|
||||
cls = ASE_Operation.classes.get(op_code)
|
||||
if cls is None:
|
||||
instance = ASE_Operation(pdu)
|
||||
instance.name = ASE_Operation.Opcode(op_code).name
|
||||
instance.op_code = op_code
|
||||
return instance
|
||||
self = cls.__new__(cls)
|
||||
ASE_Operation.__init__(self, pdu)
|
||||
if self.fields is not None:
|
||||
self.init_from_bytes(pdu, 1)
|
||||
return self
|
||||
|
||||
_OP = TypeVar("_OP", bound="ASE_Operation")
|
||||
@staticmethod
|
||||
def subclass(fields):
|
||||
def inner(cls: Type[ASE_Operation]):
|
||||
try:
|
||||
operation = ASE_Operation.Opcode[cls.__name__[4:].upper()]
|
||||
cls.name = operation.name
|
||||
cls.op_code = operation
|
||||
except:
|
||||
raise KeyError(f'PDU name {cls.name} not found in Ase_Operation.Opcode')
|
||||
cls.fields = fields
|
||||
|
||||
@classmethod
|
||||
def subclass(cls, clazz: type[_OP]) -> type[_OP]:
|
||||
clazz.name = f"ASE_{clazz.op_code.name.upper()}"
|
||||
clazz.fields = hci.HCI_Object.fields_from_dataclass(clazz)
|
||||
# Register a factory for this class
|
||||
ASE_Operation.classes[clazz.op_code] = clazz
|
||||
return clazz
|
||||
# Register a factory for this class
|
||||
ASE_Operation.classes[cls.op_code] = cls
|
||||
|
||||
@functools.cached_property
|
||||
def pdu(self) -> bytes:
|
||||
return bytes([self.op_code]) + hci.HCI_Object.dict_to_bytes(
|
||||
self.__dict__, self.fields
|
||||
)
|
||||
return cls
|
||||
|
||||
return inner
|
||||
|
||||
def __init__(self, pdu: Optional[bytes] = None, **kwargs) -> None:
|
||||
if self.fields is not None and kwargs:
|
||||
hci.HCI_Object.init_from_fields(self, self.fields, kwargs)
|
||||
if pdu is None:
|
||||
pdu = bytes([self.op_code]) + hci.HCI_Object.dict_to_bytes(
|
||||
kwargs, self.fields
|
||||
)
|
||||
self.pdu = pdu
|
||||
|
||||
def init_from_bytes(self, pdu: bytes, offset: int):
|
||||
return hci.HCI_Object.init_from_bytes(self, pdu, offset, self.fields)
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
return self.pdu
|
||||
@@ -101,128 +124,105 @@ class ASE_Operation:
|
||||
return result
|
||||
|
||||
|
||||
@ASE_Operation.subclass
|
||||
@dataclass
|
||||
@ASE_Operation.subclass(
|
||||
[
|
||||
[
|
||||
('ase_id', 1),
|
||||
('target_latency', 1),
|
||||
('target_phy', 1),
|
||||
('codec_id', hci.CodingFormat.parse_from_bytes),
|
||||
('codec_specific_configuration', 'v'),
|
||||
],
|
||||
]
|
||||
)
|
||||
class ASE_Config_Codec(ASE_Operation):
|
||||
'''
|
||||
See Audio Stream Control Service 5.1 - Config Codec Operation
|
||||
'''
|
||||
|
||||
op_code = ASE_Operation.Opcode.CONFIG_CODEC
|
||||
|
||||
ase_id: Sequence[int] = field(metadata=hci.metadata(1, list_begin=True))
|
||||
target_latency: Sequence[int] = field(metadata=hci.metadata(1))
|
||||
target_phy: Sequence[int] = field(metadata=hci.metadata(1))
|
||||
codec_id: Sequence[hci.CodingFormat] = field(
|
||||
metadata=hci.metadata(hci.CodingFormat.parse_from_bytes)
|
||||
)
|
||||
codec_specific_configuration: Sequence[bytes] = field(
|
||||
metadata=hci.metadata('v', list_end=True)
|
||||
)
|
||||
target_latency: List[int]
|
||||
target_phy: List[int]
|
||||
codec_id: List[hci.CodingFormat]
|
||||
codec_specific_configuration: List[bytes]
|
||||
|
||||
|
||||
@ASE_Operation.subclass
|
||||
@dataclass
|
||||
@ASE_Operation.subclass(
|
||||
[
|
||||
[
|
||||
('ase_id', 1),
|
||||
('cig_id', 1),
|
||||
('cis_id', 1),
|
||||
('sdu_interval', 3),
|
||||
('framing', 1),
|
||||
('phy', 1),
|
||||
('max_sdu', 2),
|
||||
('retransmission_number', 1),
|
||||
('max_transport_latency', 2),
|
||||
('presentation_delay', 3),
|
||||
],
|
||||
]
|
||||
)
|
||||
class ASE_Config_QOS(ASE_Operation):
|
||||
'''
|
||||
See Audio Stream Control Service 5.2 - Config Qos Operation
|
||||
'''
|
||||
|
||||
op_code = ASE_Operation.Opcode.CONFIG_QOS
|
||||
|
||||
ase_id: Sequence[int] = field(metadata=hci.metadata(1, list_begin=True))
|
||||
cig_id: Sequence[int] = field(metadata=hci.metadata(1))
|
||||
cis_id: Sequence[int] = field(metadata=hci.metadata(1))
|
||||
sdu_interval: Sequence[int] = field(metadata=hci.metadata(3))
|
||||
framing: Sequence[int] = field(metadata=hci.metadata(1))
|
||||
phy: Sequence[int] = field(metadata=hci.metadata(1))
|
||||
max_sdu: Sequence[int] = field(metadata=hci.metadata(2))
|
||||
retransmission_number: Sequence[int] = field(metadata=hci.metadata(1))
|
||||
max_transport_latency: Sequence[int] = field(metadata=hci.metadata(2))
|
||||
presentation_delay: Sequence[int] = field(metadata=hci.metadata(3, list_end=True))
|
||||
cig_id: List[int]
|
||||
cis_id: List[int]
|
||||
sdu_interval: List[int]
|
||||
framing: List[int]
|
||||
phy: List[int]
|
||||
max_sdu: List[int]
|
||||
retransmission_number: List[int]
|
||||
max_transport_latency: List[int]
|
||||
presentation_delay: List[int]
|
||||
|
||||
|
||||
@ASE_Operation.subclass
|
||||
@dataclass
|
||||
@ASE_Operation.subclass([[('ase_id', 1), ('metadata', 'v')]])
|
||||
class ASE_Enable(ASE_Operation):
|
||||
'''
|
||||
See Audio Stream Control Service 5.3 - Enable Operation
|
||||
'''
|
||||
|
||||
op_code = ASE_Operation.Opcode.ENABLE
|
||||
|
||||
ase_id: Sequence[int] = field(metadata=hci.metadata(1, list_begin=True))
|
||||
metadata: Sequence[bytes] = field(metadata=hci.metadata('v', list_end=True))
|
||||
metadata: bytes
|
||||
|
||||
|
||||
@ASE_Operation.subclass
|
||||
@dataclass
|
||||
@ASE_Operation.subclass([[('ase_id', 1)]])
|
||||
class ASE_Receiver_Start_Ready(ASE_Operation):
|
||||
'''
|
||||
See Audio Stream Control Service 5.4 - Receiver Start Ready Operation
|
||||
'''
|
||||
|
||||
op_code = ASE_Operation.Opcode.RECEIVER_START_READY
|
||||
|
||||
ase_id: Sequence[int] = field(
|
||||
metadata=hci.metadata(1, list_begin=True, list_end=True)
|
||||
)
|
||||
|
||||
|
||||
@ASE_Operation.subclass
|
||||
@dataclass
|
||||
@ASE_Operation.subclass([[('ase_id', 1)]])
|
||||
class ASE_Disable(ASE_Operation):
|
||||
'''
|
||||
See Audio Stream Control Service 5.5 - Disable Operation
|
||||
'''
|
||||
|
||||
op_code = ASE_Operation.Opcode.DISABLE
|
||||
|
||||
ase_id: Sequence[int] = field(
|
||||
metadata=hci.metadata(1, list_begin=True, list_end=True)
|
||||
)
|
||||
|
||||
|
||||
@ASE_Operation.subclass
|
||||
@dataclass
|
||||
@ASE_Operation.subclass([[('ase_id', 1)]])
|
||||
class ASE_Receiver_Stop_Ready(ASE_Operation):
|
||||
'''
|
||||
See Audio Stream Control Service 5.6 - Receiver Stop Ready Operation
|
||||
'''
|
||||
|
||||
op_code = ASE_Operation.Opcode.RECEIVER_STOP_READY
|
||||
|
||||
ase_id: Sequence[int] = field(
|
||||
metadata=hci.metadata(1, list_begin=True, list_end=True)
|
||||
)
|
||||
|
||||
|
||||
@ASE_Operation.subclass
|
||||
@dataclass
|
||||
@ASE_Operation.subclass([[('ase_id', 1), ('metadata', 'v')]])
|
||||
class ASE_Update_Metadata(ASE_Operation):
|
||||
'''
|
||||
See Audio Stream Control Service 5.7 - Update Metadata Operation
|
||||
'''
|
||||
|
||||
op_code = ASE_Operation.Opcode.UPDATE_METADATA
|
||||
|
||||
ase_id: Sequence[int] = field(metadata=hci.metadata(1, list_begin=True))
|
||||
metadata: Sequence[bytes] = field(metadata=hci.metadata('v', list_end=True))
|
||||
metadata: List[bytes]
|
||||
|
||||
|
||||
@ASE_Operation.subclass
|
||||
@dataclass
|
||||
@ASE_Operation.subclass([[('ase_id', 1)]])
|
||||
class ASE_Release(ASE_Operation):
|
||||
'''
|
||||
See Audio Stream Control Service 5.8 - Release Operation
|
||||
'''
|
||||
|
||||
op_code = ASE_Operation.Opcode.RELEASE
|
||||
|
||||
ase_id: Sequence[int] = field(
|
||||
metadata=hci.metadata(1, list_begin=True, list_end=True)
|
||||
)
|
||||
|
||||
|
||||
class AseResponseCode(enum.IntEnum):
|
||||
# fmt: off
|
||||
@@ -278,7 +278,7 @@ class AseStateMachine(gatt.Characteristic):
|
||||
|
||||
EVENT_STATE_CHANGE = "state_change"
|
||||
|
||||
cis_link: device.CisLink | None = None
|
||||
cis_link: Optional[device.CisLink] = None
|
||||
|
||||
# Additional parameters in CODEC_CONFIGURED State
|
||||
preferred_framing = 0 # Unframed PDU supported
|
||||
@@ -290,7 +290,7 @@ class AseStateMachine(gatt.Characteristic):
|
||||
preferred_presentation_delay_min = 0
|
||||
preferred_presentation_delay_max = 0
|
||||
codec_id = hci.CodingFormat(hci.CodecID.LC3)
|
||||
codec_specific_configuration: CodecSpecificConfiguration | bytes = b''
|
||||
codec_specific_configuration: Union[CodecSpecificConfiguration, bytes] = b''
|
||||
|
||||
# Additional parameters in QOS_CONFIGURED State
|
||||
cig_id = 0
|
||||
@@ -338,16 +338,22 @@ class AseStateMachine(gatt.Characteristic):
|
||||
self.service.device.EVENT_CIS_ESTABLISHMENT, self.on_cis_establishment
|
||||
)
|
||||
|
||||
def on_cis_request(self, cis_link: device.CisLink) -> None:
|
||||
def on_cis_request(
|
||||
self,
|
||||
acl_connection: device.Connection,
|
||||
cis_handle: int,
|
||||
cig_id: int,
|
||||
cis_id: int,
|
||||
) -> None:
|
||||
if (
|
||||
cis_link.cig_id == self.cig_id
|
||||
and cis_link.cis_id == self.cis_id
|
||||
cig_id == self.cig_id
|
||||
and cis_id == self.cis_id
|
||||
and self.state == self.State.ENABLING
|
||||
):
|
||||
utils.cancel_on_event(
|
||||
cis_link.acl_connection,
|
||||
acl_connection,
|
||||
'flush',
|
||||
self.service.device.accept_cis_request(cis_link),
|
||||
self.service.device.accept_cis_request(cis_handle),
|
||||
)
|
||||
|
||||
def on_cis_establishment(self, cis_link: device.CisLink) -> None:
|
||||
@@ -378,7 +384,7 @@ class AseStateMachine(gatt.Characteristic):
|
||||
target_phy: int,
|
||||
codec_id: hci.CodingFormat,
|
||||
codec_specific_configuration: bytes,
|
||||
) -> tuple[AseResponseCode, AseReasonCode]:
|
||||
) -> Tuple[AseResponseCode, AseReasonCode]:
|
||||
if self.state not in (
|
||||
self.State.IDLE,
|
||||
self.State.CODEC_CONFIGURED,
|
||||
@@ -414,7 +420,7 @@ class AseStateMachine(gatt.Characteristic):
|
||||
retransmission_number: int,
|
||||
max_transport_latency: int,
|
||||
presentation_delay: int,
|
||||
) -> tuple[AseResponseCode, AseReasonCode]:
|
||||
) -> Tuple[AseResponseCode, AseReasonCode]:
|
||||
if self.state not in (
|
||||
AseStateMachine.State.CODEC_CONFIGURED,
|
||||
AseStateMachine.State.QOS_CONFIGURED,
|
||||
@@ -438,7 +444,7 @@ class AseStateMachine(gatt.Characteristic):
|
||||
|
||||
return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
|
||||
|
||||
def on_enable(self, metadata: bytes) -> tuple[AseResponseCode, AseReasonCode]:
|
||||
def on_enable(self, metadata: bytes) -> Tuple[AseResponseCode, AseReasonCode]:
|
||||
if self.state != AseStateMachine.State.QOS_CONFIGURED:
|
||||
return (
|
||||
AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION,
|
||||
@@ -447,20 +453,10 @@ class AseStateMachine(gatt.Characteristic):
|
||||
|
||||
self.metadata = le_audio.Metadata.from_bytes(metadata)
|
||||
self.state = self.State.ENABLING
|
||||
# CIS could be established before enable.
|
||||
if cis_link := next(
|
||||
(
|
||||
cis_link
|
||||
for cis_link in self.service.device.cis_links.values()
|
||||
if cis_link.cig_id == self.cig_id and cis_link.cis_id == self.cis_id
|
||||
),
|
||||
None,
|
||||
):
|
||||
self.on_cis_establishment(cis_link)
|
||||
|
||||
return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
|
||||
|
||||
def on_receiver_start_ready(self) -> tuple[AseResponseCode, AseReasonCode]:
|
||||
def on_receiver_start_ready(self) -> Tuple[AseResponseCode, AseReasonCode]:
|
||||
if self.state != AseStateMachine.State.ENABLING:
|
||||
return (
|
||||
AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION,
|
||||
@@ -469,7 +465,7 @@ class AseStateMachine(gatt.Characteristic):
|
||||
self.state = self.State.STREAMING
|
||||
return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
|
||||
|
||||
def on_disable(self) -> tuple[AseResponseCode, AseReasonCode]:
|
||||
def on_disable(self) -> Tuple[AseResponseCode, AseReasonCode]:
|
||||
if self.state not in (
|
||||
AseStateMachine.State.ENABLING,
|
||||
AseStateMachine.State.STREAMING,
|
||||
@@ -484,7 +480,7 @@ class AseStateMachine(gatt.Characteristic):
|
||||
self.state = self.State.DISABLING
|
||||
return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
|
||||
|
||||
def on_receiver_stop_ready(self) -> tuple[AseResponseCode, AseReasonCode]:
|
||||
def on_receiver_stop_ready(self) -> Tuple[AseResponseCode, AseReasonCode]:
|
||||
if (
|
||||
self.role != AudioRole.SOURCE
|
||||
or self.state != AseStateMachine.State.DISABLING
|
||||
@@ -498,7 +494,7 @@ class AseStateMachine(gatt.Characteristic):
|
||||
|
||||
def on_update_metadata(
|
||||
self, metadata: bytes
|
||||
) -> tuple[AseResponseCode, AseReasonCode]:
|
||||
) -> Tuple[AseResponseCode, AseReasonCode]:
|
||||
if self.state not in (
|
||||
AseStateMachine.State.ENABLING,
|
||||
AseStateMachine.State.STREAMING,
|
||||
@@ -510,7 +506,7 @@ class AseStateMachine(gatt.Characteristic):
|
||||
self.metadata = le_audio.Metadata.from_bytes(metadata)
|
||||
return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
|
||||
|
||||
def on_release(self) -> tuple[AseResponseCode, AseReasonCode]:
|
||||
def on_release(self) -> Tuple[AseResponseCode, AseReasonCode]:
|
||||
if self.state == AseStateMachine.State.IDLE:
|
||||
return (
|
||||
AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION,
|
||||
@@ -520,7 +516,7 @@ class AseStateMachine(gatt.Characteristic):
|
||||
|
||||
async def remove_cis_async():
|
||||
if self.cis_link:
|
||||
await self.cis_link.remove_data_path([self.role])
|
||||
await self.cis_link.remove_data_path(self.role)
|
||||
self.state = self.State.IDLE
|
||||
await self.service.device.notify_subscribers(self, self.value)
|
||||
|
||||
@@ -594,7 +590,7 @@ class AseStateMachine(gatt.Characteristic):
|
||||
# Readonly. Do nothing in the setter.
|
||||
pass
|
||||
|
||||
def on_read(self, _: device.Connection) -> bytes:
|
||||
def on_read(self, _: Optional[device.Connection]) -> bytes:
|
||||
return self.value
|
||||
|
||||
def __str__(self) -> str:
|
||||
@@ -608,9 +604,9 @@ class AseStateMachine(gatt.Characteristic):
|
||||
class AudioStreamControlService(gatt.TemplateService):
|
||||
UUID = gatt.GATT_AUDIO_STREAM_CONTROL_SERVICE
|
||||
|
||||
ase_state_machines: dict[int, AseStateMachine]
|
||||
ase_state_machines: Dict[int, AseStateMachine]
|
||||
ase_control_point: gatt.Characteristic[bytes]
|
||||
_active_client: device.Connection | None = None
|
||||
_active_client: Optional[device.Connection] = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -653,9 +649,7 @@ class AudioStreamControlService(gatt.TemplateService):
|
||||
ase.state = AseStateMachine.State.IDLE
|
||||
self._active_client = None
|
||||
|
||||
def on_write_ase_control_point(
|
||||
self, connection: device.Connection, data: bytes
|
||||
) -> None:
|
||||
def on_write_ase_control_point(self, connection, data):
|
||||
if not self._active_client and connection:
|
||||
self._active_client = connection
|
||||
connection.once('disconnection', self._on_client_disconnected)
|
||||
@@ -664,7 +658,7 @@ class AudioStreamControlService(gatt.TemplateService):
|
||||
responses = []
|
||||
logger.debug(f'*** ASCS Write {operation} ***')
|
||||
|
||||
if isinstance(operation, ASE_Config_Codec):
|
||||
if operation.op_code == ASE_Operation.Opcode.CONFIG_CODEC:
|
||||
for ase_id, *args in zip(
|
||||
operation.ase_id,
|
||||
operation.target_latency,
|
||||
@@ -673,7 +667,7 @@ class AudioStreamControlService(gatt.TemplateService):
|
||||
operation.codec_specific_configuration,
|
||||
):
|
||||
responses.append(self.on_operation(operation.op_code, ase_id, args))
|
||||
elif isinstance(operation, ASE_Config_QOS):
|
||||
elif operation.op_code == ASE_Operation.Opcode.CONFIG_QOS:
|
||||
for ase_id, *args in zip(
|
||||
operation.ase_id,
|
||||
operation.cig_id,
|
||||
@@ -687,20 +681,20 @@ class AudioStreamControlService(gatt.TemplateService):
|
||||
operation.presentation_delay,
|
||||
):
|
||||
responses.append(self.on_operation(operation.op_code, ase_id, args))
|
||||
elif isinstance(operation, (ASE_Enable, ASE_Update_Metadata)):
|
||||
elif operation.op_code in (
|
||||
ASE_Operation.Opcode.ENABLE,
|
||||
ASE_Operation.Opcode.UPDATE_METADATA,
|
||||
):
|
||||
for ase_id, *args in zip(
|
||||
operation.ase_id,
|
||||
operation.metadata,
|
||||
):
|
||||
responses.append(self.on_operation(operation.op_code, ase_id, args))
|
||||
elif isinstance(
|
||||
operation,
|
||||
(
|
||||
ASE_Receiver_Start_Ready,
|
||||
ASE_Disable,
|
||||
ASE_Receiver_Stop_Ready,
|
||||
ASE_Release,
|
||||
),
|
||||
elif operation.op_code in (
|
||||
ASE_Operation.Opcode.RECEIVER_START_READY,
|
||||
ASE_Operation.Opcode.DISABLE,
|
||||
ASE_Operation.Opcode.RECEIVER_STOP_READY,
|
||||
ASE_Operation.Opcode.RELEASE,
|
||||
):
|
||||
for ase_id in operation.ase_id:
|
||||
responses.append(self.on_operation(operation.op_code, ase_id, []))
|
||||
@@ -729,8 +723,8 @@ class AudioStreamControlService(gatt.TemplateService):
|
||||
class AudioStreamControlServiceProxy(gatt_client.ProfileServiceProxy):
|
||||
SERVICE_CLASS = AudioStreamControlService
|
||||
|
||||
sink_ase: list[gatt_client.CharacteristicProxy[bytes]]
|
||||
source_ase: list[gatt_client.CharacteristicProxy[bytes]]
|
||||
sink_ase: List[gatt_client.CharacteristicProxy[bytes]]
|
||||
source_ase: List[gatt_client.CharacteristicProxy[bytes]]
|
||||
ase_control_point: gatt_client.CharacteristicProxy[bytes]
|
||||
|
||||
def __init__(self, service_proxy: gatt_client.ServiceProxy):
|
||||
|
||||
@@ -17,14 +17,16 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import enum
|
||||
import logging
|
||||
import struct
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
import logging
|
||||
from typing import List, Optional, Callable, Union, Any
|
||||
|
||||
from bumble import data_types, gatt, gatt_client, l2cap, utils
|
||||
from bumble import l2cap
|
||||
from bumble import utils
|
||||
from bumble import gatt
|
||||
from bumble import gatt_client
|
||||
from bumble.core import AdvertisingData
|
||||
from bumble.device import Connection, Device
|
||||
from bumble.device import Device, Connection
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -91,20 +93,20 @@ class AshaService(gatt.TemplateService):
|
||||
EVENT_DISCONNECTED = "disconnected"
|
||||
EVENT_VOLUME_CHANGED = "volume_changed"
|
||||
|
||||
audio_sink: Callable[[bytes], Any] | None
|
||||
active_codec: Codec | None = None
|
||||
audio_type: AudioType | None = None
|
||||
volume: int | None = None
|
||||
other_state: int | None = None
|
||||
connection: Connection | None = None
|
||||
audio_sink: Optional[Callable[[bytes], Any]]
|
||||
active_codec: Optional[Codec] = None
|
||||
audio_type: Optional[AudioType] = None
|
||||
volume: Optional[int] = None
|
||||
other_state: Optional[int] = None
|
||||
connection: Optional[Connection] = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
capability: int,
|
||||
hisyncid: list[int] | bytes,
|
||||
hisyncid: Union[List[int], bytes],
|
||||
device: Device,
|
||||
psm: int = 0,
|
||||
audio_sink: Callable[[bytes], Any] | None = None,
|
||||
audio_sink: Optional[Callable[[bytes], Any]] = None,
|
||||
feature_map: int = FeatureMap.LE_COC_AUDIO_OUTPUT_STREAMING_SUPPORTED,
|
||||
protocol_version: int = 0x01,
|
||||
render_delay_milliseconds: int = 0,
|
||||
@@ -186,18 +188,19 @@ class AshaService(gatt.TemplateService):
|
||||
return bytes(
|
||||
AdvertisingData(
|
||||
[
|
||||
data_types.ServiceData16BitUUID(
|
||||
gatt.GATT_ASHA_SERVICE,
|
||||
bytes([self.protocol_version, self.capability])
|
||||
(
|
||||
AdvertisingData.SERVICE_DATA_16_BIT_UUID,
|
||||
bytes(gatt.GATT_ASHA_SERVICE)
|
||||
+ bytes([self.protocol_version, self.capability])
|
||||
+ self.hisyncid[:4],
|
||||
)
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
# Handler for audio control commands
|
||||
async def _on_audio_control_point_write(
|
||||
self, connection: Connection, value: bytes
|
||||
self, connection: Optional[Connection], value: bytes
|
||||
) -> None:
|
||||
_logger.debug(f'--- AUDIO CONTROL POINT Write:{value.hex()}')
|
||||
opcode = value[0]
|
||||
@@ -244,7 +247,7 @@ class AshaService(gatt.TemplateService):
|
||||
)
|
||||
|
||||
# Handler for volume control
|
||||
def _on_volume_write(self, connection: Connection, value: bytes) -> None:
|
||||
def _on_volume_write(self, connection: Optional[Connection], value: bytes) -> None:
|
||||
_logger.debug(f'--- VOLUME Write:{value[0]}')
|
||||
self.volume = value[0]
|
||||
self.emit(self.EVENT_VOLUME_CHANGED)
|
||||
|
||||
@@ -18,18 +18,22 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
import dataclasses
|
||||
import enum
|
||||
import struct
|
||||
import functools
|
||||
import logging
|
||||
import struct
|
||||
from collections.abc import Sequence
|
||||
|
||||
from typing import List
|
||||
from typing_extensions import Self
|
||||
|
||||
from bumble import core, data_types, gatt, hci, utils
|
||||
from bumble import core
|
||||
from bumble import hci
|
||||
from bumble import gatt
|
||||
from bumble import utils
|
||||
from bumble.profiles import le_audio
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -257,10 +261,11 @@ class UnicastServerAdvertisingData:
|
||||
return bytes(
|
||||
core.AdvertisingData(
|
||||
[
|
||||
data_types.ServiceData16BitUUID(
|
||||
gatt.GATT_AUDIO_STREAM_CONTROL_SERVICE,
|
||||
(
|
||||
core.AdvertisingData.SERVICE_DATA_16_BIT_UUID,
|
||||
struct.pack(
|
||||
'<BIB',
|
||||
'<2sBIB',
|
||||
bytes(gatt.GATT_AUDIO_STREAM_CONTROL_SERVICE),
|
||||
self.announcement_type,
|
||||
self.available_audio_contexts,
|
||||
len(self.metadata),
|
||||
@@ -277,7 +282,7 @@ class UnicastServerAdvertisingData:
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def bits_to_channel_counts(data: int) -> list[int]:
|
||||
def bits_to_channel_counts(data: int) -> List[int]:
|
||||
pos = 0
|
||||
counts = []
|
||||
while data != 0:
|
||||
@@ -489,8 +494,12 @@ class BroadcastAudioAnnouncement:
|
||||
return bytes(
|
||||
core.AdvertisingData(
|
||||
[
|
||||
data_types.ServiceData16BitUUID(
|
||||
gatt.GATT_BROADCAST_AUDIO_ANNOUNCEMENT_SERVICE, bytes(self)
|
||||
(
|
||||
core.AdvertisingData.SERVICE_DATA_16_BIT_UUID,
|
||||
(
|
||||
bytes(gatt.GATT_BROADCAST_AUDIO_ANNOUNCEMENT_SERVICE)
|
||||
+ bytes(self)
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -518,7 +527,7 @@ class BasicAudioAnnouncement:
|
||||
codec_id: hci.CodingFormat
|
||||
codec_specific_configuration: CodecSpecificConfiguration
|
||||
metadata: le_audio.Metadata
|
||||
bis: list[BasicAudioAnnouncement.BIS]
|
||||
bis: List[BasicAudioAnnouncement.BIS]
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
metadata_bytes = bytes(self.metadata)
|
||||
@@ -536,7 +545,7 @@ class BasicAudioAnnouncement:
|
||||
)
|
||||
|
||||
presentation_delay: int
|
||||
subgroups: list[BasicAudioAnnouncement.Subgroup]
|
||||
subgroups: List[BasicAudioAnnouncement.Subgroup]
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data: bytes) -> Self:
|
||||
@@ -602,8 +611,12 @@ class BasicAudioAnnouncement:
|
||||
return bytes(
|
||||
core.AdvertisingData(
|
||||
[
|
||||
data_types.ServiceData16BitUUID(
|
||||
gatt.GATT_BASIC_AUDIO_ANNOUNCEMENT_SERVICE, bytes(self)
|
||||
(
|
||||
core.AdvertisingData.SERVICE_DATA_16_BIT_UUID,
|
||||
(
|
||||
bytes(gatt.GATT_BASIC_AUDIO_ANNOUNCEMENT_SERVICE)
|
||||
+ bytes(self)
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
@@ -17,14 +17,18 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import logging
|
||||
import struct
|
||||
from collections.abc import Sequence
|
||||
from typing import ClassVar
|
||||
from typing import ClassVar, Optional, Sequence
|
||||
|
||||
from bumble import core, device, gatt, gatt_adapters, gatt_client, hci, utils
|
||||
from bumble import core
|
||||
from bumble import device
|
||||
from bumble import gatt
|
||||
from bumble import gatt_adapters
|
||||
from bumble import gatt_client
|
||||
from bumble import hci
|
||||
from bumble import utils
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -338,12 +342,7 @@ class BroadcastAudioScanService(gatt.TemplateService):
|
||||
b"12", # TEST
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
[
|
||||
self.broadcast_audio_scan_control_point_characteristic,
|
||||
self.broadcast_receive_state_characteristic,
|
||||
]
|
||||
)
|
||||
super().__init__([self.battery_level_characteristic])
|
||||
|
||||
def on_broadcast_audio_scan_control_point_write(
|
||||
self, connection: device.Connection, value: bytes
|
||||
@@ -357,7 +356,7 @@ class BroadcastAudioScanServiceProxy(gatt_client.ProfileServiceProxy):
|
||||
|
||||
broadcast_audio_scan_control_point: gatt_client.CharacteristicProxy[bytes]
|
||||
broadcast_receive_states: list[
|
||||
gatt_client.CharacteristicProxy[BroadcastReceiveState | None]
|
||||
gatt_client.CharacteristicProxy[Optional[BroadcastReceiveState]]
|
||||
]
|
||||
|
||||
def __init__(self, service_proxy: gatt_client.ServiceProxy):
|
||||
|
||||
@@ -16,19 +16,21 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from typing import Optional
|
||||
|
||||
from bumble.gatt_client import ProfileServiceProxy
|
||||
from bumble.gatt import (
|
||||
GATT_BATTERY_LEVEL_CHARACTERISTIC,
|
||||
GATT_BATTERY_SERVICE,
|
||||
GATT_BATTERY_LEVEL_CHARACTERISTIC,
|
||||
TemplateService,
|
||||
Characteristic,
|
||||
CharacteristicValue,
|
||||
TemplateService,
|
||||
)
|
||||
from bumble.gatt_client import CharacteristicProxy
|
||||
from bumble.gatt_adapters import (
|
||||
PackedCharacteristicAdapter,
|
||||
PackedCharacteristicProxyAdapter,
|
||||
)
|
||||
from bumble.gatt_client import CharacteristicProxy, ProfileServiceProxy
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -55,7 +57,7 @@ class BatteryService(TemplateService):
|
||||
class BatteryServiceProxy(ProfileServiceProxy):
|
||||
SERVICE_CLASS = BatteryService
|
||||
|
||||
battery_level: CharacteristicProxy[int] | None
|
||||
battery_level: Optional[CharacteristicProxy[int]]
|
||||
|
||||
def __init__(self, service_proxy):
|
||||
self.service_proxy = service_proxy
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
from bumble import gatt, gatt_client
|
||||
from bumble import gatt
|
||||
from bumble import gatt_client
|
||||
from bumble.profiles import csip
|
||||
|
||||
|
||||
|
||||
@@ -17,11 +17,16 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
import struct
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from bumble import core
|
||||
from bumble import crypto
|
||||
from bumble import device
|
||||
from bumble import gatt
|
||||
from bumble import gatt_client
|
||||
|
||||
from bumble import core, crypto, device, gatt, gatt_client
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
@@ -95,17 +100,17 @@ class CoordinatedSetIdentificationService(gatt.TemplateService):
|
||||
|
||||
set_identity_resolving_key: bytes
|
||||
set_identity_resolving_key_characteristic: gatt.Characteristic[bytes]
|
||||
coordinated_set_size_characteristic: gatt.Characteristic[bytes] | None = None
|
||||
set_member_lock_characteristic: gatt.Characteristic[bytes] | None = None
|
||||
set_member_rank_characteristic: gatt.Characteristic[bytes] | None = None
|
||||
coordinated_set_size_characteristic: Optional[gatt.Characteristic[bytes]] = None
|
||||
set_member_lock_characteristic: Optional[gatt.Characteristic[bytes]] = None
|
||||
set_member_rank_characteristic: Optional[gatt.Characteristic[bytes]] = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
set_identity_resolving_key: bytes,
|
||||
set_identity_resolving_key_type: SirkType,
|
||||
coordinated_set_size: int | None = None,
|
||||
set_member_lock: MemberLock | None = None,
|
||||
set_member_rank: int | None = None,
|
||||
coordinated_set_size: Optional[int] = None,
|
||||
set_member_lock: Optional[MemberLock] = None,
|
||||
set_member_rank: Optional[int] = None,
|
||||
) -> None:
|
||||
if len(set_identity_resolving_key) != SET_IDENTITY_RESOLVING_KEY_LENGTH:
|
||||
raise core.InvalidArgumentError(
|
||||
@@ -159,10 +164,12 @@ class CoordinatedSetIdentificationService(gatt.TemplateService):
|
||||
|
||||
super().__init__(characteristics)
|
||||
|
||||
async def on_sirk_read(self, connection: device.Connection) -> bytes:
|
||||
async def on_sirk_read(self, connection: Optional[device.Connection]) -> bytes:
|
||||
if self.set_identity_resolving_key_type == SirkType.PLAINTEXT:
|
||||
sirk_bytes = self.set_identity_resolving_key
|
||||
else:
|
||||
assert connection
|
||||
|
||||
if connection.transport == core.PhysicalTransport.LE:
|
||||
key = await connection.device.get_long_term_key(
|
||||
connection_handle=connection.handle, rand=b'', ediv=0
|
||||
@@ -197,9 +204,9 @@ class CoordinatedSetIdentificationProxy(gatt_client.ProfileServiceProxy):
|
||||
SERVICE_CLASS = CoordinatedSetIdentificationService
|
||||
|
||||
set_identity_resolving_key: gatt_client.CharacteristicProxy[bytes]
|
||||
coordinated_set_size: gatt_client.CharacteristicProxy[bytes] | None = None
|
||||
set_member_lock: gatt_client.CharacteristicProxy[bytes] | None = None
|
||||
set_member_rank: gatt_client.CharacteristicProxy[bytes] | None = None
|
||||
coordinated_set_size: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
set_member_lock: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
set_member_rank: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
|
||||
def __init__(self, service_proxy: gatt_client.ServiceProxy) -> None:
|
||||
self.service_proxy = service_proxy
|
||||
@@ -223,7 +230,7 @@ class CoordinatedSetIdentificationProxy(gatt_client.ProfileServiceProxy):
|
||||
):
|
||||
self.set_member_rank = characteristics[0]
|
||||
|
||||
async def read_set_identity_resolving_key(self) -> tuple[SirkType, bytes]:
|
||||
async def read_set_identity_resolving_key(self) -> Tuple[SirkType, bytes]:
|
||||
'''Reads SIRK and decrypts if encrypted.'''
|
||||
response = await self.set_identity_resolving_key.read_value()
|
||||
if len(response) != SET_IDENTITY_RESOLVING_KEY_LENGTH + 1:
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import struct
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from bumble.gatt import (
|
||||
GATT_DEVICE_INFORMATION_SERVICE,
|
||||
@@ -24,12 +25,12 @@ from bumble.gatt import (
|
||||
GATT_HARDWARE_REVISION_STRING_CHARACTERISTIC,
|
||||
GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC,
|
||||
GATT_MODEL_NUMBER_STRING_CHARACTERISTIC,
|
||||
GATT_REGULATORY_CERTIFICATION_DATA_LIST_CHARACTERISTIC,
|
||||
GATT_SERIAL_NUMBER_STRING_CHARACTERISTIC,
|
||||
GATT_SOFTWARE_REVISION_STRING_CHARACTERISTIC,
|
||||
GATT_SYSTEM_ID_CHARACTERISTIC,
|
||||
Characteristic,
|
||||
GATT_REGULATORY_CERTIFICATION_DATA_LIST_CHARACTERISTIC,
|
||||
TemplateService,
|
||||
Characteristic,
|
||||
)
|
||||
from bumble.gatt_adapters import (
|
||||
DelegatedCharacteristicProxyAdapter,
|
||||
@@ -53,14 +54,14 @@ class DeviceInformationService(TemplateService):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
manufacturer_name: str | None = None,
|
||||
model_number: str | None = None,
|
||||
serial_number: str | None = None,
|
||||
hardware_revision: str | None = None,
|
||||
firmware_revision: str | None = None,
|
||||
software_revision: str | None = None,
|
||||
system_id: tuple[int, int] | None = None, # (OUI, Manufacturer ID)
|
||||
ieee_regulatory_certification_data_list: bytes | None = None,
|
||||
manufacturer_name: Optional[str] = None,
|
||||
model_number: Optional[str] = None,
|
||||
serial_number: Optional[str] = None,
|
||||
hardware_revision: Optional[str] = None,
|
||||
firmware_revision: Optional[str] = None,
|
||||
software_revision: Optional[str] = None,
|
||||
system_id: Optional[Tuple[int, int]] = None, # (OUI, Manufacturer ID)
|
||||
ieee_regulatory_certification_data_list: Optional[bytes] = None,
|
||||
# TODO: pnp_id
|
||||
):
|
||||
characteristics: list[Characteristic[bytes]] = [
|
||||
@@ -108,14 +109,14 @@ class DeviceInformationService(TemplateService):
|
||||
class DeviceInformationServiceProxy(ProfileServiceProxy):
|
||||
SERVICE_CLASS = DeviceInformationService
|
||||
|
||||
manufacturer_name: CharacteristicProxy[str] | None
|
||||
model_number: CharacteristicProxy[str] | None
|
||||
serial_number: CharacteristicProxy[str] | None
|
||||
hardware_revision: CharacteristicProxy[str] | None
|
||||
firmware_revision: CharacteristicProxy[str] | None
|
||||
software_revision: CharacteristicProxy[str] | None
|
||||
system_id: CharacteristicProxy[tuple[int, int]] | None
|
||||
ieee_regulatory_certification_data_list: CharacteristicProxy[bytes] | None
|
||||
manufacturer_name: Optional[CharacteristicProxy[str]]
|
||||
model_number: Optional[CharacteristicProxy[str]]
|
||||
serial_number: Optional[CharacteristicProxy[str]]
|
||||
hardware_revision: Optional[CharacteristicProxy[str]]
|
||||
firmware_revision: Optional[CharacteristicProxy[str]]
|
||||
software_revision: Optional[CharacteristicProxy[str]]
|
||||
system_id: Optional[CharacteristicProxy[tuple[int, int]]]
|
||||
ieee_regulatory_certification_data_list: Optional[CharacteristicProxy[bytes]]
|
||||
|
||||
def __init__(self, service_proxy: ServiceProxy):
|
||||
self.service_proxy = service_proxy
|
||||
|
||||
@@ -19,14 +19,15 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
import logging
|
||||
import struct
|
||||
from typing import Optional, Tuple, Union
|
||||
|
||||
from bumble.core import Appearance
|
||||
from bumble.gatt import (
|
||||
GATT_APPEARANCE_CHARACTERISTIC,
|
||||
GATT_DEVICE_NAME_CHARACTERISTIC,
|
||||
GATT_GENERIC_ACCESS_SERVICE,
|
||||
Characteristic,
|
||||
TemplateService,
|
||||
Characteristic,
|
||||
GATT_GENERIC_ACCESS_SERVICE,
|
||||
GATT_DEVICE_NAME_CHARACTERISTIC,
|
||||
GATT_APPEARANCE_CHARACTERISTIC,
|
||||
)
|
||||
from bumble.gatt_adapters import (
|
||||
DelegatedCharacteristicProxyAdapter,
|
||||
@@ -53,7 +54,7 @@ class GenericAccessService(TemplateService):
|
||||
appearance_characteristic: Characteristic[bytes]
|
||||
|
||||
def __init__(
|
||||
self, device_name: str, appearance: Appearance | tuple[int, int] | int = 0
|
||||
self, device_name: str, appearance: Union[Appearance, Tuple[int, int], int] = 0
|
||||
):
|
||||
if isinstance(appearance, int):
|
||||
appearance_int = appearance
|
||||
@@ -87,8 +88,8 @@ class GenericAccessService(TemplateService):
|
||||
class GenericAccessServiceProxy(ProfileServiceProxy):
|
||||
SERVICE_CLASS = GenericAccessService
|
||||
|
||||
device_name: CharacteristicProxy[str] | None
|
||||
appearance: CharacteristicProxy[Appearance] | None
|
||||
device_name: Optional[CharacteristicProxy[str]]
|
||||
appearance: Optional[CharacteristicProxy[Appearance]]
|
||||
|
||||
def __init__(self, service_proxy: ServiceProxy):
|
||||
self.service_proxy = service_proxy
|
||||
|
||||
@@ -17,7 +17,10 @@ from __future__ import annotations
|
||||
import struct
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bumble import att, crypto, gatt, gatt_client
|
||||
from bumble import att
|
||||
from bumble import gatt
|
||||
from bumble import gatt_client
|
||||
from bumble import crypto
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bumble import device
|
||||
@@ -40,6 +43,7 @@ class GenericAttributeProfileService(gatt.TemplateService):
|
||||
database_hash_enabled: bool = True,
|
||||
service_change_enabled: bool = True,
|
||||
) -> None:
|
||||
|
||||
if server_supported_features is not None:
|
||||
self.server_supported_features_characteristic = gatt.Characteristic(
|
||||
uuid=gatt.GATT_SERVER_SUPPORTED_FEATURES_CHARACTERISTIC,
|
||||
@@ -123,7 +127,9 @@ class GenericAttributeProfileService(gatt.TemplateService):
|
||||
|
||||
return b''
|
||||
|
||||
def get_database_hash(self, connection: device.Connection) -> bytes:
|
||||
def get_database_hash(self, connection: device.Connection | None) -> bytes:
|
||||
assert connection
|
||||
|
||||
m = b''.join(
|
||||
[
|
||||
self.get_attribute_data(attribute)
|
||||
|
||||
@@ -18,20 +18,21 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import struct
|
||||
from enum import IntFlag
|
||||
from typing import Optional
|
||||
|
||||
from bumble.gatt import (
|
||||
GATT_BGR_FEATURES_CHARACTERISTIC,
|
||||
GATT_BGS_FEATURES_CHARACTERISTIC,
|
||||
TemplateService,
|
||||
Characteristic,
|
||||
GATT_GAMING_AUDIO_SERVICE,
|
||||
GATT_GMAP_ROLE_CHARACTERISTIC,
|
||||
GATT_UGG_FEATURES_CHARACTERISTIC,
|
||||
GATT_UGT_FEATURES_CHARACTERISTIC,
|
||||
Characteristic,
|
||||
TemplateService,
|
||||
GATT_BGS_FEATURES_CHARACTERISTIC,
|
||||
GATT_BGR_FEATURES_CHARACTERISTIC,
|
||||
)
|
||||
from bumble.gatt_adapters import DelegatedCharacteristicProxyAdapter
|
||||
from bumble.gatt_client import CharacteristicProxy, ProfileServiceProxy, ServiceProxy
|
||||
from enum import IntFlag
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -76,18 +77,18 @@ class GamingAudioService(TemplateService):
|
||||
UUID = GATT_GAMING_AUDIO_SERVICE
|
||||
|
||||
gmap_role: Characteristic
|
||||
ugg_features: Characteristic | None = None
|
||||
ugt_features: Characteristic | None = None
|
||||
bgs_features: Characteristic | None = None
|
||||
bgr_features: Characteristic | None = None
|
||||
ugg_features: Optional[Characteristic] = None
|
||||
ugt_features: Optional[Characteristic] = None
|
||||
bgs_features: Optional[Characteristic] = None
|
||||
bgr_features: Optional[Characteristic] = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
gmap_role: GmapRole,
|
||||
ugg_features: UggFeatures | None = None,
|
||||
ugt_features: UgtFeatures | None = None,
|
||||
bgs_features: BgsFeatures | None = None,
|
||||
bgr_features: BgrFeatures | None = None,
|
||||
ugg_features: Optional[UggFeatures] = None,
|
||||
ugt_features: Optional[UgtFeatures] = None,
|
||||
bgs_features: Optional[BgsFeatures] = None,
|
||||
bgr_features: Optional[BgrFeatures] = None,
|
||||
) -> None:
|
||||
characteristics = []
|
||||
|
||||
@@ -149,10 +150,10 @@ class GamingAudioService(TemplateService):
|
||||
class GamingAudioServiceProxy(ProfileServiceProxy):
|
||||
SERVICE_CLASS = GamingAudioService
|
||||
|
||||
ugg_features: CharacteristicProxy[UggFeatures] | None = None
|
||||
ugt_features: CharacteristicProxy[UgtFeatures] | None = None
|
||||
bgs_features: CharacteristicProxy[BgsFeatures] | None = None
|
||||
bgr_features: CharacteristicProxy[BgrFeatures] | None = None
|
||||
ugg_features: Optional[CharacteristicProxy[UggFeatures]] = None
|
||||
ugt_features: Optional[CharacteristicProxy[UgtFeatures]] = None
|
||||
bgs_features: Optional[CharacteristicProxy[BgsFeatures]] = None
|
||||
bgr_features: Optional[CharacteristicProxy[BgrFeatures]] = None
|
||||
|
||||
def __init__(self, service_proxy: ServiceProxy) -> None:
|
||||
self.service_proxy = service_proxy
|
||||
|
||||
@@ -16,15 +16,16 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import functools
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional, Set, Union
|
||||
|
||||
from bumble import att, gatt, gatt_adapters, gatt_client, utils
|
||||
from bumble import att, gatt, gatt_adapters, gatt_client
|
||||
from bumble.core import InvalidArgumentError, InvalidStateError
|
||||
from bumble.device import Connection, Device
|
||||
from bumble.device import Device, Connection
|
||||
from bumble import utils
|
||||
from bumble.hci import Address
|
||||
|
||||
|
||||
@@ -145,7 +146,7 @@ class PresetChangedOperation:
|
||||
return bytes([self.prev_index]) + bytes(self.preset_record)
|
||||
|
||||
change_id: ChangeId
|
||||
additional_parameters: Generic | int
|
||||
additional_parameters: Union[Generic, int]
|
||||
|
||||
def to_bytes(self, is_last: bool) -> bytes:
|
||||
if isinstance(self.additional_parameters, PresetChangedOperation.Generic):
|
||||
@@ -227,25 +228,23 @@ class HearingAccessService(gatt.TemplateService):
|
||||
hearing_aid_preset_control_point: gatt.Characteristic[bytes]
|
||||
active_preset_index_characteristic: gatt.Characteristic[bytes]
|
||||
active_preset_index: int
|
||||
active_preset_index_per_device: dict[Address, int]
|
||||
active_preset_index_per_device: Dict[Address, int]
|
||||
|
||||
device: Device
|
||||
|
||||
server_features: HearingAidFeatures
|
||||
preset_records: dict[int, PresetRecord] # key is the preset index
|
||||
preset_records: Dict[int, PresetRecord] # key is the preset index
|
||||
read_presets_request_in_progress: bool
|
||||
|
||||
other_server_in_binaural_set: HearingAccessService | None = None
|
||||
|
||||
preset_changed_operations_history_per_device: dict[
|
||||
Address, list[PresetChangedOperation]
|
||||
preset_changed_operations_history_per_device: Dict[
|
||||
Address, List[PresetChangedOperation]
|
||||
]
|
||||
|
||||
# Keep an updated list of connected client to send notification to
|
||||
currently_connected_clients: set[Connection]
|
||||
currently_connected_clients: Set[Connection]
|
||||
|
||||
def __init__(
|
||||
self, device: Device, features: HearingAidFeatures, presets: list[PresetRecord]
|
||||
self, device: Device, features: HearingAidFeatures, presets: List[PresetRecord]
|
||||
) -> None:
|
||||
self.active_preset_index_per_device = {}
|
||||
self.read_presets_request_in_progress = False
|
||||
@@ -271,21 +270,14 @@ class HearingAccessService(gatt.TemplateService):
|
||||
def on_connection(connection: Connection) -> None:
|
||||
@connection.on(connection.EVENT_DISCONNECTION)
|
||||
def on_disconnection(_reason) -> None:
|
||||
self.currently_connected_clients.discard(connection)
|
||||
|
||||
@connection.on(connection.EVENT_CONNECTION_ATT_MTU_UPDATE)
|
||||
def on_mtu_update(*_: Any) -> None:
|
||||
self.on_incoming_connection(connection)
|
||||
|
||||
@connection.on(connection.EVENT_CONNECTION_ENCRYPTION_CHANGE)
|
||||
def on_encryption_change(*_: Any) -> None:
|
||||
self.on_incoming_connection(connection)
|
||||
self.currently_connected_clients.remove(connection)
|
||||
|
||||
@connection.on(connection.EVENT_PAIRING)
|
||||
def on_pairing(*_: Any) -> None:
|
||||
self.on_incoming_connection(connection)
|
||||
self.on_incoming_paired_connection(connection)
|
||||
|
||||
self.on_incoming_connection(connection)
|
||||
if connection.peer_resolvable_address:
|
||||
self.on_incoming_paired_connection(connection)
|
||||
|
||||
self.hearing_aid_features_characteristic = gatt.Characteristic(
|
||||
uuid=gatt.GATT_HEARING_AID_FEATURES_CHARACTERISTIC,
|
||||
@@ -322,30 +314,9 @@ class HearingAccessService(gatt.TemplateService):
|
||||
]
|
||||
)
|
||||
|
||||
def on_incoming_connection(self, connection: Connection):
|
||||
def on_incoming_paired_connection(self, connection: Connection):
|
||||
'''Setup initial operations to handle a remote bonded HAP device'''
|
||||
# TODO Should we filter on HAP device only ?
|
||||
|
||||
if not connection.is_encrypted:
|
||||
logging.debug(f'HAS: {connection.peer_address} is not encrypted')
|
||||
return
|
||||
|
||||
if not connection.peer_resolvable_address:
|
||||
logging.debug(f'HAS: {connection.peer_address} is not paired')
|
||||
return
|
||||
|
||||
if connection.att_mtu < 49:
|
||||
logging.debug(
|
||||
f'HAS: {connection.peer_address} invalid MTU={connection.att_mtu}'
|
||||
)
|
||||
return
|
||||
|
||||
if connection.peer_address in self.currently_connected_clients:
|
||||
logging.debug(
|
||||
f'HAS: Already connected to {connection.peer_address} nothing to do'
|
||||
)
|
||||
return
|
||||
|
||||
self.currently_connected_clients.add(connection)
|
||||
if (
|
||||
connection.peer_address
|
||||
@@ -362,10 +333,11 @@ class HearingAccessService(gatt.TemplateService):
|
||||
# Update the active preset index if needed
|
||||
await self.notify_active_preset_for_connection(connection)
|
||||
|
||||
connection.cancel_on_disconnection(on_connection_async())
|
||||
utils.cancel_on_event(connection, 'disconnection', on_connection_async())
|
||||
|
||||
def _on_read_active_preset_index(self, connection: Connection) -> bytes:
|
||||
del connection # Unused
|
||||
def _on_read_active_preset_index(
|
||||
self, __connection__: Optional[Connection]
|
||||
) -> bytes:
|
||||
return bytes([self.active_preset_index])
|
||||
|
||||
# TODO this need to be triggered when device is unbonded
|
||||
@@ -373,13 +345,18 @@ class HearingAccessService(gatt.TemplateService):
|
||||
self.preset_changed_operations_history_per_device.pop(addr)
|
||||
|
||||
async def _on_write_hearing_aid_preset_control_point(
|
||||
self, connection: Connection, value: bytes
|
||||
self, connection: Optional[Connection], value: bytes
|
||||
):
|
||||
assert connection
|
||||
|
||||
opcode = HearingAidPresetControlPointOpcode(value[0])
|
||||
handler = getattr(self, '_on_' + opcode.name.lower())
|
||||
await handler(connection, value)
|
||||
|
||||
async def _on_read_presets_request(self, connection: Connection, value: bytes):
|
||||
async def _on_read_presets_request(
|
||||
self, connection: Optional[Connection], value: bytes
|
||||
):
|
||||
assert connection
|
||||
if connection.att_mtu < 49: # 2.5. GATT sub-procedure requirements
|
||||
logging.warning(f'HAS require MTU >= 49: {connection}')
|
||||
|
||||
@@ -400,19 +377,17 @@ class HearingAccessService(gatt.TemplateService):
|
||||
self.preset_records[key]
|
||||
for key in sorted(self.preset_records.keys())
|
||||
if self.preset_records[key].index >= start_index
|
||||
][:num_presets]
|
||||
]
|
||||
del presets[num_presets:]
|
||||
if len(presets) == 0:
|
||||
raise att.ATT_Error(att.ErrorCode.OUT_OF_RANGE)
|
||||
|
||||
utils.AsyncRunner.spawn(self._read_preset_response(connection, presets))
|
||||
|
||||
async def _read_preset_response(
|
||||
self, connection: Connection, presets: list[PresetRecord]
|
||||
self, connection: Connection, presets: List[PresetRecord]
|
||||
):
|
||||
# If the ATT bearer is terminated before all notifications or indications are
|
||||
# sent, then the server shall consider the Read Presets Request operation
|
||||
# aborted and shall not either continue or restart the operation when the client
|
||||
# reconnects.
|
||||
# If the ATT bearer is terminated before all notifications or indications are sent, then the server shall consider the Read Presets Request operation aborted and shall not either continue or restart the operation when the client reconnects.
|
||||
try:
|
||||
for i, preset in enumerate(presets):
|
||||
await connection.device.indicate_subscriber(
|
||||
@@ -433,7 +408,7 @@ class HearingAccessService(gatt.TemplateService):
|
||||
|
||||
async def generic_update(self, op: PresetChangedOperation) -> None:
|
||||
'''Server API to perform a generic update. It is the responsibility of the caller to modify the preset_records to match the PresetChangedOperation being sent'''
|
||||
await self._notify_preset_operations(op)
|
||||
await self._notifyPresetOperations(op)
|
||||
|
||||
async def delete_preset(self, index: int) -> None:
|
||||
'''Server API to delete a preset. It should not be the current active preset'''
|
||||
@@ -442,14 +417,14 @@ class HearingAccessService(gatt.TemplateService):
|
||||
raise InvalidStateError('Cannot delete active preset')
|
||||
|
||||
del self.preset_records[index]
|
||||
await self._notify_preset_operations(PresetChangedOperationDeleted(index))
|
||||
await self._notifyPresetOperations(PresetChangedOperationDeleted(index))
|
||||
|
||||
async def available_preset(self, index: int) -> None:
|
||||
'''Server API to make a preset available'''
|
||||
|
||||
preset = self.preset_records[index]
|
||||
preset.properties.is_available = PresetRecord.Property.IsAvailable.IS_AVAILABLE
|
||||
await self._notify_preset_operations(PresetChangedOperationAvailable(index))
|
||||
await self._notifyPresetOperations(PresetChangedOperationAvailable(index))
|
||||
|
||||
async def unavailable_preset(self, index: int) -> None:
|
||||
'''Server API to make a preset unavailable. It should not be the current active preset'''
|
||||
@@ -461,7 +436,7 @@ class HearingAccessService(gatt.TemplateService):
|
||||
preset.properties.is_available = (
|
||||
PresetRecord.Property.IsAvailable.IS_UNAVAILABLE
|
||||
)
|
||||
await self._notify_preset_operations(PresetChangedOperationUnavailable(index))
|
||||
await self._notifyPresetOperations(PresetChangedOperationUnavailable(index))
|
||||
|
||||
async def _preset_changed_operation(self, connection: Connection) -> None:
|
||||
'''Send all PresetChangedOperation saved for a given connection'''
|
||||
@@ -476,31 +451,30 @@ class HearingAccessService(gatt.TemplateService):
|
||||
return op.additional_parameters
|
||||
|
||||
op_list.sort(key=get_op_index)
|
||||
# If the ATT bearer is terminated before all notifications or indications are
|
||||
# sent, then the server shall consider the Preset Changed operation aborted and
|
||||
# shall continue the operation when the client reconnects.
|
||||
while op_list:
|
||||
# If the ATT bearer is terminated before all notifications or indications are sent, then the server shall consider the Preset Changed operation aborted and shall continue the operation when the client reconnects.
|
||||
while len(op_list) > 0:
|
||||
try:
|
||||
await connection.device.indicate_subscriber(
|
||||
connection,
|
||||
self.hearing_aid_preset_control_point,
|
||||
value=op_list[0].to_bytes(len(op_list) == 1),
|
||||
force=True, # TODO GATT notification subscription should be persistent
|
||||
)
|
||||
# Remove item once sent, and keep the non sent item in the list
|
||||
op_list.pop(0)
|
||||
except TimeoutError:
|
||||
break
|
||||
|
||||
async def _notify_preset_operations(self, op: PresetChangedOperation) -> None:
|
||||
for history_list in self.preset_changed_operations_history_per_device.values():
|
||||
history_list.append(op)
|
||||
async def _notifyPresetOperations(self, op: PresetChangedOperation) -> None:
|
||||
for historyList in self.preset_changed_operations_history_per_device.values():
|
||||
historyList.append(op)
|
||||
|
||||
for connection in self.currently_connected_clients:
|
||||
await self._preset_changed_operation(connection)
|
||||
|
||||
async def _on_write_preset_name(self, connection: Connection, value: bytes):
|
||||
del connection # Unused
|
||||
async def _on_write_preset_name(
|
||||
self, connection: Optional[Connection], value: bytes
|
||||
):
|
||||
assert connection
|
||||
|
||||
if self.read_presets_request_in_progress:
|
||||
raise att.ATT_Error(att.ErrorCode.PROCEDURE_ALREADY_IN_PROGRESS)
|
||||
@@ -548,7 +522,10 @@ class HearingAccessService(gatt.TemplateService):
|
||||
for connection in self.currently_connected_clients:
|
||||
await self.notify_active_preset_for_connection(connection)
|
||||
|
||||
async def set_active_preset(self, value: bytes) -> None:
|
||||
async def set_active_preset(
|
||||
self, connection: Optional[Connection], value: bytes
|
||||
) -> None:
|
||||
assert connection
|
||||
index = value[1]
|
||||
preset = self.preset_records.get(index, None)
|
||||
if (
|
||||
@@ -565,85 +542,86 @@ class HearingAccessService(gatt.TemplateService):
|
||||
self.active_preset_index = index
|
||||
await self.notify_active_preset()
|
||||
|
||||
async def _on_set_active_preset(self, connection: Connection, value: bytes):
|
||||
del connection # Unused
|
||||
await self.set_active_preset(value)
|
||||
async def _on_set_active_preset(
|
||||
self, connection: Optional[Connection], value: bytes
|
||||
):
|
||||
await self.set_active_preset(connection, value)
|
||||
|
||||
async def set_next_or_previous_preset(self, is_previous: bool) -> None:
|
||||
async def set_next_or_previous_preset(
|
||||
self, connection: Optional[Connection], is_previous
|
||||
):
|
||||
'''Set the next or the previous preset as active'''
|
||||
assert connection
|
||||
|
||||
if self.active_preset_index == 0x00:
|
||||
raise att.ATT_Error(ErrorCode.PRESET_OPERATION_NOT_POSSIBLE)
|
||||
|
||||
presets = sorted(
|
||||
[
|
||||
record
|
||||
for record in self.preset_records.values()
|
||||
if record.is_available()
|
||||
],
|
||||
key=lambda record: record.index,
|
||||
)
|
||||
current_preset = self.preset_records[self.active_preset_index]
|
||||
current_preset_pos = presets.index(current_preset)
|
||||
if is_previous:
|
||||
new_preset = presets[(current_preset_pos - 1) % len(presets)]
|
||||
else:
|
||||
new_preset = presets[(current_preset_pos + 1) % len(presets)]
|
||||
first_preset: Optional[PresetRecord] = None # To loop to first preset
|
||||
next_preset: Optional[PresetRecord] = None
|
||||
for index, record in sorted(self.preset_records.items(), reverse=is_previous):
|
||||
if not record.is_available():
|
||||
continue
|
||||
if first_preset == None:
|
||||
first_preset = record
|
||||
if is_previous:
|
||||
if index >= self.active_preset_index:
|
||||
continue
|
||||
elif index <= self.active_preset_index:
|
||||
continue
|
||||
next_preset = record
|
||||
break
|
||||
|
||||
if current_preset == new_preset: # If no other preset are available
|
||||
if not first_preset: # If no other preset are available
|
||||
raise att.ATT_Error(ErrorCode.PRESET_OPERATION_NOT_POSSIBLE)
|
||||
|
||||
self.active_preset_index = new_preset.index
|
||||
if next_preset:
|
||||
self.active_preset_index = next_preset.index
|
||||
else:
|
||||
self.active_preset_index = first_preset.index
|
||||
await self.notify_active_preset()
|
||||
|
||||
async def _on_set_next_preset(self, connection: Connection, value: bytes) -> None:
|
||||
del connection, value # Unused.
|
||||
await self.set_next_or_previous_preset(False)
|
||||
async def _on_set_next_preset(
|
||||
self, connection: Optional[Connection], __value__: bytes
|
||||
) -> None:
|
||||
await self.set_next_or_previous_preset(connection, False)
|
||||
|
||||
async def _on_set_previous_preset(
|
||||
self, connection: Connection, value: bytes
|
||||
self, connection: Optional[Connection], __value__: bytes
|
||||
) -> None:
|
||||
del connection, value # Unused.
|
||||
await self.set_next_or_previous_preset(True)
|
||||
await self.set_next_or_previous_preset(connection, True)
|
||||
|
||||
async def _on_set_active_preset_synchronized_locally(
|
||||
self, connection: Connection, value: bytes
|
||||
self, connection: Optional[Connection], value: bytes
|
||||
):
|
||||
del connection # Unused.
|
||||
if (
|
||||
self.server_features.preset_synchronization_support
|
||||
== PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_NOT_SUPPORTED
|
||||
== PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_SUPPORTED
|
||||
):
|
||||
raise att.ATT_Error(ErrorCode.PRESET_SYNCHRONIZATION_NOT_SUPPORTED)
|
||||
await self.set_active_preset(value)
|
||||
if self.other_server_in_binaural_set:
|
||||
await self.other_server_in_binaural_set.set_active_preset(value)
|
||||
await self.set_active_preset(connection, value)
|
||||
# TODO (low priority) inform other server of the change
|
||||
|
||||
async def _on_set_next_preset_synchronized_locally(
|
||||
self, connection: Connection, value: bytes
|
||||
self, connection: Optional[Connection], __value__: bytes
|
||||
):
|
||||
del connection, value # Unused.
|
||||
if (
|
||||
self.server_features.preset_synchronization_support
|
||||
== PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_NOT_SUPPORTED
|
||||
== PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_SUPPORTED
|
||||
):
|
||||
raise att.ATT_Error(ErrorCode.PRESET_SYNCHRONIZATION_NOT_SUPPORTED)
|
||||
await self.set_next_or_previous_preset(False)
|
||||
if self.other_server_in_binaural_set:
|
||||
await self.other_server_in_binaural_set.set_next_or_previous_preset(False)
|
||||
await self.set_next_or_previous_preset(connection, False)
|
||||
# TODO (low priority) inform other server of the change
|
||||
|
||||
async def _on_set_previous_preset_synchronized_locally(
|
||||
self, connection: Connection, value: bytes
|
||||
self, connection: Optional[Connection], __value__: bytes
|
||||
):
|
||||
del connection, value # Unused.
|
||||
if (
|
||||
self.server_features.preset_synchronization_support
|
||||
== PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_NOT_SUPPORTED
|
||||
== PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_SUPPORTED
|
||||
):
|
||||
raise att.ATT_Error(ErrorCode.PRESET_SYNCHRONIZATION_NOT_SUPPORTED)
|
||||
await self.set_next_or_previous_preset(True)
|
||||
if self.other_server_in_binaural_set:
|
||||
await self.other_server_in_binaural_set.set_next_or_previous_preset(True)
|
||||
await self.set_next_or_previous_preset(connection, True)
|
||||
# TODO (low priority) inform other server of the change
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -653,13 +631,11 @@ class HearingAccessServiceProxy(gatt_client.ProfileServiceProxy):
|
||||
SERVICE_CLASS = HearingAccessService
|
||||
|
||||
hearing_aid_preset_control_point: gatt_client.CharacteristicProxy
|
||||
preset_control_point_indications: asyncio.Queue[bytes]
|
||||
active_preset_index_notification: asyncio.Queue[bytes]
|
||||
preset_control_point_indications: asyncio.Queue
|
||||
active_preset_index_notification: asyncio.Queue
|
||||
|
||||
def __init__(self, service_proxy: gatt_client.ServiceProxy) -> None:
|
||||
self.service_proxy = service_proxy
|
||||
self.preset_control_point_indications = asyncio.Queue()
|
||||
self.active_preset_index_notification = asyncio.Queue()
|
||||
|
||||
self.server_features = gatt_adapters.PackedCharacteristicProxyAdapter(
|
||||
service_proxy.get_characteristics_by_uuid(
|
||||
@@ -681,12 +657,20 @@ class HearingAccessServiceProxy(gatt_client.ProfileServiceProxy):
|
||||
'B',
|
||||
)
|
||||
|
||||
async def setup_subscription(self) -> None:
|
||||
async def setup_subscription(self):
|
||||
self.preset_control_point_indications = asyncio.Queue()
|
||||
self.active_preset_index_notification = asyncio.Queue()
|
||||
|
||||
def on_active_preset_index_notification(data: bytes):
|
||||
self.active_preset_index_notification.put_nowait(data)
|
||||
|
||||
def on_preset_control_point_indication(data: bytes):
|
||||
self.preset_control_point_indications.put_nowait(data)
|
||||
|
||||
await self.hearing_aid_preset_control_point.subscribe(
|
||||
self.preset_control_point_indications.put_nowait,
|
||||
prefer_notify=False,
|
||||
functools.partial(on_preset_control_point_indication), prefer_notify=False
|
||||
)
|
||||
|
||||
await self.active_preset_index.subscribe(
|
||||
self.active_preset_index_notification.put_nowait
|
||||
functools.partial(on_active_preset_index_notification)
|
||||
)
|
||||
|
||||
@@ -17,20 +17,20 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
from enum import IntEnum
|
||||
import struct
|
||||
from typing import Optional
|
||||
|
||||
from bumble import core
|
||||
from bumble.att import ATT_Error
|
||||
from bumble.gatt import (
|
||||
GATT_HEART_RATE_SERVICE,
|
||||
GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC,
|
||||
GATT_BODY_SENSOR_LOCATION_CHARACTERISTIC,
|
||||
GATT_HEART_RATE_CONTROL_POINT_CHARACTERISTIC,
|
||||
GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC,
|
||||
GATT_HEART_RATE_SERVICE,
|
||||
TemplateService,
|
||||
Characteristic,
|
||||
CharacteristicValue,
|
||||
TemplateService,
|
||||
)
|
||||
from bumble.gatt_adapters import (
|
||||
DelegatedCharacteristicAdapter,
|
||||
@@ -206,13 +206,13 @@ class HeartRateService(TemplateService):
|
||||
class HeartRateServiceProxy(ProfileServiceProxy):
|
||||
SERVICE_CLASS = HeartRateService
|
||||
|
||||
heart_rate_measurement: (
|
||||
CharacteristicProxy[HeartRateService.HeartRateMeasurement] | None
|
||||
)
|
||||
body_sensor_location: (
|
||||
CharacteristicProxy[HeartRateService.BodySensorLocation] | None
|
||||
)
|
||||
heart_rate_control_point: CharacteristicProxy[int] | None
|
||||
heart_rate_measurement: Optional[
|
||||
CharacteristicProxy[HeartRateService.HeartRateMeasurement]
|
||||
]
|
||||
body_sensor_location: Optional[
|
||||
CharacteristicProxy[HeartRateService.BodySensorLocation]
|
||||
]
|
||||
heart_rate_control_point: Optional[CharacteristicProxy[int]]
|
||||
|
||||
def __init__(self, service_proxy):
|
||||
self.service_proxy = service_proxy
|
||||
|
||||
@@ -16,16 +16,14 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import enum
|
||||
import struct
|
||||
from typing import Any
|
||||
|
||||
from typing import Any, List, Type
|
||||
from typing_extensions import Self
|
||||
|
||||
from bumble import utils
|
||||
from bumble.profiles import bap
|
||||
from bumble import utils
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -110,13 +108,13 @@ class Metadata:
|
||||
return self.data
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls: type[Self], data: bytes) -> Self:
|
||||
def from_bytes(cls: Type[Self], data: bytes) -> Self:
|
||||
return cls(tag=Metadata.Tag(data[0]), data=data[1:])
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
return bytes([len(self.data) + 1, self.tag]) + self.data
|
||||
|
||||
entries: list[Entry] = dataclasses.field(default_factory=list)
|
||||
entries: List[Entry] = dataclasses.field(default_factory=list)
|
||||
|
||||
def pretty_print(self, indent: str) -> str:
|
||||
"""Convenience method to generate a string with one key-value pair per line."""
|
||||
@@ -137,12 +135,12 @@ class Metadata:
|
||||
values.append(str(decoded))
|
||||
|
||||
return '\n'.join(
|
||||
f'{indent}{key}: {" " * (max_key_length - len(key))}{value}'
|
||||
f'{indent}{key}: {" " * (max_key_length-len(key))}{value}'
|
||||
for key, value in zip(keys, values)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls: type[Self], data: bytes) -> Self:
|
||||
def from_bytes(cls: Type[Self], data: bytes) -> Self:
|
||||
entries = []
|
||||
offset = 0
|
||||
length = len(data)
|
||||
|
||||
@@ -22,12 +22,16 @@ import asyncio
|
||||
import dataclasses
|
||||
import enum
|
||||
import struct
|
||||
from typing import TYPE_CHECKING, ClassVar
|
||||
|
||||
from bumble import core
|
||||
from bumble import device
|
||||
from bumble import gatt
|
||||
from bumble import gatt_client
|
||||
from bumble import utils
|
||||
|
||||
from typing import Type, Optional, ClassVar, Dict, TYPE_CHECKING
|
||||
from typing_extensions import Self
|
||||
|
||||
from bumble import core, device, gatt, gatt_client, utils
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -163,7 +167,7 @@ class ObjectId(int):
|
||||
'''See Media Control Service 4.4.2. Object ID field.'''
|
||||
|
||||
@classmethod
|
||||
def create_from_bytes(cls: type[Self], data: bytes) -> Self:
|
||||
def create_from_bytes(cls: Type[Self], data: bytes) -> Self:
|
||||
return cls(int.from_bytes(data, byteorder='little', signed=False))
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
@@ -178,7 +182,7 @@ class GroupObjectType:
|
||||
object_id: ObjectId
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls: type[Self], data: bytes) -> Self:
|
||||
def from_bytes(cls: Type[Self], data: bytes) -> Self:
|
||||
return cls(
|
||||
object_type=ObjectType(data[0]),
|
||||
object_id=ObjectId.create_from_bytes(data[1:]),
|
||||
@@ -196,7 +200,7 @@ class MediaControlService(gatt.TemplateService):
|
||||
|
||||
UUID = gatt.GATT_MEDIA_CONTROL_SERVICE
|
||||
|
||||
def __init__(self, media_player_name: str | None = None) -> None:
|
||||
def __init__(self, media_player_name: Optional[str] = None) -> None:
|
||||
self.track_position = 0
|
||||
|
||||
self.media_player_name_characteristic = gatt.Characteristic(
|
||||
@@ -283,8 +287,11 @@ class MediaControlService(gatt.TemplateService):
|
||||
)
|
||||
|
||||
async def on_media_control_point(
|
||||
self, connection: device.Connection, data: bytes
|
||||
self, connection: Optional[device.Connection], data: bytes
|
||||
) -> None:
|
||||
if not connection:
|
||||
raise core.InvalidStateError()
|
||||
|
||||
opcode = MediaControlPointOpcode(data[0])
|
||||
|
||||
await connection.device.notify_subscriber(
|
||||
@@ -306,7 +313,7 @@ class MediaControlServiceProxy(
|
||||
):
|
||||
SERVICE_CLASS = MediaControlService
|
||||
|
||||
_CHARACTERISTICS: ClassVar[dict[str, core.UUID]] = {
|
||||
_CHARACTERISTICS: ClassVar[Dict[str, core.UUID]] = {
|
||||
'media_player_name': gatt.GATT_MEDIA_PLAYER_NAME_CHARACTERISTIC,
|
||||
'media_player_icon_object_id': gatt.GATT_MEDIA_PLAYER_ICON_OBJECT_ID_CHARACTERISTIC,
|
||||
'media_player_icon_url': gatt.GATT_MEDIA_PLAYER_ICON_URL_CHARACTERISTIC,
|
||||
@@ -337,32 +344,32 @@ class MediaControlServiceProxy(
|
||||
EVENT_TRACK_DURATION = "track_duration"
|
||||
EVENT_TRACK_POSITION = "track_position"
|
||||
|
||||
media_player_name: gatt_client.CharacteristicProxy[bytes] | None = None
|
||||
media_player_icon_object_id: gatt_client.CharacteristicProxy[bytes] | None = None
|
||||
media_player_icon_url: gatt_client.CharacteristicProxy[bytes] | None = None
|
||||
track_changed: gatt_client.CharacteristicProxy[bytes] | None = None
|
||||
track_title: gatt_client.CharacteristicProxy[bytes] | None = None
|
||||
track_duration: gatt_client.CharacteristicProxy[bytes] | None = None
|
||||
track_position: gatt_client.CharacteristicProxy[bytes] | None = None
|
||||
playback_speed: gatt_client.CharacteristicProxy[bytes] | None = None
|
||||
seeking_speed: gatt_client.CharacteristicProxy[bytes] | None = None
|
||||
current_track_segments_object_id: gatt_client.CharacteristicProxy[bytes] | None = (
|
||||
None
|
||||
)
|
||||
current_track_object_id: gatt_client.CharacteristicProxy[bytes] | None = None
|
||||
next_track_object_id: gatt_client.CharacteristicProxy[bytes] | None = None
|
||||
parent_group_object_id: gatt_client.CharacteristicProxy[bytes] | None = None
|
||||
current_group_object_id: gatt_client.CharacteristicProxy[bytes] | None = None
|
||||
playing_order: gatt_client.CharacteristicProxy[bytes] | None = None
|
||||
playing_orders_supported: gatt_client.CharacteristicProxy[bytes] | None = None
|
||||
media_state: gatt_client.CharacteristicProxy[bytes] | None = None
|
||||
media_control_point: gatt_client.CharacteristicProxy[bytes] | None = None
|
||||
media_control_point_opcodes_supported: (
|
||||
gatt_client.CharacteristicProxy[bytes] | None
|
||||
) = None
|
||||
search_control_point: gatt_client.CharacteristicProxy[bytes] | None = None
|
||||
search_results_object_id: gatt_client.CharacteristicProxy[bytes] | None = None
|
||||
content_control_id: gatt_client.CharacteristicProxy[bytes] | None = None
|
||||
media_player_name: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
media_player_icon_object_id: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
media_player_icon_url: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
track_changed: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
track_title: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
track_duration: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
track_position: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
playback_speed: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
seeking_speed: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
current_track_segments_object_id: Optional[
|
||||
gatt_client.CharacteristicProxy[bytes]
|
||||
] = None
|
||||
current_track_object_id: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
next_track_object_id: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
parent_group_object_id: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
current_group_object_id: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
playing_order: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
playing_orders_supported: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
media_state: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
media_control_point: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
media_control_point_opcodes_supported: Optional[
|
||||
gatt_client.CharacteristicProxy[bytes]
|
||||
] = None
|
||||
search_control_point: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
search_results_object_id: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
content_control_id: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||
|
||||
if TYPE_CHECKING:
|
||||
media_control_point_notifications: asyncio.Queue[bytes]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user