forked from auracaster/bumble_mirror
Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2e523b6f49 | |||
| 3b399ea1a2 | |||
| 84f7cad678 | |||
| 778f439e1c | |||
| 1b95d4e1df | |||
| 512f6d4ee1 | |||
| c52b614abb | |||
| 7b7afc7179 | |||
| b1c6044533 | |||
| 38499dfe3c | |||
| b58c29202a | |||
| ca759ca967 | |||
| 3858bf80c1 | |||
| a88a034ce2 | |||
| 6b2cd1147d | |||
| bb8dcaf63e | |||
| 8e84b528ce | |||
| 8b59b4f515 | |||
| dcc72e49a2 | |||
| ce04c163db | |||
| 9f1e95d87f | |||
| 088bcbed0b | |||
| 57fbad6fa4 | |||
| 6926d5cb70 | |||
| 00c7df6a11 | |||
| fbd03ed4a5 | |||
| d3bd5a759f | |||
| dedef79bef | |||
| 8db974877e | |||
| e7d1531eae | |||
| 4785fe6002 | |||
| 22d6a7bf05 | |||
| 97757c0c3d | |||
| ab60b42b85 | |||
| febed8179b | |||
| 1bd83273e8 | |||
| 5e9fc89f80 | |||
| 2686663eb2 | |||
| 55801bc2ca | |||
| 6cecc16519 | |||
| a57cf13e2e | |||
| 58f153afc4 | |||
| 7569da37e4 | |||
| a8019a70da | |||
| 685f1dc43e | |||
| 220b3b0236 | |||
| 3495eb52ba | |||
| 1f7a1401eb | |||
| ce2b02b62a | |||
| 5e55c0e358 |
@@ -0,0 +1,26 @@
|
|||||||
|
#
|
||||||
|
# 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
|
||||||
|
#
|
||||||
|
# http://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.
|
||||||
|
#
|
||||||
|
|
||||||
|
org.gradle.configureondemand=true
|
||||||
|
org.gradle.caching=true
|
||||||
|
org.gradle.parallel=true
|
||||||
|
|
||||||
|
# Declare we support AndroidX
|
||||||
|
android.useAndroidX=true
|
||||||
|
|
||||||
|
org.gradle.jvmargs=-Xmx4608m -XX:MaxMetaspaceSize=1536m -XX:+HeapDumpOnOutOfMemoryError
|
||||||
|
|
||||||
|
kotlin.compiler.execution.strategy=in-process
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
name: Gradle Android Build & test
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
paths:
|
||||||
|
- 'extras/android/BtBench/**'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 40
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out from Git
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up JDK
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'zulu'
|
||||||
|
java-version: 17
|
||||||
|
|
||||||
|
- name: Setup Gradle
|
||||||
|
uses: gradle/actions/setup-gradle@v3
|
||||||
|
|
||||||
|
- name: Build with Gradle
|
||||||
|
run: cd extras/android/BtBench && ./gradlew build
|
||||||
+37
-29
@@ -18,7 +18,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import asyncio.subprocess
|
|
||||||
import collections
|
import collections
|
||||||
import contextlib
|
import contextlib
|
||||||
import dataclasses
|
import dataclasses
|
||||||
@@ -36,7 +35,6 @@ from typing import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
import click
|
import click
|
||||||
import pyee
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import lc3 # type: ignore # pylint: disable=E0401
|
import lc3 # type: ignore # pylint: disable=E0401
|
||||||
@@ -99,12 +97,31 @@ def codec_config_string(
|
|||||||
return '\n'.join(indent + line for line in lines)
|
return '\n'.join(indent + line for line in lines)
|
||||||
|
|
||||||
|
|
||||||
|
def broadcast_code_bytes(broadcast_code: str) -> bytes:
|
||||||
|
"""
|
||||||
|
Convert a broadcast code string to a 16-byte value.
|
||||||
|
|
||||||
|
If `broadcast_code` is `0x` followed by 32 hex characters, it is interpreted as a
|
||||||
|
raw 16-byte raw broadcast code in big-endian byte order.
|
||||||
|
Otherwise, `broadcast_code` is converted to a 16-byte value as specified in
|
||||||
|
BLUETOOTH CORE SPECIFICATION Version 6.0 | Vol 3, Part C , section 3.2.6.3
|
||||||
|
"""
|
||||||
|
if broadcast_code.startswith("0x") and len(broadcast_code) == 34:
|
||||||
|
return bytes.fromhex(broadcast_code[2:])[::-1]
|
||||||
|
|
||||||
|
broadcast_code_utf8 = broadcast_code.encode("utf-8")
|
||||||
|
if len(broadcast_code_utf8) > 16:
|
||||||
|
raise ValueError("broadcast code must be <= 16 bytes in utf-8 encoding")
|
||||||
|
padding = bytes(16 - len(broadcast_code_utf8))
|
||||||
|
return broadcast_code_utf8 + padding
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Scan For Broadcasts
|
# Scan For Broadcasts
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class BroadcastScanner(pyee.EventEmitter):
|
class BroadcastScanner(bumble.utils.EventEmitter):
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class Broadcast(pyee.EventEmitter):
|
class Broadcast(bumble.utils.EventEmitter):
|
||||||
name: str | None
|
name: str | None
|
||||||
sync: bumble.device.PeriodicAdvertisingSync
|
sync: bumble.device.PeriodicAdvertisingSync
|
||||||
broadcast_id: int
|
broadcast_id: int
|
||||||
@@ -234,22 +251,14 @@ class BroadcastScanner(pyee.EventEmitter):
|
|||||||
|
|
||||||
if self.biginfo:
|
if self.biginfo:
|
||||||
print(color(' BIG:', 'cyan'))
|
print(color(' BIG:', 'cyan'))
|
||||||
print(
|
print(color(' Number of BIS:', 'magenta'), self.biginfo.num_bis)
|
||||||
color(' Number of BIS:', 'magenta'),
|
print(color(' ISO Interval: ', 'magenta'), self.biginfo.iso_interval)
|
||||||
self.biginfo.num_bis,
|
print(color(' Max PDU: ', 'magenta'), self.biginfo.max_pdu)
|
||||||
)
|
print(color(' SDU Interval: ', 'magenta'), self.biginfo.sdu_interval)
|
||||||
print(
|
print(color(' Max SDU: ', 'magenta'), self.biginfo.max_sdu)
|
||||||
color(' PHY: ', 'magenta'),
|
print(color(' PHY: ', 'magenta'), self.biginfo.phy.name)
|
||||||
self.biginfo.phy.name,
|
print(color(' Framed: ', 'magenta'), self.biginfo.framed)
|
||||||
)
|
print(color(' Encrypted: ', 'magenta'), self.biginfo.encrypted)
|
||||||
print(
|
|
||||||
color(' Framed: ', 'magenta'),
|
|
||||||
self.biginfo.framed,
|
|
||||||
)
|
|
||||||
print(
|
|
||||||
color(' Encrypted: ', 'magenta'),
|
|
||||||
self.biginfo.encrypted,
|
|
||||||
)
|
|
||||||
|
|
||||||
def on_sync_establishment(self) -> None:
|
def on_sync_establishment(self) -> None:
|
||||||
self.emit('sync_establishment')
|
self.emit('sync_establishment')
|
||||||
@@ -365,7 +374,7 @@ class BroadcastScanner(pyee.EventEmitter):
|
|||||||
self.emit('broadcast_loss', broadcast)
|
self.emit('broadcast_loss', broadcast)
|
||||||
|
|
||||||
|
|
||||||
class PrintingBroadcastScanner(pyee.EventEmitter):
|
class PrintingBroadcastScanner(bumble.utils.EventEmitter):
|
||||||
def __init__(
|
def __init__(
|
||||||
self, device: bumble.device.Device, filter_duplicates: bool, sync_timeout: float
|
self, device: bumble.device.Device, filter_duplicates: bool, sync_timeout: float
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -702,14 +711,13 @@ async def run_receive(
|
|||||||
|
|
||||||
def on_change() -> None:
|
def on_change() -> None:
|
||||||
if (
|
if (
|
||||||
broadcast.basic_audio_announcement
|
broadcast.basic_audio_announcement and broadcast.biginfo
|
||||||
and not basic_audio_announcement_scanned.is_set()
|
) and not basic_audio_announcement_scanned.is_set():
|
||||||
):
|
|
||||||
basic_audio_announcement_scanned.set()
|
basic_audio_announcement_scanned.set()
|
||||||
|
|
||||||
broadcast.on('change', on_change)
|
broadcast.on('change', on_change)
|
||||||
if not broadcast.basic_audio_announcement:
|
if not broadcast.basic_audio_announcement or not broadcast.biginfo:
|
||||||
print('Wait for Basic Audio Announcement...')
|
print('Wait for Basic Audio Announcement and BIG Info...')
|
||||||
await basic_audio_announcement_scanned.wait()
|
await basic_audio_announcement_scanned.wait()
|
||||||
print('Basic Audio Announcement found')
|
print('Basic Audio Announcement found')
|
||||||
broadcast.print()
|
broadcast.print()
|
||||||
@@ -730,7 +738,7 @@ async def run_receive(
|
|||||||
big_sync_timeout=0x4000,
|
big_sync_timeout=0x4000,
|
||||||
bis=[bis.index for bis in subgroup.bis],
|
bis=[bis.index for bis in subgroup.bis],
|
||||||
broadcast_code=(
|
broadcast_code=(
|
||||||
bytes.fromhex(broadcast_code) if broadcast_code else None
|
broadcast_code_bytes(broadcast_code) if broadcast_code else None
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -944,7 +952,7 @@ async def run_transmit(
|
|||||||
max_transport_latency=65,
|
max_transport_latency=65,
|
||||||
rtn=4,
|
rtn=4,
|
||||||
broadcast_code=(
|
broadcast_code=(
|
||||||
bytes.fromhex(broadcast_code) if broadcast_code else None
|
broadcast_code_bytes(broadcast_code) if broadcast_code else None
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -1088,7 +1096,7 @@ def pair(ctx, transport, address):
|
|||||||
'--broadcast-code',
|
'--broadcast-code',
|
||||||
metavar='BROADCAST_CODE',
|
metavar='BROADCAST_CODE',
|
||||||
type=str,
|
type=str,
|
||||||
help='Broadcast encryption code in hex format',
|
help='Broadcast encryption code (string or raw hex format prefixed with 0x)',
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'--sync-timeout',
|
'--sync-timeout',
|
||||||
|
|||||||
+17
-11
@@ -28,8 +28,7 @@ import click
|
|||||||
|
|
||||||
from bumble import l2cap
|
from bumble import l2cap
|
||||||
from bumble.core import (
|
from bumble.core import (
|
||||||
BT_BR_EDR_TRANSPORT,
|
PhysicalTransport,
|
||||||
BT_LE_TRANSPORT,
|
|
||||||
BT_L2CAP_PROTOCOL_ID,
|
BT_L2CAP_PROTOCOL_ID,
|
||||||
BT_RFCOMM_PROTOCOL_ID,
|
BT_RFCOMM_PROTOCOL_ID,
|
||||||
UUID,
|
UUID,
|
||||||
@@ -42,8 +41,7 @@ from bumble.hci import (
|
|||||||
HCI_LE_1M_PHY,
|
HCI_LE_1M_PHY,
|
||||||
HCI_LE_2M_PHY,
|
HCI_LE_2M_PHY,
|
||||||
HCI_LE_CODED_PHY,
|
HCI_LE_CODED_PHY,
|
||||||
HCI_CENTRAL_ROLE,
|
Role,
|
||||||
HCI_PERIPHERAL_ROLE,
|
|
||||||
HCI_Constant,
|
HCI_Constant,
|
||||||
HCI_Error,
|
HCI_Error,
|
||||||
HCI_StatusError,
|
HCI_StatusError,
|
||||||
@@ -113,7 +111,7 @@ def print_connection_phy(phy):
|
|||||||
|
|
||||||
def print_connection(connection):
|
def print_connection(connection):
|
||||||
params = []
|
params = []
|
||||||
if connection.transport == BT_LE_TRANSPORT:
|
if connection.transport == PhysicalTransport.LE:
|
||||||
params.append(
|
params.append(
|
||||||
'DL=('
|
'DL=('
|
||||||
f'TX:{connection.data_length[0]}/{connection.data_length[1]},'
|
f'TX:{connection.data_length[0]}/{connection.data_length[1]},'
|
||||||
@@ -123,9 +121,9 @@ def print_connection(connection):
|
|||||||
|
|
||||||
params.append(
|
params.append(
|
||||||
'Parameters='
|
'Parameters='
|
||||||
f'{connection.parameters.connection_interval * 1.25:.2f}/'
|
f'{connection.parameters.connection_interval:.2f}/'
|
||||||
f'{connection.parameters.peripheral_latency}/'
|
f'{connection.parameters.peripheral_latency}/'
|
||||||
f'{connection.parameters.supervision_timeout * 10} '
|
f'{connection.parameters.supervision_timeout:.2f} '
|
||||||
)
|
)
|
||||||
|
|
||||||
params.append(f'MTU={connection.att_mtu}')
|
params.append(f'MTU={connection.att_mtu}')
|
||||||
@@ -189,7 +187,7 @@ def log_stats(title, stats, precision=2):
|
|||||||
|
|
||||||
|
|
||||||
async def switch_roles(connection, role):
|
async def switch_roles(connection, role):
|
||||||
target_role = HCI_CENTRAL_ROLE if role == "central" else HCI_PERIPHERAL_ROLE
|
target_role = Role.CENTRAL if role == "central" else Role.PERIPHERAL
|
||||||
if connection.role != target_role:
|
if connection.role != target_role:
|
||||||
logging.info(f'{color("### Switching roles to:", "cyan")} {role}')
|
logging.info(f'{color("### Switching roles to:", "cyan")} {role}')
|
||||||
try:
|
try:
|
||||||
@@ -1258,6 +1256,7 @@ class Central(Connection.Listener):
|
|||||||
self.device.classic_enabled = self.classic
|
self.device.classic_enabled = self.classic
|
||||||
|
|
||||||
# Set up a pairing config factory with minimal requirements.
|
# Set up a pairing config factory with minimal requirements.
|
||||||
|
self.device.config.keystore = "JsonKeyStore"
|
||||||
self.device.pairing_config_factory = lambda _: PairingConfig(
|
self.device.pairing_config_factory = lambda _: PairingConfig(
|
||||||
sc=False, mitm=False, bonding=False
|
sc=False, mitm=False, bonding=False
|
||||||
)
|
)
|
||||||
@@ -1275,7 +1274,11 @@ class Central(Connection.Listener):
|
|||||||
self.connection = await self.device.connect(
|
self.connection = await self.device.connect(
|
||||||
self.peripheral_address,
|
self.peripheral_address,
|
||||||
connection_parameters_preferences=self.connection_parameter_preferences,
|
connection_parameters_preferences=self.connection_parameter_preferences,
|
||||||
transport=BT_BR_EDR_TRANSPORT if self.classic else BT_LE_TRANSPORT,
|
transport=(
|
||||||
|
PhysicalTransport.BR_EDR
|
||||||
|
if self.classic
|
||||||
|
else PhysicalTransport.LE
|
||||||
|
),
|
||||||
)
|
)
|
||||||
except CommandTimeoutError:
|
except CommandTimeoutError:
|
||||||
logging.info(color('!!! Connection timed out', 'red'))
|
logging.info(color('!!! Connection timed out', 'red'))
|
||||||
@@ -1289,8 +1292,10 @@ class Central(Connection.Listener):
|
|||||||
logging.info(color('### Connected', 'cyan'))
|
logging.info(color('### Connected', 'cyan'))
|
||||||
self.connection.listener = self
|
self.connection.listener = self
|
||||||
print_connection(self.connection)
|
print_connection(self.connection)
|
||||||
phy = await self.connection.get_phy()
|
|
||||||
print_connection_phy(phy)
|
if not self.classic:
|
||||||
|
phy = await self.connection.get_phy()
|
||||||
|
print_connection_phy(phy)
|
||||||
|
|
||||||
# Switch roles if needed.
|
# Switch roles if needed.
|
||||||
if self.role_switch:
|
if self.role_switch:
|
||||||
@@ -1404,6 +1409,7 @@ class Peripheral(Device.Listener, Connection.Listener):
|
|||||||
self.device.classic_enabled = self.classic
|
self.device.classic_enabled = self.classic
|
||||||
|
|
||||||
# Set up a pairing config factory with minimal requirements.
|
# Set up a pairing config factory with minimal requirements.
|
||||||
|
self.device.config.keystore = "JsonKeyStore"
|
||||||
self.device.pairing_config_factory = lambda _: PairingConfig(
|
self.device.pairing_config_factory = lambda _: PairingConfig(
|
||||||
sc=False, mitm=False, bonding=False
|
sc=False, mitm=False, bonding=False
|
||||||
)
|
)
|
||||||
|
|||||||
+3
-3
@@ -55,7 +55,7 @@ from prompt_toolkit.layout import (
|
|||||||
from bumble import __version__
|
from bumble import __version__
|
||||||
import bumble.core
|
import bumble.core
|
||||||
from bumble import colors
|
from bumble import colors
|
||||||
from bumble.core import UUID, AdvertisingData, BT_LE_TRANSPORT
|
from bumble.core import UUID, AdvertisingData, PhysicalTransport
|
||||||
from bumble.device import (
|
from bumble.device import (
|
||||||
ConnectionParametersPreferences,
|
ConnectionParametersPreferences,
|
||||||
ConnectionPHY,
|
ConnectionPHY,
|
||||||
@@ -335,9 +335,9 @@ class ConsoleApp:
|
|||||||
elif self.connected_peer:
|
elif self.connected_peer:
|
||||||
connection = self.connected_peer.connection
|
connection = self.connected_peer.connection
|
||||||
connection_parameters = (
|
connection_parameters = (
|
||||||
f'{connection.parameters.connection_interval}/'
|
f'{connection.parameters.connection_interval:.2f}/'
|
||||||
f'{connection.parameters.peripheral_latency}/'
|
f'{connection.parameters.peripheral_latency}/'
|
||||||
f'{connection.parameters.supervision_timeout}'
|
f'{connection.parameters.supervision_timeout:.2f}'
|
||||||
)
|
)
|
||||||
if self.connection_phy is not None:
|
if self.connection_phy is not None:
|
||||||
phy_state = (
|
phy_state = (
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import click
|
|||||||
import aiohttp.web
|
import aiohttp.web
|
||||||
|
|
||||||
import bumble
|
import bumble
|
||||||
|
from bumble import utils
|
||||||
from bumble.core import AdvertisingData
|
from bumble.core import AdvertisingData
|
||||||
from bumble.colors import color
|
from bumble.colors import color
|
||||||
from bumble.device import Device, DeviceConfiguration, AdvertisingParameters, CisLink
|
from bumble.device import Device, DeviceConfiguration, AdvertisingParameters, CisLink
|
||||||
@@ -359,7 +360,9 @@ class Speaker:
|
|||||||
pcm = decoder.decode(
|
pcm = decoder.decode(
|
||||||
pdu.iso_sdu_fragment, bit_depth=DEFAULT_PCM_BYTES_PER_SAMPLE * 8
|
pdu.iso_sdu_fragment, bit_depth=DEFAULT_PCM_BYTES_PER_SAMPLE * 8
|
||||||
)
|
)
|
||||||
self.device.abort_on('disconnection', self.ui_server.send_audio(pcm))
|
utils.cancel_on_event(
|
||||||
|
self.device, 'disconnection', self.ui_server.send_audio(pcm)
|
||||||
|
)
|
||||||
|
|
||||||
def on_ase_state_change(ase: ascs.AseStateMachine) -> None:
|
def on_ase_state_change(ase: ascs.AseStateMachine) -> None:
|
||||||
codec_config = ase.codec_specific_configuration
|
codec_config = ase.codec_specific_configuration
|
||||||
@@ -373,7 +376,8 @@ class Speaker:
|
|||||||
or codec_config.codec_frames_per_sdu is None
|
or codec_config.codec_frames_per_sdu is None
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
ase.cis_link.abort_on(
|
utils.cancel_on_event(
|
||||||
|
ase.cis_link,
|
||||||
'disconnection',
|
'disconnection',
|
||||||
lc3_source_task(
|
lc3_source_task(
|
||||||
filename=self.lc3_input_file_path,
|
filename=self.lc3_input_file_path,
|
||||||
|
|||||||
+189
-35
@@ -18,9 +18,12 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
import struct
|
||||||
|
|
||||||
import click
|
import click
|
||||||
from prompt_toolkit.shortcuts import PromptSession
|
from prompt_toolkit.shortcuts import PromptSession
|
||||||
|
|
||||||
|
from bumble.a2dp import make_audio_sink_service_sdp_records
|
||||||
from bumble.colors import color
|
from bumble.colors import color
|
||||||
from bumble.device import Device, Peer
|
from bumble.device import Device, Peer
|
||||||
from bumble.transport import open_transport_or_link
|
from bumble.transport import open_transport_or_link
|
||||||
@@ -30,17 +33,20 @@ from bumble.smp import error_name as smp_error_name
|
|||||||
from bumble.keys import JsonKeyStore
|
from bumble.keys import JsonKeyStore
|
||||||
from bumble.core import (
|
from bumble.core import (
|
||||||
AdvertisingData,
|
AdvertisingData,
|
||||||
|
Appearance,
|
||||||
ProtocolError,
|
ProtocolError,
|
||||||
BT_LE_TRANSPORT,
|
PhysicalTransport,
|
||||||
BT_BR_EDR_TRANSPORT,
|
UUID,
|
||||||
)
|
)
|
||||||
from bumble.gatt import (
|
from bumble.gatt import (
|
||||||
GATT_DEVICE_NAME_CHARACTERISTIC,
|
GATT_DEVICE_NAME_CHARACTERISTIC,
|
||||||
GATT_GENERIC_ACCESS_SERVICE,
|
GATT_GENERIC_ACCESS_SERVICE,
|
||||||
|
GATT_HEART_RATE_SERVICE,
|
||||||
|
GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC,
|
||||||
Service,
|
Service,
|
||||||
Characteristic,
|
Characteristic,
|
||||||
CharacteristicValue,
|
|
||||||
)
|
)
|
||||||
|
from bumble.hci import OwnAddressType
|
||||||
from bumble.att import (
|
from bumble.att import (
|
||||||
ATT_Error,
|
ATT_Error,
|
||||||
ATT_INSUFFICIENT_AUTHENTICATION_ERROR,
|
ATT_INSUFFICIENT_AUTHENTICATION_ERROR,
|
||||||
@@ -63,7 +69,7 @@ class Waiter:
|
|||||||
self.linger = linger
|
self.linger = linger
|
||||||
|
|
||||||
def terminate(self):
|
def terminate(self):
|
||||||
if not self.linger:
|
if not self.linger and not self.done.done:
|
||||||
self.done.set_result(None)
|
self.done.set_result(None)
|
||||||
|
|
||||||
async def wait_until_terminated(self):
|
async def wait_until_terminated(self):
|
||||||
@@ -194,7 +200,7 @@ class Delegate(PairingDelegate):
|
|||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
async def get_peer_name(peer, mode):
|
async def get_peer_name(peer, mode):
|
||||||
if mode == 'classic':
|
if peer.connection.transport == PhysicalTransport.BR_EDR:
|
||||||
return await peer.request_name()
|
return await peer.request_name()
|
||||||
|
|
||||||
# Try to get the peer name from GATT
|
# Try to get the peer name from GATT
|
||||||
@@ -226,13 +232,14 @@ def read_with_error(connection):
|
|||||||
raise ATT_Error(ATT_INSUFFICIENT_AUTHENTICATION_ERROR)
|
raise ATT_Error(ATT_INSUFFICIENT_AUTHENTICATION_ERROR)
|
||||||
|
|
||||||
|
|
||||||
def write_with_error(connection, _value):
|
# -----------------------------------------------------------------------------
|
||||||
if not connection.is_encrypted:
|
def sdp_records():
|
||||||
raise ATT_Error(ATT_INSUFFICIENT_ENCRYPTION_ERROR)
|
service_record_handle = 0x00010001
|
||||||
|
return {
|
||||||
if not AUTHENTICATION_ERROR_RETURNED[1]:
|
service_record_handle: make_audio_sink_service_sdp_records(
|
||||||
AUTHENTICATION_ERROR_RETURNED[1] = True
|
service_record_handle
|
||||||
raise ATT_Error(ATT_INSUFFICIENT_AUTHENTICATION_ERROR)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -240,15 +247,19 @@ def on_connection(connection, request):
|
|||||||
print(color(f'<<< Connection: {connection}', 'green'))
|
print(color(f'<<< Connection: {connection}', 'green'))
|
||||||
|
|
||||||
# Listen for pairing events
|
# Listen for pairing events
|
||||||
connection.on('pairing_start', on_pairing_start)
|
connection.on(connection.EVENT_PAIRING_START, on_pairing_start)
|
||||||
connection.on('pairing', lambda keys: on_pairing(connection, keys))
|
connection.on(connection.EVENT_PAIRING, lambda keys: on_pairing(connection, keys))
|
||||||
connection.on(
|
connection.on(
|
||||||
'pairing_failure', lambda reason: on_pairing_failure(connection, reason)
|
connection.EVENT_CLASSIC_PAIRING, lambda: on_classic_pairing(connection)
|
||||||
|
)
|
||||||
|
connection.on(
|
||||||
|
connection.EVENT_PAIRING_FAILURE,
|
||||||
|
lambda reason: on_pairing_failure(connection, reason),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Listen for encryption changes
|
# Listen for encryption changes
|
||||||
connection.on(
|
connection.on(
|
||||||
'connection_encryption_change',
|
connection.EVENT_CONNECTION_ENCRYPTION_CHANGE,
|
||||||
lambda: on_connection_encryption_change(connection),
|
lambda: on_connection_encryption_change(connection),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -289,6 +300,20 @@ async def on_pairing(connection, keys):
|
|||||||
Waiter.instance.terminate()
|
Waiter.instance.terminate()
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
@AsyncRunner.run_in_task()
|
||||||
|
async def on_classic_pairing(connection):
|
||||||
|
print(color('***-----------------------------------', 'cyan'))
|
||||||
|
print(
|
||||||
|
color(
|
||||||
|
f'*** Paired [Classic]! (peer identity={connection.peer_address})', 'cyan'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
print(color('***-----------------------------------', 'cyan'))
|
||||||
|
await asyncio.sleep(POST_PAIRING_DELAY)
|
||||||
|
Waiter.instance.terminate()
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@AsyncRunner.run_in_task()
|
@AsyncRunner.run_in_task()
|
||||||
async def on_pairing_failure(connection, reason):
|
async def on_pairing_failure(connection, reason):
|
||||||
@@ -306,6 +331,7 @@ async def pair(
|
|||||||
mitm,
|
mitm,
|
||||||
bond,
|
bond,
|
||||||
ctkd,
|
ctkd,
|
||||||
|
advertising_address,
|
||||||
identity_address,
|
identity_address,
|
||||||
linger,
|
linger,
|
||||||
io,
|
io,
|
||||||
@@ -314,6 +340,8 @@ async def pair(
|
|||||||
request,
|
request,
|
||||||
print_keys,
|
print_keys,
|
||||||
keystore_file,
|
keystore_file,
|
||||||
|
advertise_service_uuids,
|
||||||
|
advertise_appearance,
|
||||||
device_config,
|
device_config,
|
||||||
hci_transport,
|
hci_transport,
|
||||||
address_or_name,
|
address_or_name,
|
||||||
@@ -329,29 +357,33 @@ async def pair(
|
|||||||
|
|
||||||
# Expose a GATT characteristic that can be used to trigger pairing by
|
# Expose a GATT characteristic that can be used to trigger pairing by
|
||||||
# responding with an authentication error when read
|
# responding with an authentication error when read
|
||||||
if mode == 'le':
|
if mode in ('le', 'dual'):
|
||||||
device.le_enabled = True
|
|
||||||
device.add_service(
|
device.add_service(
|
||||||
Service(
|
Service(
|
||||||
'50DB505C-8AC4-4738-8448-3B1D9CC09CC5',
|
GATT_HEART_RATE_SERVICE,
|
||||||
[
|
[
|
||||||
Characteristic(
|
Characteristic(
|
||||||
'552957FB-CF1F-4A31-9535-E78847E1A714',
|
GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC,
|
||||||
Characteristic.Properties.READ
|
Characteristic.Properties.READ,
|
||||||
| Characteristic.Properties.WRITE,
|
Characteristic.READ_REQUIRES_AUTHENTICATION,
|
||||||
Characteristic.READABLE | Characteristic.WRITEABLE,
|
bytes(1),
|
||||||
CharacteristicValue(
|
|
||||||
read=read_with_error, write=write_with_error
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Select LE or Classic
|
# LE and Classic support
|
||||||
if mode == 'classic':
|
if mode in ('classic', 'dual'):
|
||||||
device.classic_enabled = True
|
device.classic_enabled = True
|
||||||
device.classic_smp_enabled = ctkd
|
device.classic_smp_enabled = ctkd
|
||||||
|
if mode in ('le', 'dual'):
|
||||||
|
device.le_enabled = True
|
||||||
|
if mode == 'dual':
|
||||||
|
device.le_simultaneous_enabled = True
|
||||||
|
|
||||||
|
# Setup SDP
|
||||||
|
if mode in ('classic', 'dual'):
|
||||||
|
device.sdp_service_records = sdp_records()
|
||||||
|
|
||||||
# Get things going
|
# Get things going
|
||||||
await device.power_on()
|
await device.power_on()
|
||||||
@@ -422,7 +454,9 @@ async def pair(
|
|||||||
print(color(f'=== Connecting to {address_or_name}...', 'green'))
|
print(color(f'=== Connecting to {address_or_name}...', 'green'))
|
||||||
connection = await device.connect(
|
connection = await device.connect(
|
||||||
address_or_name,
|
address_or_name,
|
||||||
transport=BT_LE_TRANSPORT if mode == 'le' else BT_BR_EDR_TRANSPORT,
|
transport=(
|
||||||
|
PhysicalTransport.LE if mode == 'le' else PhysicalTransport.BR_EDR
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
if not request:
|
if not request:
|
||||||
@@ -435,13 +469,109 @@ async def pair(
|
|||||||
print(color(f'Pairing failed: {error}', 'red'))
|
print(color(f'Pairing failed: {error}', 'red'))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if mode == 'le':
|
if mode in ('le', 'dual'):
|
||||||
# Advertise so that peers can find us and connect
|
# Advertise so that peers can find us and connect.
|
||||||
await device.start_advertising(auto_restart=True)
|
# Include the heart rate service UUID in the advertisement data
|
||||||
else:
|
# so that devices like iPhones can show this device in their
|
||||||
|
# Bluetooth selector.
|
||||||
|
service_uuids_16 = []
|
||||||
|
service_uuids_32 = []
|
||||||
|
service_uuids_128 = []
|
||||||
|
if advertise_service_uuids:
|
||||||
|
for uuid in advertise_service_uuids:
|
||||||
|
uuid = uuid.replace("-", "")
|
||||||
|
if len(uuid) == 4:
|
||||||
|
service_uuids_16.append(UUID(uuid))
|
||||||
|
elif len(uuid) == 8:
|
||||||
|
service_uuids_32.append(UUID(uuid))
|
||||||
|
elif len(uuid) == 32:
|
||||||
|
service_uuids_128.append(UUID(uuid))
|
||||||
|
else:
|
||||||
|
print(color('Invalid UUID format', 'red'))
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
service_uuids_16.append(GATT_HEART_RATE_SERVICE)
|
||||||
|
|
||||||
|
flags = AdvertisingData.Flags.LE_LIMITED_DISCOVERABLE_MODE
|
||||||
|
if mode == 'le':
|
||||||
|
flags |= AdvertisingData.Flags.BR_EDR_NOT_SUPPORTED
|
||||||
|
if mode == 'dual':
|
||||||
|
flags |= AdvertisingData.Flags.SIMULTANEOUS_LE_BR_EDR_CAPABLE
|
||||||
|
|
||||||
|
ad_structs = [
|
||||||
|
(
|
||||||
|
AdvertisingData.FLAGS,
|
||||||
|
bytes([flags]),
|
||||||
|
),
|
||||||
|
(AdvertisingData.COMPLETE_LOCAL_NAME, 'Bumble'.encode()),
|
||||||
|
]
|
||||||
|
if 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:
|
||||||
|
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:
|
||||||
|
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:
|
||||||
|
advertise_appearance_int = int(advertise_appearance)
|
||||||
|
except ValueError:
|
||||||
|
category, subcategory = advertise_appearance.split('/')
|
||||||
|
try:
|
||||||
|
category_enum = Appearance.Category[category]
|
||||||
|
except ValueError:
|
||||||
|
print(
|
||||||
|
color(f'Invalid appearance category {category}', 'red')
|
||||||
|
)
|
||||||
|
return
|
||||||
|
subcategory_class = Appearance.SUBCATEGORY_CLASSES[
|
||||||
|
category_enum
|
||||||
|
]
|
||||||
|
try:
|
||||||
|
subcategory_enum = subcategory_class[subcategory]
|
||||||
|
except ValueError:
|
||||||
|
print(color(f'Invalid subcategory {subcategory}', 'red'))
|
||||||
|
return
|
||||||
|
advertise_appearance_int = int(
|
||||||
|
Appearance(category_enum, subcategory_enum)
|
||||||
|
)
|
||||||
|
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=(
|
||||||
|
OwnAddressType.PUBLIC
|
||||||
|
if advertising_address == 'public'
|
||||||
|
else OwnAddressType.RANDOM
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
if mode in ('classic', 'dual'):
|
||||||
# Become discoverable and connectable
|
# Become discoverable and connectable
|
||||||
await device.set_discoverable(True)
|
await device.set_discoverable(True)
|
||||||
await device.set_connectable(True)
|
await device.set_connectable(True)
|
||||||
|
print(color('Ready for connections on', 'blue'), device.public_address)
|
||||||
|
|
||||||
# Run until the user asks to exit
|
# Run until the user asks to exit
|
||||||
await Waiter.instance.wait_until_terminated()
|
await Waiter.instance.wait_until_terminated()
|
||||||
@@ -461,7 +591,10 @@ class LogHandler(logging.Handler):
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.option(
|
@click.option(
|
||||||
'--mode', type=click.Choice(['le', 'classic']), default='le', show_default=True
|
'--mode',
|
||||||
|
type=click.Choice(['le', 'classic', 'dual']),
|
||||||
|
default='le',
|
||||||
|
show_default=True,
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'--sc',
|
'--sc',
|
||||||
@@ -483,6 +616,10 @@ class LogHandler(logging.Handler):
|
|||||||
help='Enable CTKD',
|
help='Enable CTKD',
|
||||||
show_default=True,
|
show_default=True,
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
'--advertising-address',
|
||||||
|
type=click.Choice(['random', 'public']),
|
||||||
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'--identity-address',
|
'--identity-address',
|
||||||
type=click.Choice(['random', 'public']),
|
type=click.Choice(['random', 'public']),
|
||||||
@@ -511,9 +648,20 @@ class LogHandler(logging.Handler):
|
|||||||
@click.option('--print-keys', is_flag=True, help='Print the bond keys before pairing')
|
@click.option('--print-keys', is_flag=True, help='Print the bond keys before pairing')
|
||||||
@click.option(
|
@click.option(
|
||||||
'--keystore-file',
|
'--keystore-file',
|
||||||
metavar='<filename>',
|
metavar='FILENAME',
|
||||||
help='File in which to store the pairing keys',
|
help='File in which to store the pairing keys',
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
'--advertise-service-uuid',
|
||||||
|
metavar="UUID",
|
||||||
|
multiple=True,
|
||||||
|
help="Advertise a GATT service UUID (may be specified more than once)",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
'--advertise-appearance',
|
||||||
|
metavar='APPEARANCE',
|
||||||
|
help='Advertise an Appearance ID (int value or string)',
|
||||||
|
)
|
||||||
@click.argument('device-config')
|
@click.argument('device-config')
|
||||||
@click.argument('hci_transport')
|
@click.argument('hci_transport')
|
||||||
@click.argument('address-or-name', required=False)
|
@click.argument('address-or-name', required=False)
|
||||||
@@ -523,6 +671,7 @@ def main(
|
|||||||
mitm,
|
mitm,
|
||||||
bond,
|
bond,
|
||||||
ctkd,
|
ctkd,
|
||||||
|
advertising_address,
|
||||||
identity_address,
|
identity_address,
|
||||||
linger,
|
linger,
|
||||||
io,
|
io,
|
||||||
@@ -531,6 +680,8 @@ def main(
|
|||||||
request,
|
request,
|
||||||
print_keys,
|
print_keys,
|
||||||
keystore_file,
|
keystore_file,
|
||||||
|
advertise_service_uuid,
|
||||||
|
advertise_appearance,
|
||||||
device_config,
|
device_config,
|
||||||
hci_transport,
|
hci_transport,
|
||||||
address_or_name,
|
address_or_name,
|
||||||
@@ -549,6 +700,7 @@ def main(
|
|||||||
mitm,
|
mitm,
|
||||||
bond,
|
bond,
|
||||||
ctkd,
|
ctkd,
|
||||||
|
advertising_address,
|
||||||
identity_address,
|
identity_address,
|
||||||
linger,
|
linger,
|
||||||
io,
|
io,
|
||||||
@@ -557,6 +709,8 @@ def main(
|
|||||||
request,
|
request,
|
||||||
print_keys,
|
print_keys,
|
||||||
keystore_file,
|
keystore_file,
|
||||||
|
advertise_service_uuid,
|
||||||
|
advertise_appearance,
|
||||||
device_config,
|
device_config,
|
||||||
hci_transport,
|
hci_transport,
|
||||||
address_or_name,
|
address_or_name,
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ from bumble.core import (
|
|||||||
AdvertisingData,
|
AdvertisingData,
|
||||||
ConnectionError as BumbleConnectionError,
|
ConnectionError as BumbleConnectionError,
|
||||||
DeviceClass,
|
DeviceClass,
|
||||||
BT_BR_EDR_TRANSPORT,
|
PhysicalTransport,
|
||||||
)
|
)
|
||||||
from bumble.device import Connection, Device, DeviceConfiguration
|
from bumble.device import Connection, Device, DeviceConfiguration
|
||||||
from bumble.hci import Address, HCI_CONNECTION_ALREADY_EXISTS_ERROR, HCI_Constant
|
from bumble.hci import Address, HCI_CONNECTION_ALREADY_EXISTS_ERROR, HCI_Constant
|
||||||
@@ -286,7 +286,7 @@ class Player:
|
|||||||
|
|
||||||
async def connect(self, device: Device, address: str) -> Connection:
|
async def connect(self, device: Device, address: str) -> Connection:
|
||||||
print(color(f"Connecting to {address}...", "green"))
|
print(color(f"Connecting to {address}...", "green"))
|
||||||
connection = await device.connect(address, transport=BT_BR_EDR_TRANSPORT)
|
connection = await device.connect(address, transport=PhysicalTransport.BR_EDR)
|
||||||
|
|
||||||
# Request authentication
|
# Request authentication
|
||||||
if self.authenticate:
|
if self.authenticate:
|
||||||
@@ -402,7 +402,7 @@ class Player:
|
|||||||
|
|
||||||
async def pair(self, device: Device, address: str) -> None:
|
async def pair(self, device: Device, address: str) -> None:
|
||||||
print(color(f"Connecting to {address}...", "green"))
|
print(color(f"Connecting to {address}...", "green"))
|
||||||
connection = await device.connect(address, transport=BT_BR_EDR_TRANSPORT)
|
connection = await device.connect(address, transport=PhysicalTransport.BR_EDR)
|
||||||
|
|
||||||
print(color("Pairing...", "magenta"))
|
print(color("Pairing...", "magenta"))
|
||||||
await connection.authenticate()
|
await connection.authenticate()
|
||||||
|
|||||||
@@ -271,7 +271,7 @@ class ClientBridge:
|
|||||||
print(color(f"@@@ Connecting to Bluetooth {self.address}", "blue"))
|
print(color(f"@@@ Connecting to Bluetooth {self.address}", "blue"))
|
||||||
assert self.device
|
assert self.device
|
||||||
self.connection = await self.device.connect(
|
self.connection = await self.device.connect(
|
||||||
self.address, transport=core.BT_BR_EDR_TRANSPORT
|
self.address, transport=core.PhysicalTransport.BR_EDR
|
||||||
)
|
)
|
||||||
print(color(f"@@@ Bluetooth connection: {self.connection}", "blue"))
|
print(color(f"@@@ Bluetooth connection: {self.connection}", "blue"))
|
||||||
self.connection.on("disconnection", self.on_disconnection)
|
self.connection.on("disconnection", self.on_disconnection)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
<tr><td>Codec</td><td><span id="codecText"></span></td></tr>
|
<tr><td>Codec</td><td><span id="codecText"></span></td></tr>
|
||||||
<tr><td>Packets</td><td><span id="packetsReceivedText"></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>Bytes</td><td><span id="bytesReceivedText"></span></td></tr>
|
||||||
|
<tr><td>Bitrate</td><td><span id="bitrate"></span></td></tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
+113
-62
@@ -7,17 +7,19 @@ let connectionText;
|
|||||||
let codecText;
|
let codecText;
|
||||||
let packetsReceivedText;
|
let packetsReceivedText;
|
||||||
let bytesReceivedText;
|
let bytesReceivedText;
|
||||||
|
let bitrateText;
|
||||||
let streamStateText;
|
let streamStateText;
|
||||||
let connectionStateText;
|
let connectionStateText;
|
||||||
let controlsDiv;
|
let controlsDiv;
|
||||||
let audioOnButton;
|
let audioOnButton;
|
||||||
let mediaSource;
|
let audioDecoder;
|
||||||
let sourceBuffer;
|
let audioCodec;
|
||||||
let audioElement;
|
|
||||||
let audioContext;
|
let audioContext;
|
||||||
let audioAnalyzer;
|
let audioAnalyzer;
|
||||||
let audioFrequencyBinCount;
|
let audioFrequencyBinCount;
|
||||||
let audioFrequencyData;
|
let audioFrequencyData;
|
||||||
|
let nextAudioStartPosition = 0;
|
||||||
|
let audioStartTime = 0;
|
||||||
let packetsReceived = 0;
|
let packetsReceived = 0;
|
||||||
let bytesReceived = 0;
|
let bytesReceived = 0;
|
||||||
let audioState = "stopped";
|
let audioState = "stopped";
|
||||||
@@ -29,20 +31,17 @@ let bandwidthCanvas;
|
|||||||
let bandwidthCanvasContext;
|
let bandwidthCanvasContext;
|
||||||
let bandwidthBinCount;
|
let bandwidthBinCount;
|
||||||
let bandwidthBins = [];
|
let bandwidthBins = [];
|
||||||
|
let bitrateSamples = [];
|
||||||
|
|
||||||
const FFT_WIDTH = 800;
|
const FFT_WIDTH = 800;
|
||||||
const FFT_HEIGHT = 256;
|
const FFT_HEIGHT = 256;
|
||||||
const BANDWIDTH_WIDTH = 500;
|
const BANDWIDTH_WIDTH = 500;
|
||||||
const BANDWIDTH_HEIGHT = 100;
|
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() {
|
function init() {
|
||||||
initUI();
|
initUI();
|
||||||
initMediaSource();
|
initAudioContext();
|
||||||
initAudioElement();
|
|
||||||
initAnalyzer();
|
initAnalyzer();
|
||||||
|
|
||||||
connect();
|
connect();
|
||||||
@@ -56,6 +55,7 @@ function initUI() {
|
|||||||
codecText = document.getElementById("codecText");
|
codecText = document.getElementById("codecText");
|
||||||
packetsReceivedText = document.getElementById("packetsReceivedText");
|
packetsReceivedText = document.getElementById("packetsReceivedText");
|
||||||
bytesReceivedText = document.getElementById("bytesReceivedText");
|
bytesReceivedText = document.getElementById("bytesReceivedText");
|
||||||
|
bitrateText = document.getElementById("bitrate");
|
||||||
streamStateText = document.getElementById("streamStateText");
|
streamStateText = document.getElementById("streamStateText");
|
||||||
connectionStateText = document.getElementById("connectionStateText");
|
connectionStateText = document.getElementById("connectionStateText");
|
||||||
audioSupportMessageText = document.getElementById("audioSupportMessageText");
|
audioSupportMessageText = document.getElementById("audioSupportMessageText");
|
||||||
@@ -67,17 +67,9 @@ function initUI() {
|
|||||||
requestAnimationFrame(onAnimationFrame);
|
requestAnimationFrame(onAnimationFrame);
|
||||||
}
|
}
|
||||||
|
|
||||||
function initMediaSource() {
|
function initAudioContext() {
|
||||||
mediaSource = new MediaSource();
|
audioContext = new AudioContext();
|
||||||
mediaSource.onsourceopen = onMediaSourceOpen;
|
audioContext.onstatechange = () => console.log("AudioContext state:", audioContext.state);
|
||||||
mediaSource.onsourceclose = onMediaSourceClose;
|
|
||||||
mediaSource.onsourceended = onMediaSourceEnd;
|
|
||||||
}
|
|
||||||
|
|
||||||
function initAudioElement() {
|
|
||||||
audioElement = document.getElementById("audio");
|
|
||||||
audioElement.src = URL.createObjectURL(mediaSource);
|
|
||||||
// audioElement.controls = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function initAnalyzer() {
|
function initAnalyzer() {
|
||||||
@@ -94,24 +86,16 @@ function initAnalyzer() {
|
|||||||
bandwidthCanvasContext = bandwidthCanvas.getContext('2d');
|
bandwidthCanvasContext = bandwidthCanvas.getContext('2d');
|
||||||
bandwidthCanvasContext.fillStyle = "rgb(255, 255, 255)";
|
bandwidthCanvasContext.fillStyle = "rgb(255, 255, 255)";
|
||||||
bandwidthCanvasContext.fillRect(0, 0, BANDWIDTH_WIDTH, BANDWIDTH_HEIGHT);
|
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;
|
bandwidthBinCount = BANDWIDTH_WIDTH / 2;
|
||||||
bandwidthBins = [];
|
bandwidthBins = [];
|
||||||
|
bitrateSamples = [];
|
||||||
|
|
||||||
|
audioAnalyzer = audioContext.createAnalyser();
|
||||||
|
audioAnalyzer.fftSize = 128;
|
||||||
|
audioFrequencyBinCount = audioAnalyzer.frequencyBinCount;
|
||||||
|
audioFrequencyData = new Uint8Array(audioFrequencyBinCount);
|
||||||
|
|
||||||
|
audioAnalyzer.connect(audioContext.destination)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setConnectionText(message) {
|
function setConnectionText(message) {
|
||||||
@@ -148,7 +132,8 @@ function onAnimationFrame() {
|
|||||||
bandwidthCanvasContext.fillRect(0, 0, BANDWIDTH_WIDTH, BANDWIDTH_HEIGHT);
|
bandwidthCanvasContext.fillRect(0, 0, BANDWIDTH_WIDTH, BANDWIDTH_HEIGHT);
|
||||||
bandwidthCanvasContext.fillStyle = `rgb(100, 100, 100)`;
|
bandwidthCanvasContext.fillStyle = `rgb(100, 100, 100)`;
|
||||||
for (let t = 0; t < bandwidthBins.length; t++) {
|
for (let t = 0; t < bandwidthBins.length; t++) {
|
||||||
const lineHeight = (bandwidthBins[t] / 1000) * BANDWIDTH_HEIGHT;
|
const bytesReceived = bandwidthBins[t]
|
||||||
|
const lineHeight = (bytesReceived / 1000) * BANDWIDTH_HEIGHT;
|
||||||
bandwidthCanvasContext.fillRect(t * 2, BANDWIDTH_HEIGHT - lineHeight, 2, lineHeight);
|
bandwidthCanvasContext.fillRect(t * 2, BANDWIDTH_HEIGHT - lineHeight, 2, lineHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,28 +141,14 @@ function onAnimationFrame() {
|
|||||||
requestAnimationFrame(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() {
|
async function startAudio() {
|
||||||
try {
|
try {
|
||||||
console.log("starting audio...");
|
console.log("starting audio...");
|
||||||
audioOnButton.disabled = true;
|
audioOnButton.disabled = true;
|
||||||
audioState = "starting";
|
audioState = "starting";
|
||||||
await audioElement.play();
|
audioContext.resume();
|
||||||
console.log("audio started");
|
console.log("audio started");
|
||||||
audioState = "playing";
|
audioState = "playing";
|
||||||
startAnalyzer();
|
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
console.error(`play failed: ${error}`);
|
console.error(`play failed: ${error}`);
|
||||||
audioState = "stopped";
|
audioState = "stopped";
|
||||||
@@ -185,12 +156,47 @@ async function startAudio() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onAudioPacket(packet) {
|
function onDecodedAudio(audioData) {
|
||||||
if (audioState != "stopped") {
|
const bufferSource = audioContext.createBufferSource()
|
||||||
// Queue the audio packet.
|
|
||||||
sourceBuffer.appendBuffer(packet);
|
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}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
packetsReceived += 1;
|
||||||
packetsReceivedText.innerText = packetsReceived;
|
packetsReceivedText.innerText = packetsReceived;
|
||||||
bytesReceived += packet.byteLength;
|
bytesReceived += packet.byteLength;
|
||||||
@@ -200,6 +206,48 @@ function onAudioPacket(packet) {
|
|||||||
if (bandwidthBins.length > bandwidthBinCount) {
|
if (bandwidthBins.length > bandwidthBinCount) {
|
||||||
bandwidthBins.shift();
|
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() {
|
function onChannelOpen() {
|
||||||
@@ -249,16 +297,19 @@ function onChannelMessage(message) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onHelloMessage(params) {
|
async function onHelloMessage(params) {
|
||||||
codecText.innerText = params.codec;
|
codecText.innerText = params.codec;
|
||||||
if (params.codec != "aac") {
|
|
||||||
audioOnButton.disabled = true;
|
if (params.codec == "aac" || params.codec == "opus") {
|
||||||
audioSupportMessageText.innerText = "Only AAC can be played, audio will be disabled";
|
audioCodec = params.codec
|
||||||
audioSupportMessageText.style.display = "inline-block";
|
|
||||||
} else {
|
|
||||||
audioSupportMessageText.innerText = "";
|
audioSupportMessageText.innerText = "";
|
||||||
audioSupportMessageText.style.display = "none";
|
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) {
|
if (params.streamState) {
|
||||||
setStreamState(params.streamState);
|
setStreamState(params.streamState);
|
||||||
}
|
}
|
||||||
|
|||||||
+128
-18
@@ -34,7 +34,7 @@ from aiohttp import web
|
|||||||
|
|
||||||
import bumble
|
import bumble
|
||||||
from bumble.colors import color
|
from bumble.colors import color
|
||||||
from bumble.core import BT_BR_EDR_TRANSPORT, CommandTimeoutError
|
from bumble.core import PhysicalTransport, CommandTimeoutError
|
||||||
from bumble.device import Connection, Device, DeviceConfiguration
|
from bumble.device import Connection, Device, DeviceConfiguration
|
||||||
from bumble.hci import HCI_StatusError
|
from bumble.hci import HCI_StatusError
|
||||||
from bumble.pairing import PairingConfig
|
from bumble.pairing import PairingConfig
|
||||||
@@ -50,8 +50,10 @@ from bumble.a2dp import (
|
|||||||
make_audio_sink_service_sdp_records,
|
make_audio_sink_service_sdp_records,
|
||||||
A2DP_SBC_CODEC_TYPE,
|
A2DP_SBC_CODEC_TYPE,
|
||||||
A2DP_MPEG_2_4_AAC_CODEC_TYPE,
|
A2DP_MPEG_2_4_AAC_CODEC_TYPE,
|
||||||
|
A2DP_NON_A2DP_CODEC_TYPE,
|
||||||
SbcMediaCodecInformation,
|
SbcMediaCodecInformation,
|
||||||
AacMediaCodecInformation,
|
AacMediaCodecInformation,
|
||||||
|
OpusMediaCodecInformation,
|
||||||
)
|
)
|
||||||
from bumble.utils import AsyncRunner
|
from bumble.utils import AsyncRunner
|
||||||
from bumble.codecs import AacAudioRtpPacket
|
from bumble.codecs import AacAudioRtpPacket
|
||||||
@@ -78,6 +80,8 @@ class AudioExtractor:
|
|||||||
return AacAudioExtractor()
|
return AacAudioExtractor()
|
||||||
if codec == 'sbc':
|
if codec == 'sbc':
|
||||||
return SbcAudioExtractor()
|
return SbcAudioExtractor()
|
||||||
|
if codec == 'opus':
|
||||||
|
return OpusAudioExtractor()
|
||||||
|
|
||||||
def extract_audio(self, packet: MediaPacket) -> bytes:
|
def extract_audio(self, packet: MediaPacket) -> bytes:
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
@@ -102,6 +106,13 @@ class SbcAudioExtractor:
|
|||||||
return packet.payload[1:]
|
return packet.payload[1:]
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class OpusAudioExtractor:
|
||||||
|
def extract_audio(self, packet: MediaPacket) -> bytes:
|
||||||
|
# TODO: parse fields
|
||||||
|
return packet.payload[1:]
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class Output:
|
class Output:
|
||||||
async def start(self) -> None:
|
async def start(self) -> None:
|
||||||
@@ -235,7 +246,7 @@ class FfplayOutput(QueuedOutput):
|
|||||||
await super().start()
|
await super().start()
|
||||||
|
|
||||||
self.subprocess = await asyncio.create_subprocess_shell(
|
self.subprocess = await asyncio.create_subprocess_shell(
|
||||||
f'ffplay -f {self.codec} pipe:0',
|
f'ffplay -probesize 32 -f {self.codec} pipe:0',
|
||||||
stdin=asyncio.subprocess.PIPE,
|
stdin=asyncio.subprocess.PIPE,
|
||||||
stdout=asyncio.subprocess.PIPE,
|
stdout=asyncio.subprocess.PIPE,
|
||||||
stderr=asyncio.subprocess.PIPE,
|
stderr=asyncio.subprocess.PIPE,
|
||||||
@@ -399,10 +410,24 @@ class Speaker:
|
|||||||
STARTED = 2
|
STARTED = 2
|
||||||
SUSPENDED = 3
|
SUSPENDED = 3
|
||||||
|
|
||||||
def __init__(self, device_config, transport, codec, discover, outputs, ui_port):
|
def __init__(
|
||||||
|
self,
|
||||||
|
device_config,
|
||||||
|
transport,
|
||||||
|
codec,
|
||||||
|
sampling_frequencies,
|
||||||
|
bitrate,
|
||||||
|
vbr,
|
||||||
|
discover,
|
||||||
|
outputs,
|
||||||
|
ui_port,
|
||||||
|
):
|
||||||
self.device_config = device_config
|
self.device_config = device_config
|
||||||
self.transport = transport
|
self.transport = transport
|
||||||
self.codec = codec
|
self.codec = codec
|
||||||
|
self.sampling_frequencies = sampling_frequencies
|
||||||
|
self.bitrate = bitrate
|
||||||
|
self.vbr = vbr
|
||||||
self.discover = discover
|
self.discover = discover
|
||||||
self.ui_port = ui_port
|
self.ui_port = ui_port
|
||||||
self.device = None
|
self.device = None
|
||||||
@@ -438,32 +463,56 @@ class Speaker:
|
|||||||
if self.codec == 'sbc':
|
if self.codec == 'sbc':
|
||||||
return self.sbc_codec_capabilities()
|
return self.sbc_codec_capabilities()
|
||||||
|
|
||||||
|
if self.codec == 'opus':
|
||||||
|
return self.opus_codec_capabilities()
|
||||||
|
|
||||||
raise RuntimeError('unsupported codec')
|
raise RuntimeError('unsupported codec')
|
||||||
|
|
||||||
def aac_codec_capabilities(self) -> MediaCodecCapabilities:
|
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(
|
return MediaCodecCapabilities(
|
||||||
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
||||||
media_codec_type=A2DP_MPEG_2_4_AAC_CODEC_TYPE,
|
media_codec_type=A2DP_MPEG_2_4_AAC_CODEC_TYPE,
|
||||||
media_codec_information=AacMediaCodecInformation(
|
media_codec_information=AacMediaCodecInformation(
|
||||||
object_type=AacMediaCodecInformation.ObjectType.MPEG_2_AAC_LC,
|
object_type=AacMediaCodecInformation.ObjectType.MPEG_2_AAC_LC,
|
||||||
sampling_frequency=AacMediaCodecInformation.SamplingFrequency.SF_48000
|
sampling_frequency=supported_sampling_frequencies,
|
||||||
| AacMediaCodecInformation.SamplingFrequency.SF_44100,
|
|
||||||
channels=AacMediaCodecInformation.Channels.MONO
|
channels=AacMediaCodecInformation.Channels.MONO
|
||||||
| AacMediaCodecInformation.Channels.STEREO,
|
| AacMediaCodecInformation.Channels.STEREO,
|
||||||
vbr=1,
|
vbr=1 if self.vbr else 0,
|
||||||
bitrate=256000,
|
bitrate=self.bitrate or 256000,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def sbc_codec_capabilities(self) -> MediaCodecCapabilities:
|
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(
|
return MediaCodecCapabilities(
|
||||||
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
||||||
media_codec_type=A2DP_SBC_CODEC_TYPE,
|
media_codec_type=A2DP_SBC_CODEC_TYPE,
|
||||||
media_codec_information=SbcMediaCodecInformation(
|
media_codec_information=SbcMediaCodecInformation(
|
||||||
sampling_frequency=SbcMediaCodecInformation.SamplingFrequency.SF_48000
|
sampling_frequency=supported_sampling_frequencies,
|
||||||
| SbcMediaCodecInformation.SamplingFrequency.SF_44100
|
|
||||||
| SbcMediaCodecInformation.SamplingFrequency.SF_32000
|
|
||||||
| SbcMediaCodecInformation.SamplingFrequency.SF_16000,
|
|
||||||
channel_mode=SbcMediaCodecInformation.ChannelMode.MONO
|
channel_mode=SbcMediaCodecInformation.ChannelMode.MONO
|
||||||
| SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL
|
| SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL
|
||||||
| SbcMediaCodecInformation.ChannelMode.STEREO
|
| SbcMediaCodecInformation.ChannelMode.STEREO
|
||||||
@@ -481,6 +530,25 @@ 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):
|
async def dispatch_to_outputs(self, function):
|
||||||
for output in self.outputs:
|
for output in self.outputs:
|
||||||
await function(output)
|
await function(output)
|
||||||
@@ -568,7 +636,9 @@ class Speaker:
|
|||||||
async def connect(self, address):
|
async def connect(self, address):
|
||||||
# Connect to the source
|
# Connect to the source
|
||||||
print(f'=== Connecting to {address}...')
|
print(f'=== Connecting to {address}...')
|
||||||
connection = await self.device.connect(address, transport=BT_BR_EDR_TRANSPORT)
|
connection = await self.device.connect(
|
||||||
|
address, transport=PhysicalTransport.BR_EDR
|
||||||
|
)
|
||||||
print(f'=== Connected to {connection.peer_address}')
|
print(f'=== Connected to {connection.peer_address}')
|
||||||
|
|
||||||
# Request authentication
|
# Request authentication
|
||||||
@@ -673,7 +743,26 @@ def speaker_cli(ctx, device_config):
|
|||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.option(
|
@click.option(
|
||||||
'--codec', type=click.Choice(['sbc', 'aac']), default='aac', show_default=True
|
'--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)'
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'--discover', is_flag=True, help='Discover remote endpoints once connected'
|
'--discover', is_flag=True, help='Discover remote endpoints once connected'
|
||||||
@@ -704,7 +793,16 @@ def speaker_cli(ctx, device_config):
|
|||||||
@click.option('--device-config', metavar='FILENAME', help='Device configuration file')
|
@click.option('--device-config', metavar='FILENAME', help='Device configuration file')
|
||||||
@click.argument('transport')
|
@click.argument('transport')
|
||||||
def speaker(
|
def speaker(
|
||||||
transport, codec, connect_address, discover, output, ui_port, device_config
|
transport,
|
||||||
|
codec,
|
||||||
|
sampling_frequency,
|
||||||
|
bitrate,
|
||||||
|
vbr,
|
||||||
|
connect_address,
|
||||||
|
discover,
|
||||||
|
output,
|
||||||
|
ui_port,
|
||||||
|
device_config,
|
||||||
):
|
):
|
||||||
"""Run the speaker."""
|
"""Run the speaker."""
|
||||||
|
|
||||||
@@ -719,15 +817,27 @@ def speaker(
|
|||||||
output = list(filter(lambda x: x != '@ffplay', output))
|
output = list(filter(lambda x: x != '@ffplay', output))
|
||||||
|
|
||||||
asyncio.run(
|
asyncio.run(
|
||||||
Speaker(device_config, transport, codec, discover, output, ui_port).run(
|
Speaker(
|
||||||
connect_address
|
device_config,
|
||||||
)
|
transport,
|
||||||
|
codec,
|
||||||
|
sampling_frequency,
|
||||||
|
bitrate,
|
||||||
|
vbr,
|
||||||
|
discover,
|
||||||
|
output,
|
||||||
|
ui_port,
|
||||||
|
).run(connect_address)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
def main():
|
def main():
|
||||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
|
logging.basicConfig(
|
||||||
|
level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper(),
|
||||||
|
format="[%(asctime)s.%(msecs)03d] %(levelname)s:%(name)s:%(message)s",
|
||||||
|
datefmt="%H:%M:%S",
|
||||||
|
)
|
||||||
speaker()
|
speaker()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+15
-7
@@ -26,9 +26,9 @@ from typing import Awaitable, Callable
|
|||||||
from typing_extensions import ClassVar, Self
|
from typing_extensions import ClassVar, Self
|
||||||
|
|
||||||
|
|
||||||
from .codecs import AacAudioRtpPacket
|
from bumble.codecs import AacAudioRtpPacket
|
||||||
from .company_ids import COMPANY_IDENTIFIERS
|
from bumble.company_ids import COMPANY_IDENTIFIERS
|
||||||
from .sdp import (
|
from bumble.sdp import (
|
||||||
DataElement,
|
DataElement,
|
||||||
ServiceAttribute,
|
ServiceAttribute,
|
||||||
SDP_PUBLIC_BROWSE_ROOT,
|
SDP_PUBLIC_BROWSE_ROOT,
|
||||||
@@ -38,7 +38,7 @@ from .sdp import (
|
|||||||
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||||
SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||||
)
|
)
|
||||||
from .core import (
|
from bumble.core import (
|
||||||
BT_L2CAP_PROTOCOL_ID,
|
BT_L2CAP_PROTOCOL_ID,
|
||||||
BT_AUDIO_SOURCE_SERVICE,
|
BT_AUDIO_SOURCE_SERVICE,
|
||||||
BT_AUDIO_SINK_SERVICE,
|
BT_AUDIO_SINK_SERVICE,
|
||||||
@@ -46,7 +46,7 @@ from .core import (
|
|||||||
BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE,
|
BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE,
|
||||||
name_or_number,
|
name_or_number,
|
||||||
)
|
)
|
||||||
from .rtp import MediaPacket
|
from bumble.rtp import MediaPacket
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -155,7 +155,7 @@ def flags_to_list(flags, values):
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
def make_audio_source_service_sdp_records(service_record_handle, version=(1, 3)):
|
def make_audio_source_service_sdp_records(service_record_handle, version=(1, 3)):
|
||||||
# pylint: disable=import-outside-toplevel
|
# pylint: disable=import-outside-toplevel
|
||||||
from .avdtp import AVDTP_PSM
|
from bumble.avdtp import AVDTP_PSM
|
||||||
|
|
||||||
version_int = version[0] << 8 | version[1]
|
version_int = version[0] << 8 | version[1]
|
||||||
return [
|
return [
|
||||||
@@ -209,7 +209,7 @@ def make_audio_source_service_sdp_records(service_record_handle, version=(1, 3))
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
def make_audio_sink_service_sdp_records(service_record_handle, version=(1, 3)):
|
def make_audio_sink_service_sdp_records(service_record_handle, version=(1, 3)):
|
||||||
# pylint: disable=import-outside-toplevel
|
# pylint: disable=import-outside-toplevel
|
||||||
from .avdtp import AVDTP_PSM
|
from bumble.avdtp import AVDTP_PSM
|
||||||
|
|
||||||
version_int = version[0] << 8 | version[1]
|
version_int = version[0] << 8 | version[1]
|
||||||
return [
|
return [
|
||||||
@@ -479,6 +479,14 @@ class OpusMediaCodecInformation(VendorSpecificMediaCodecInformation):
|
|||||||
class SamplingFrequency(enum.IntFlag):
|
class SamplingFrequency(enum.IntFlag):
|
||||||
SF_48000 = 1 << 0
|
SF_48000 = 1 << 0
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_int(
|
||||||
|
cls, sampling_frequency: int
|
||||||
|
) -> OpusMediaCodecInformation.SamplingFrequency:
|
||||||
|
if sampling_frequency != 48000:
|
||||||
|
raise ValueError("no such sampling frequency")
|
||||||
|
return cls.SF_48000
|
||||||
|
|
||||||
VENDOR_ID: ClassVar[int] = 0x000000E0
|
VENDOR_ID: ClassVar[int] = 0x000000E0
|
||||||
CODEC_ID: ClassVar[int] = 0x0001
|
CODEC_ID: ClassVar[int] = 0x0001
|
||||||
|
|
||||||
|
|||||||
+15
-15
@@ -41,7 +41,6 @@ from typing import (
|
|||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
)
|
)
|
||||||
|
|
||||||
from pyee import EventEmitter
|
|
||||||
|
|
||||||
from bumble import utils
|
from bumble import utils
|
||||||
from bumble.core import UUID, name_or_number, InvalidOperationError, ProtocolError
|
from bumble.core import UUID, name_or_number, InvalidOperationError, ProtocolError
|
||||||
@@ -771,34 +770,32 @@ class AttributeValue(Generic[_T]):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
read: Union[
|
read: Union[
|
||||||
Callable[[Optional[Connection]], _T],
|
Callable[[Connection], _T],
|
||||||
Callable[[Optional[Connection]], Awaitable[_T]],
|
Callable[[Connection], Awaitable[_T]],
|
||||||
None,
|
None,
|
||||||
] = None,
|
] = None,
|
||||||
write: Union[
|
write: Union[
|
||||||
Callable[[Optional[Connection], _T], None],
|
Callable[[Connection, _T], None],
|
||||||
Callable[[Optional[Connection], _T], Awaitable[None]],
|
Callable[[Connection, _T], Awaitable[None]],
|
||||||
None,
|
None,
|
||||||
] = None,
|
] = None,
|
||||||
):
|
):
|
||||||
self._read = read
|
self._read = read
|
||||||
self._write = write
|
self._write = write
|
||||||
|
|
||||||
def read(self, connection: Optional[Connection]) -> Union[_T, Awaitable[_T]]:
|
def read(self, connection: Connection) -> Union[_T, Awaitable[_T]]:
|
||||||
if self._read is None:
|
if self._read is None:
|
||||||
raise InvalidOperationError('AttributeValue has no read function')
|
raise InvalidOperationError('AttributeValue has no read function')
|
||||||
return self._read(connection)
|
return self._read(connection)
|
||||||
|
|
||||||
def write(
|
def write(self, connection: Connection, value: _T) -> Union[Awaitable[None], None]:
|
||||||
self, connection: Optional[Connection], value: _T
|
|
||||||
) -> Union[Awaitable[None], None]:
|
|
||||||
if self._write is None:
|
if self._write is None:
|
||||||
raise InvalidOperationError('AttributeValue has no write function')
|
raise InvalidOperationError('AttributeValue has no write function')
|
||||||
return self._write(connection, value)
|
return self._write(connection, value)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class Attribute(EventEmitter, Generic[_T]):
|
class Attribute(utils.EventEmitter, Generic[_T]):
|
||||||
class Permissions(enum.IntFlag):
|
class Permissions(enum.IntFlag):
|
||||||
READABLE = 0x01
|
READABLE = 0x01
|
||||||
WRITEABLE = 0x02
|
WRITEABLE = 0x02
|
||||||
@@ -837,6 +834,9 @@ class Attribute(EventEmitter, Generic[_T]):
|
|||||||
READ_REQUIRES_AUTHORIZATION = Permissions.READ_REQUIRES_AUTHORIZATION
|
READ_REQUIRES_AUTHORIZATION = Permissions.READ_REQUIRES_AUTHORIZATION
|
||||||
WRITE_REQUIRES_AUTHORIZATION = Permissions.WRITE_REQUIRES_AUTHORIZATION
|
WRITE_REQUIRES_AUTHORIZATION = Permissions.WRITE_REQUIRES_AUTHORIZATION
|
||||||
|
|
||||||
|
EVENT_READ = "read"
|
||||||
|
EVENT_WRITE = "write"
|
||||||
|
|
||||||
value: Union[AttributeValue[_T], _T, None]
|
value: Union[AttributeValue[_T], _T, None]
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -845,7 +845,7 @@ class Attribute(EventEmitter, Generic[_T]):
|
|||||||
permissions: Union[str, Attribute.Permissions],
|
permissions: Union[str, Attribute.Permissions],
|
||||||
value: Union[AttributeValue[_T], _T, None] = None,
|
value: Union[AttributeValue[_T], _T, None] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
EventEmitter.__init__(self)
|
utils.EventEmitter.__init__(self)
|
||||||
self.handle = 0
|
self.handle = 0
|
||||||
self.end_group_handle = 0
|
self.end_group_handle = 0
|
||||||
if isinstance(permissions, str):
|
if isinstance(permissions, str):
|
||||||
@@ -869,7 +869,7 @@ class Attribute(EventEmitter, Generic[_T]):
|
|||||||
def decode_value(self, value: bytes) -> _T:
|
def decode_value(self, value: bytes) -> _T:
|
||||||
return value # type: ignore
|
return value # type: ignore
|
||||||
|
|
||||||
async def read_value(self, connection: Optional[Connection]) -> bytes:
|
async def read_value(self, connection: Connection) -> bytes:
|
||||||
if (
|
if (
|
||||||
(self.permissions & self.READ_REQUIRES_ENCRYPTION)
|
(self.permissions & self.READ_REQUIRES_ENCRYPTION)
|
||||||
and connection is not None
|
and connection is not None
|
||||||
@@ -907,11 +907,11 @@ class Attribute(EventEmitter, Generic[_T]):
|
|||||||
else:
|
else:
|
||||||
value = self.value
|
value = self.value
|
||||||
|
|
||||||
self.emit('read', connection, b'' if value is None else value)
|
self.emit(self.EVENT_READ, connection, b'' if value is None else value)
|
||||||
|
|
||||||
return b'' if value is None else self.encode_value(value)
|
return b'' if value is None else self.encode_value(value)
|
||||||
|
|
||||||
async def write_value(self, connection: Optional[Connection], value: bytes) -> None:
|
async def write_value(self, connection: Connection, value: bytes) -> None:
|
||||||
if (
|
if (
|
||||||
(self.permissions & self.WRITE_REQUIRES_ENCRYPTION)
|
(self.permissions & self.WRITE_REQUIRES_ENCRYPTION)
|
||||||
and connection is not None
|
and connection is not None
|
||||||
@@ -948,7 +948,7 @@ class Attribute(EventEmitter, Generic[_T]):
|
|||||||
else:
|
else:
|
||||||
self.value = decoded_value
|
self.value = decoded_value
|
||||||
|
|
||||||
self.emit('write', connection, decoded_value)
|
self.emit(self.EVENT_WRITE, connection, decoded_value)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
if isinstance(self.value, bytes):
|
if isinstance(self.value, bytes):
|
||||||
|
|||||||
+5
-5
@@ -21,7 +21,7 @@ import struct
|
|||||||
from typing import Dict, Type, Union, Tuple
|
from typing import Dict, Type, Union, Tuple
|
||||||
|
|
||||||
from bumble import core
|
from bumble import core
|
||||||
from bumble.utils import OpenIntEnum
|
from bumble import utils
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -43,7 +43,7 @@ class Frame:
|
|||||||
EXTENDED = 0x1E
|
EXTENDED = 0x1E
|
||||||
UNIT = 0x1F
|
UNIT = 0x1F
|
||||||
|
|
||||||
class OperationCode(OpenIntEnum):
|
class OperationCode(utils.OpenIntEnum):
|
||||||
# 0x00 - 0x0F: Unit and subunit commands
|
# 0x00 - 0x0F: Unit and subunit commands
|
||||||
VENDOR_DEPENDENT = 0x00
|
VENDOR_DEPENDENT = 0x00
|
||||||
RESERVE = 0x01
|
RESERVE = 0x01
|
||||||
@@ -204,7 +204,7 @@ class Frame:
|
|||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class CommandFrame(Frame):
|
class CommandFrame(Frame):
|
||||||
class CommandType(OpenIntEnum):
|
class CommandType(utils.OpenIntEnum):
|
||||||
# AV/C Digital Interface Command Set General Specification Version 4.1
|
# AV/C Digital Interface Command Set General Specification Version 4.1
|
||||||
# Table 7.1
|
# Table 7.1
|
||||||
CONTROL = 0x00
|
CONTROL = 0x00
|
||||||
@@ -240,7 +240,7 @@ class CommandFrame(Frame):
|
|||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class ResponseFrame(Frame):
|
class ResponseFrame(Frame):
|
||||||
class ResponseCode(OpenIntEnum):
|
class ResponseCode(utils.OpenIntEnum):
|
||||||
# AV/C Digital Interface Command Set General Specification Version 4.1
|
# AV/C Digital Interface Command Set General Specification Version 4.1
|
||||||
# Table 7.2
|
# Table 7.2
|
||||||
NOT_IMPLEMENTED = 0x08
|
NOT_IMPLEMENTED = 0x08
|
||||||
@@ -368,7 +368,7 @@ class PassThroughFrame:
|
|||||||
PRESSED = 0
|
PRESSED = 0
|
||||||
RELEASED = 1
|
RELEASED = 1
|
||||||
|
|
||||||
class OperationId(OpenIntEnum):
|
class OperationId(utils.OpenIntEnum):
|
||||||
SELECT = 0x00
|
SELECT = 0x00
|
||||||
UP = 0x01
|
UP = 0x01
|
||||||
DOWN = 0x01
|
DOWN = 0x01
|
||||||
|
|||||||
+2
-2
@@ -166,8 +166,8 @@ class Protocol:
|
|||||||
|
|
||||||
# Register to receive PDUs from the channel
|
# Register to receive PDUs from the channel
|
||||||
l2cap_channel.sink = self.on_pdu
|
l2cap_channel.sink = self.on_pdu
|
||||||
l2cap_channel.on("open", self.on_l2cap_channel_open)
|
l2cap_channel.on(l2cap_channel.EVENT_OPEN, self.on_l2cap_channel_open)
|
||||||
l2cap_channel.on("close", self.on_l2cap_channel_close)
|
l2cap_channel.on(l2cap_channel.EVENT_CLOSE, self.on_l2cap_channel_close)
|
||||||
|
|
||||||
def on_l2cap_channel_open(self):
|
def on_l2cap_channel_open(self):
|
||||||
logger.debug(color("<<< AVCTP channel open", "magenta"))
|
logger.debug(color("<<< AVCTP channel open", "magenta"))
|
||||||
|
|||||||
+131
-78
@@ -37,16 +37,15 @@ from typing import (
|
|||||||
cast,
|
cast,
|
||||||
)
|
)
|
||||||
|
|
||||||
from pyee import EventEmitter
|
|
||||||
|
|
||||||
from .core import (
|
from bumble.core import (
|
||||||
BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE,
|
BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE,
|
||||||
InvalidStateError,
|
InvalidStateError,
|
||||||
ProtocolError,
|
ProtocolError,
|
||||||
InvalidArgumentError,
|
InvalidArgumentError,
|
||||||
name_or_number,
|
name_or_number,
|
||||||
)
|
)
|
||||||
from .a2dp import (
|
from bumble.a2dp import (
|
||||||
A2DP_CODEC_TYPE_NAMES,
|
A2DP_CODEC_TYPE_NAMES,
|
||||||
A2DP_MPEG_2_4_AAC_CODEC_TYPE,
|
A2DP_MPEG_2_4_AAC_CODEC_TYPE,
|
||||||
A2DP_NON_A2DP_CODEC_TYPE,
|
A2DP_NON_A2DP_CODEC_TYPE,
|
||||||
@@ -56,9 +55,9 @@ from .a2dp import (
|
|||||||
SbcMediaCodecInformation,
|
SbcMediaCodecInformation,
|
||||||
VendorSpecificMediaCodecInformation,
|
VendorSpecificMediaCodecInformation,
|
||||||
)
|
)
|
||||||
from .rtp import MediaPacket
|
from bumble.rtp import MediaPacket
|
||||||
from . import sdp, device, l2cap
|
from bumble import sdp, device, l2cap, utils
|
||||||
from .colors import color
|
from bumble.colors import color
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -897,7 +896,7 @@ class Set_Configuration_Reject(Message):
|
|||||||
self.service_category = self.payload[0]
|
self.service_category = self.payload[0]
|
||||||
self.error_code = self.payload[1]
|
self.error_code = self.payload[1]
|
||||||
|
|
||||||
def __init__(self, service_category, error_code):
|
def __init__(self, error_code: int, service_category: int = 0) -> None:
|
||||||
super().__init__(payload=bytes([service_category, error_code]))
|
super().__init__(payload=bytes([service_category, error_code]))
|
||||||
self.service_category = service_category
|
self.service_category = service_category
|
||||||
self.error_code = error_code
|
self.error_code = error_code
|
||||||
@@ -1133,6 +1132,14 @@ class Security_Control_Command(Message):
|
|||||||
See Bluetooth AVDTP spec - 8.17.1 Security Control Command
|
See Bluetooth AVDTP spec - 8.17.1 Security Control Command
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
def init_from_payload(self):
|
||||||
|
# pylint: disable=attribute-defined-outside-init
|
||||||
|
self.acp_seid = self.payload[0] >> 2
|
||||||
|
self.data = self.payload[1:]
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.to_string([f'ACP_SEID: {self.acp_seid}', f'data: {self.data}'])
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@Message.subclass
|
@Message.subclass
|
||||||
@@ -1194,13 +1201,16 @@ class DelayReport_Reject(Simple_Reject):
|
|||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class Protocol(EventEmitter):
|
class Protocol(utils.EventEmitter):
|
||||||
local_endpoints: List[LocalStreamEndPoint]
|
local_endpoints: List[LocalStreamEndPoint]
|
||||||
remote_endpoints: Dict[int, DiscoveredStreamEndPoint]
|
remote_endpoints: Dict[int, DiscoveredStreamEndPoint]
|
||||||
streams: Dict[int, Stream]
|
streams: Dict[int, Stream]
|
||||||
transaction_results: List[Optional[asyncio.Future[Message]]]
|
transaction_results: List[Optional[asyncio.Future[Message]]]
|
||||||
channel_connector: Callable[[], Awaitable[l2cap.ClassicChannel]]
|
channel_connector: Callable[[], Awaitable[l2cap.ClassicChannel]]
|
||||||
|
|
||||||
|
EVENT_OPEN = "open"
|
||||||
|
EVENT_CLOSE = "close"
|
||||||
|
|
||||||
class PacketType(enum.IntEnum):
|
class PacketType(enum.IntEnum):
|
||||||
SINGLE_PACKET = 0
|
SINGLE_PACKET = 0
|
||||||
START_PACKET = 1
|
START_PACKET = 1
|
||||||
@@ -1240,8 +1250,8 @@ class Protocol(EventEmitter):
|
|||||||
|
|
||||||
# Register to receive PDUs from the channel
|
# Register to receive PDUs from the channel
|
||||||
l2cap_channel.sink = self.on_pdu
|
l2cap_channel.sink = self.on_pdu
|
||||||
l2cap_channel.on('open', self.on_l2cap_channel_open)
|
l2cap_channel.on(l2cap_channel.EVENT_OPEN, self.on_l2cap_channel_open)
|
||||||
l2cap_channel.on('close', self.on_l2cap_channel_close)
|
l2cap_channel.on(l2cap_channel.EVENT_CLOSE, self.on_l2cap_channel_close)
|
||||||
|
|
||||||
def get_local_endpoint_by_seid(self, seid: int) -> Optional[LocalStreamEndPoint]:
|
def get_local_endpoint_by_seid(self, seid: int) -> Optional[LocalStreamEndPoint]:
|
||||||
if 0 < seid <= len(self.local_endpoints):
|
if 0 < seid <= len(self.local_endpoints):
|
||||||
@@ -1411,20 +1421,20 @@ class Protocol(EventEmitter):
|
|||||||
self.transaction_results[transaction_label] = None
|
self.transaction_results[transaction_label] = None
|
||||||
self.transaction_semaphore.release()
|
self.transaction_semaphore.release()
|
||||||
|
|
||||||
def on_l2cap_connection(self, channel):
|
def on_l2cap_connection(self, channel: l2cap.ClassicChannel) -> None:
|
||||||
# Forward the channel to the endpoint that's expecting it
|
# Forward the channel to the endpoint that's expecting it
|
||||||
if self.channel_acceptor is None:
|
if self.channel_acceptor is None:
|
||||||
logger.warning(color('!!! l2cap connection with no acceptor', 'red'))
|
logger.warning(color('!!! l2cap connection with no acceptor', 'red'))
|
||||||
return
|
return
|
||||||
self.channel_acceptor.on_l2cap_connection(channel)
|
self.channel_acceptor.on_l2cap_connection(channel)
|
||||||
|
|
||||||
def on_l2cap_channel_open(self):
|
def on_l2cap_channel_open(self) -> None:
|
||||||
logger.debug(color('<<< L2CAP channel open', 'magenta'))
|
logger.debug(color('<<< L2CAP channel open', 'magenta'))
|
||||||
self.emit('open')
|
self.emit(self.EVENT_OPEN)
|
||||||
|
|
||||||
def on_l2cap_channel_close(self):
|
def on_l2cap_channel_close(self) -> None:
|
||||||
logger.debug(color('<<< L2CAP channel close', 'magenta'))
|
logger.debug(color('<<< L2CAP channel close', 'magenta'))
|
||||||
self.emit('close')
|
self.emit(self.EVENT_CLOSE)
|
||||||
|
|
||||||
def send_message(self, transaction_label: int, message: Message) -> None:
|
def send_message(self, transaction_label: int, message: Message) -> None:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
@@ -1542,28 +1552,34 @@ class Protocol(EventEmitter):
|
|||||||
async def abort(self, seid: int) -> Abort_Response:
|
async def abort(self, seid: int) -> Abort_Response:
|
||||||
return await self.send_command(Abort_Command(seid))
|
return await self.send_command(Abort_Command(seid))
|
||||||
|
|
||||||
def on_discover_command(self, _command):
|
def on_discover_command(self, command: Discover_Command) -> Optional[Message]:
|
||||||
endpoint_infos = [
|
endpoint_infos = [
|
||||||
EndPointInfo(endpoint.seid, 0, endpoint.media_type, endpoint.tsep)
|
EndPointInfo(endpoint.seid, 0, endpoint.media_type, endpoint.tsep)
|
||||||
for endpoint in self.local_endpoints
|
for endpoint in self.local_endpoints
|
||||||
]
|
]
|
||||||
return Discover_Response(endpoint_infos)
|
return Discover_Response(endpoint_infos)
|
||||||
|
|
||||||
def on_get_capabilities_command(self, command):
|
def on_get_capabilities_command(
|
||||||
|
self, command: Get_Capabilities_Command
|
||||||
|
) -> Optional[Message]:
|
||||||
endpoint = self.get_local_endpoint_by_seid(command.acp_seid)
|
endpoint = self.get_local_endpoint_by_seid(command.acp_seid)
|
||||||
if endpoint is None:
|
if endpoint is None:
|
||||||
return Get_Capabilities_Reject(AVDTP_BAD_ACP_SEID_ERROR)
|
return Get_Capabilities_Reject(AVDTP_BAD_ACP_SEID_ERROR)
|
||||||
|
|
||||||
return Get_Capabilities_Response(endpoint.capabilities)
|
return Get_Capabilities_Response(endpoint.capabilities)
|
||||||
|
|
||||||
def on_get_all_capabilities_command(self, command):
|
def on_get_all_capabilities_command(
|
||||||
|
self, command: Get_All_Capabilities_Command
|
||||||
|
) -> Optional[Message]:
|
||||||
endpoint = self.get_local_endpoint_by_seid(command.acp_seid)
|
endpoint = self.get_local_endpoint_by_seid(command.acp_seid)
|
||||||
if endpoint is None:
|
if endpoint is None:
|
||||||
return Get_All_Capabilities_Reject(AVDTP_BAD_ACP_SEID_ERROR)
|
return Get_All_Capabilities_Reject(AVDTP_BAD_ACP_SEID_ERROR)
|
||||||
|
|
||||||
return Get_All_Capabilities_Response(endpoint.capabilities)
|
return Get_All_Capabilities_Response(endpoint.capabilities)
|
||||||
|
|
||||||
def on_set_configuration_command(self, command):
|
def on_set_configuration_command(
|
||||||
|
self, command: Set_Configuration_Command
|
||||||
|
) -> Optional[Message]:
|
||||||
endpoint = self.get_local_endpoint_by_seid(command.acp_seid)
|
endpoint = self.get_local_endpoint_by_seid(command.acp_seid)
|
||||||
if endpoint is None:
|
if endpoint is None:
|
||||||
return Set_Configuration_Reject(AVDTP_BAD_ACP_SEID_ERROR)
|
return Set_Configuration_Reject(AVDTP_BAD_ACP_SEID_ERROR)
|
||||||
@@ -1579,7 +1595,9 @@ class Protocol(EventEmitter):
|
|||||||
result = stream.on_set_configuration_command(command.capabilities)
|
result = stream.on_set_configuration_command(command.capabilities)
|
||||||
return result or Set_Configuration_Response()
|
return result or Set_Configuration_Response()
|
||||||
|
|
||||||
def on_get_configuration_command(self, command):
|
def on_get_configuration_command(
|
||||||
|
self, command: Get_Configuration_Command
|
||||||
|
) -> Optional[Message]:
|
||||||
endpoint = self.get_local_endpoint_by_seid(command.acp_seid)
|
endpoint = self.get_local_endpoint_by_seid(command.acp_seid)
|
||||||
if endpoint is None:
|
if endpoint is None:
|
||||||
return Get_Configuration_Reject(AVDTP_BAD_ACP_SEID_ERROR)
|
return Get_Configuration_Reject(AVDTP_BAD_ACP_SEID_ERROR)
|
||||||
@@ -1588,7 +1606,7 @@ class Protocol(EventEmitter):
|
|||||||
|
|
||||||
return endpoint.stream.on_get_configuration_command()
|
return endpoint.stream.on_get_configuration_command()
|
||||||
|
|
||||||
def on_reconfigure_command(self, command):
|
def on_reconfigure_command(self, command: Reconfigure_Command) -> Optional[Message]:
|
||||||
endpoint = self.get_local_endpoint_by_seid(command.acp_seid)
|
endpoint = self.get_local_endpoint_by_seid(command.acp_seid)
|
||||||
if endpoint is None:
|
if endpoint is None:
|
||||||
return Reconfigure_Reject(0, AVDTP_BAD_ACP_SEID_ERROR)
|
return Reconfigure_Reject(0, AVDTP_BAD_ACP_SEID_ERROR)
|
||||||
@@ -1598,7 +1616,7 @@ class Protocol(EventEmitter):
|
|||||||
result = endpoint.stream.on_reconfigure_command(command.capabilities)
|
result = endpoint.stream.on_reconfigure_command(command.capabilities)
|
||||||
return result or Reconfigure_Response()
|
return result or Reconfigure_Response()
|
||||||
|
|
||||||
def on_open_command(self, command):
|
def on_open_command(self, command: Open_Command) -> Optional[Message]:
|
||||||
endpoint = self.get_local_endpoint_by_seid(command.acp_seid)
|
endpoint = self.get_local_endpoint_by_seid(command.acp_seid)
|
||||||
if endpoint is None:
|
if endpoint is None:
|
||||||
return Open_Reject(AVDTP_BAD_ACP_SEID_ERROR)
|
return Open_Reject(AVDTP_BAD_ACP_SEID_ERROR)
|
||||||
@@ -1608,25 +1626,26 @@ class Protocol(EventEmitter):
|
|||||||
result = endpoint.stream.on_open_command()
|
result = endpoint.stream.on_open_command()
|
||||||
return result or Open_Response()
|
return result or Open_Response()
|
||||||
|
|
||||||
def on_start_command(self, command):
|
def on_start_command(self, command: Start_Command) -> Optional[Message]:
|
||||||
for seid in command.acp_seids:
|
for seid in command.acp_seids:
|
||||||
endpoint = self.get_local_endpoint_by_seid(seid)
|
endpoint = self.get_local_endpoint_by_seid(seid)
|
||||||
if endpoint is None:
|
if endpoint is None:
|
||||||
return Start_Reject(seid, AVDTP_BAD_ACP_SEID_ERROR)
|
return Start_Reject(seid, AVDTP_BAD_ACP_SEID_ERROR)
|
||||||
if endpoint.stream is None:
|
if endpoint.stream is None:
|
||||||
return Start_Reject(AVDTP_BAD_STATE_ERROR)
|
return Start_Reject(seid, AVDTP_BAD_STATE_ERROR)
|
||||||
|
|
||||||
# Start all streams
|
# Start all streams
|
||||||
# TODO: deal with partial failures
|
# TODO: deal with partial failures
|
||||||
for seid in command.acp_seids:
|
for seid in command.acp_seids:
|
||||||
endpoint = self.get_local_endpoint_by_seid(seid)
|
endpoint = self.get_local_endpoint_by_seid(seid)
|
||||||
result = endpoint.stream.on_start_command()
|
if not endpoint or not endpoint.stream:
|
||||||
if result is not None:
|
raise InvalidStateError("Should already be checked!")
|
||||||
|
if (result := endpoint.stream.on_start_command()) is not None:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
return Start_Response()
|
return Start_Response()
|
||||||
|
|
||||||
def on_suspend_command(self, command):
|
def on_suspend_command(self, command: Suspend_Command) -> Optional[Message]:
|
||||||
for seid in command.acp_seids:
|
for seid in command.acp_seids:
|
||||||
endpoint = self.get_local_endpoint_by_seid(seid)
|
endpoint = self.get_local_endpoint_by_seid(seid)
|
||||||
if endpoint is None:
|
if endpoint is None:
|
||||||
@@ -1638,13 +1657,14 @@ class Protocol(EventEmitter):
|
|||||||
# TODO: deal with partial failures
|
# TODO: deal with partial failures
|
||||||
for seid in command.acp_seids:
|
for seid in command.acp_seids:
|
||||||
endpoint = self.get_local_endpoint_by_seid(seid)
|
endpoint = self.get_local_endpoint_by_seid(seid)
|
||||||
result = endpoint.stream.on_suspend_command()
|
if not endpoint or not endpoint.stream:
|
||||||
if result is not None:
|
raise InvalidStateError("Should already be checked!")
|
||||||
|
if (result := endpoint.stream.on_suspend_command()) is not None:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
return Suspend_Response()
|
return Suspend_Response()
|
||||||
|
|
||||||
def on_close_command(self, command):
|
def on_close_command(self, command: Close_Command) -> Optional[Message]:
|
||||||
endpoint = self.get_local_endpoint_by_seid(command.acp_seid)
|
endpoint = self.get_local_endpoint_by_seid(command.acp_seid)
|
||||||
if endpoint is None:
|
if endpoint is None:
|
||||||
return Close_Reject(AVDTP_BAD_ACP_SEID_ERROR)
|
return Close_Reject(AVDTP_BAD_ACP_SEID_ERROR)
|
||||||
@@ -1654,7 +1674,7 @@ class Protocol(EventEmitter):
|
|||||||
result = endpoint.stream.on_close_command()
|
result = endpoint.stream.on_close_command()
|
||||||
return result or Close_Response()
|
return result or Close_Response()
|
||||||
|
|
||||||
def on_abort_command(self, command):
|
def on_abort_command(self, command: Abort_Command) -> Optional[Message]:
|
||||||
endpoint = self.get_local_endpoint_by_seid(command.acp_seid)
|
endpoint = self.get_local_endpoint_by_seid(command.acp_seid)
|
||||||
if endpoint is None or endpoint.stream is None:
|
if endpoint is None or endpoint.stream is None:
|
||||||
return Abort_Response()
|
return Abort_Response()
|
||||||
@@ -1662,15 +1682,17 @@ class Protocol(EventEmitter):
|
|||||||
endpoint.stream.on_abort_command()
|
endpoint.stream.on_abort_command()
|
||||||
return Abort_Response()
|
return Abort_Response()
|
||||||
|
|
||||||
def on_security_control_command(self, command):
|
def on_security_control_command(
|
||||||
|
self, command: Security_Control_Command
|
||||||
|
) -> Optional[Message]:
|
||||||
endpoint = self.get_local_endpoint_by_seid(command.acp_seid)
|
endpoint = self.get_local_endpoint_by_seid(command.acp_seid)
|
||||||
if endpoint is None:
|
if endpoint is None:
|
||||||
return Security_Control_Reject(AVDTP_BAD_ACP_SEID_ERROR)
|
return Security_Control_Reject(AVDTP_BAD_ACP_SEID_ERROR)
|
||||||
|
|
||||||
result = endpoint.on_security_control_command(command.payload)
|
result = endpoint.on_security_control_command(command.data)
|
||||||
return result or Security_Control_Response()
|
return result or Security_Control_Response()
|
||||||
|
|
||||||
def on_delayreport_command(self, command):
|
def on_delayreport_command(self, command: DelayReport_Command) -> Optional[Message]:
|
||||||
endpoint = self.get_local_endpoint_by_seid(command.acp_seid)
|
endpoint = self.get_local_endpoint_by_seid(command.acp_seid)
|
||||||
if endpoint is None:
|
if endpoint is None:
|
||||||
return DelayReport_Reject(AVDTP_BAD_ACP_SEID_ERROR)
|
return DelayReport_Reject(AVDTP_BAD_ACP_SEID_ERROR)
|
||||||
@@ -1680,9 +1702,11 @@ class Protocol(EventEmitter):
|
|||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class Listener(EventEmitter):
|
class Listener(utils.EventEmitter):
|
||||||
servers: Dict[int, Protocol]
|
servers: Dict[int, Protocol]
|
||||||
|
|
||||||
|
EVENT_CONNECTION = "connection"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_registrar(device: device.Device):
|
def create_registrar(device: device.Device):
|
||||||
warnings.warn("Please use Listener.for_device()", DeprecationWarning)
|
warnings.warn("Please use Listener.for_device()", DeprecationWarning)
|
||||||
@@ -1717,7 +1741,7 @@ class Listener(EventEmitter):
|
|||||||
l2cap_server = device.create_l2cap_server(
|
l2cap_server = device.create_l2cap_server(
|
||||||
spec=l2cap.ClassicChannelSpec(psm=AVDTP_PSM)
|
spec=l2cap.ClassicChannelSpec(psm=AVDTP_PSM)
|
||||||
)
|
)
|
||||||
l2cap_server.on('connection', listener.on_l2cap_connection)
|
l2cap_server.on(l2cap_server.EVENT_CONNECTION, listener.on_l2cap_connection)
|
||||||
return listener
|
return listener
|
||||||
|
|
||||||
def on_l2cap_connection(self, channel: l2cap.ClassicChannel) -> None:
|
def on_l2cap_connection(self, channel: l2cap.ClassicChannel) -> None:
|
||||||
@@ -1733,14 +1757,14 @@ class Listener(EventEmitter):
|
|||||||
logger.debug('setting up new Protocol for the connection')
|
logger.debug('setting up new Protocol for the connection')
|
||||||
server = Protocol(channel, self.version)
|
server = Protocol(channel, self.version)
|
||||||
self.set_server(channel.connection, server)
|
self.set_server(channel.connection, server)
|
||||||
self.emit('connection', server)
|
self.emit(self.EVENT_CONNECTION, server)
|
||||||
|
|
||||||
def on_channel_close():
|
def on_channel_close():
|
||||||
logger.debug('removing Protocol for the connection')
|
logger.debug('removing Protocol for the connection')
|
||||||
self.remove_server(channel.connection)
|
self.remove_server(channel.connection)
|
||||||
|
|
||||||
channel.on('open', on_channel_open)
|
channel.on(channel.EVENT_OPEN, on_channel_open)
|
||||||
channel.on('close', on_channel_close)
|
channel.on(channel.EVENT_CLOSE, on_channel_close)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -1789,6 +1813,7 @@ class Stream:
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def start(self) -> None:
|
async def start(self) -> None:
|
||||||
|
"""[Source] Start streaming."""
|
||||||
# Auto-open if needed
|
# Auto-open if needed
|
||||||
if self.state == AVDTP_CONFIGURED_STATE:
|
if self.state == AVDTP_CONFIGURED_STATE:
|
||||||
await self.open()
|
await self.open()
|
||||||
@@ -1805,6 +1830,7 @@ class Stream:
|
|||||||
self.change_state(AVDTP_STREAMING_STATE)
|
self.change_state(AVDTP_STREAMING_STATE)
|
||||||
|
|
||||||
async def stop(self) -> None:
|
async def stop(self) -> None:
|
||||||
|
"""[Source] Stop streaming and transit to OPEN state."""
|
||||||
if self.state != AVDTP_STREAMING_STATE:
|
if self.state != AVDTP_STREAMING_STATE:
|
||||||
raise InvalidStateError('current state is not STREAMING')
|
raise InvalidStateError('current state is not STREAMING')
|
||||||
|
|
||||||
@@ -1817,6 +1843,7 @@ class Stream:
|
|||||||
self.change_state(AVDTP_OPEN_STATE)
|
self.change_state(AVDTP_OPEN_STATE)
|
||||||
|
|
||||||
async def close(self) -> None:
|
async def close(self) -> None:
|
||||||
|
"""[Source] Close channel and transit to IDLE state."""
|
||||||
if self.state not in (AVDTP_OPEN_STATE, AVDTP_STREAMING_STATE):
|
if self.state not in (AVDTP_OPEN_STATE, AVDTP_STREAMING_STATE):
|
||||||
raise InvalidStateError('current state is not OPEN or STREAMING')
|
raise InvalidStateError('current state is not OPEN or STREAMING')
|
||||||
|
|
||||||
@@ -1848,7 +1875,7 @@ class Stream:
|
|||||||
self.change_state(AVDTP_CONFIGURED_STATE)
|
self.change_state(AVDTP_CONFIGURED_STATE)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def on_get_configuration_command(self, configuration):
|
def on_get_configuration_command(self):
|
||||||
if self.state not in (
|
if self.state not in (
|
||||||
AVDTP_CONFIGURED_STATE,
|
AVDTP_CONFIGURED_STATE,
|
||||||
AVDTP_OPEN_STATE,
|
AVDTP_OPEN_STATE,
|
||||||
@@ -1856,7 +1883,7 @@ class Stream:
|
|||||||
):
|
):
|
||||||
return Get_Configuration_Reject(AVDTP_BAD_STATE_ERROR)
|
return Get_Configuration_Reject(AVDTP_BAD_STATE_ERROR)
|
||||||
|
|
||||||
return self.local_endpoint.on_get_configuration_command(configuration)
|
return self.local_endpoint.on_get_configuration_command()
|
||||||
|
|
||||||
def on_reconfigure_command(self, configuration):
|
def on_reconfigure_command(self, configuration):
|
||||||
if self.state != AVDTP_OPEN_STATE:
|
if self.state != AVDTP_OPEN_STATE:
|
||||||
@@ -1936,20 +1963,20 @@ class Stream:
|
|||||||
# Wait for the RTP channel to be closed
|
# Wait for the RTP channel to be closed
|
||||||
self.change_state(AVDTP_ABORTING_STATE)
|
self.change_state(AVDTP_ABORTING_STATE)
|
||||||
|
|
||||||
def on_l2cap_connection(self, channel):
|
def on_l2cap_connection(self, channel: l2cap.ClassicChannel) -> None:
|
||||||
logger.debug(color('<<< stream channel connected', 'magenta'))
|
logger.debug(color('<<< stream channel connected', 'magenta'))
|
||||||
self.rtp_channel = channel
|
self.rtp_channel = channel
|
||||||
channel.on('open', self.on_l2cap_channel_open)
|
channel.on(channel.EVENT_OPEN, self.on_l2cap_channel_open)
|
||||||
channel.on('close', self.on_l2cap_channel_close)
|
channel.on(channel.EVENT_CLOSE, self.on_l2cap_channel_close)
|
||||||
|
|
||||||
# We don't need more channels
|
# We don't need more channels
|
||||||
self.protocol.channel_acceptor = None
|
self.protocol.channel_acceptor = None
|
||||||
|
|
||||||
def on_l2cap_channel_open(self):
|
def on_l2cap_channel_open(self) -> None:
|
||||||
logger.debug(color('<<< stream channel open', 'magenta'))
|
logger.debug(color('<<< stream channel open', 'magenta'))
|
||||||
self.local_endpoint.on_rtp_channel_open()
|
self.local_endpoint.on_rtp_channel_open()
|
||||||
|
|
||||||
def on_l2cap_channel_close(self):
|
def on_l2cap_channel_close(self) -> None:
|
||||||
logger.debug(color('<<< stream channel closed', 'magenta'))
|
logger.debug(color('<<< stream channel closed', 'magenta'))
|
||||||
self.local_endpoint.on_rtp_channel_close()
|
self.local_endpoint.on_rtp_channel_close()
|
||||||
self.local_endpoint.in_use = 0
|
self.local_endpoint.in_use = 0
|
||||||
@@ -2063,9 +2090,22 @@ class DiscoveredStreamEndPoint(StreamEndPoint, StreamEndPointProxy):
|
|||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class LocalStreamEndPoint(StreamEndPoint, EventEmitter):
|
class LocalStreamEndPoint(StreamEndPoint, utils.EventEmitter):
|
||||||
stream: Optional[Stream]
|
stream: Optional[Stream]
|
||||||
|
|
||||||
|
EVENT_CONFIGURATION = "configuration"
|
||||||
|
EVENT_OPEN = "open"
|
||||||
|
EVENT_START = "start"
|
||||||
|
EVENT_STOP = "stop"
|
||||||
|
EVENT_RTP_PACKET = "rtp_packet"
|
||||||
|
EVENT_SUSPEND = "suspend"
|
||||||
|
EVENT_CLOSE = "close"
|
||||||
|
EVENT_ABORT = "abort"
|
||||||
|
EVENT_DELAY_REPORT = "delay_report"
|
||||||
|
EVENT_SECURITY_CONTROL = "security_control"
|
||||||
|
EVENT_RTP_CHANNEL_OPEN = "rtp_channel_open"
|
||||||
|
EVENT_RTP_CHANNEL_CLOSE = "rtp_channel_close"
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
protocol: Protocol,
|
protocol: Protocol,
|
||||||
@@ -2076,57 +2116,70 @@ class LocalStreamEndPoint(StreamEndPoint, EventEmitter):
|
|||||||
configuration: Optional[Iterable[ServiceCapabilities]] = None,
|
configuration: Optional[Iterable[ServiceCapabilities]] = None,
|
||||||
):
|
):
|
||||||
StreamEndPoint.__init__(self, seid, media_type, tsep, 0, capabilities)
|
StreamEndPoint.__init__(self, seid, media_type, tsep, 0, capabilities)
|
||||||
EventEmitter.__init__(self)
|
utils.EventEmitter.__init__(self)
|
||||||
self.protocol = protocol
|
self.protocol = protocol
|
||||||
self.configuration = configuration if configuration is not None else []
|
self.configuration = configuration if configuration is not None else []
|
||||||
self.stream = None
|
self.stream = None
|
||||||
|
|
||||||
async def start(self):
|
async def start(self) -> None:
|
||||||
pass
|
"""[Source Only] Handles when receiving start command."""
|
||||||
|
|
||||||
async def stop(self):
|
async def stop(self) -> None:
|
||||||
pass
|
"""[Source Only] Handles when receiving stop command."""
|
||||||
|
|
||||||
async def close(self):
|
async def close(self) -> None:
|
||||||
pass
|
"""[Source Only] Handles when receiving close command."""
|
||||||
|
|
||||||
def on_reconfigure_command(self, command):
|
def on_reconfigure_command(self, command) -> Optional[Message]:
|
||||||
pass
|
return None
|
||||||
|
|
||||||
def on_set_configuration_command(self, configuration):
|
def on_set_configuration_command(self, configuration) -> Optional[Message]:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
'<<< received configuration: '
|
'<<< received configuration: '
|
||||||
f'{",".join([str(capability) for capability in configuration])}'
|
f'{",".join([str(capability) for capability in configuration])}'
|
||||||
)
|
)
|
||||||
self.configuration = configuration
|
self.configuration = configuration
|
||||||
self.emit('configuration')
|
self.emit(self.EVENT_CONFIGURATION)
|
||||||
|
return None
|
||||||
|
|
||||||
def on_get_configuration_command(self):
|
def on_get_configuration_command(self) -> Optional[Message]:
|
||||||
return Get_Configuration_Response(self.configuration)
|
return Get_Configuration_Response(self.configuration)
|
||||||
|
|
||||||
def on_open_command(self):
|
def on_open_command(self) -> Optional[Message]:
|
||||||
self.emit('open')
|
self.emit(self.EVENT_OPEN)
|
||||||
|
return None
|
||||||
|
|
||||||
def on_start_command(self):
|
def on_start_command(self) -> Optional[Message]:
|
||||||
self.emit('start')
|
self.emit(self.EVENT_START)
|
||||||
|
return None
|
||||||
|
|
||||||
def on_suspend_command(self):
|
def on_suspend_command(self) -> Optional[Message]:
|
||||||
self.emit('suspend')
|
self.emit(self.EVENT_SUSPEND)
|
||||||
|
return None
|
||||||
|
|
||||||
def on_close_command(self):
|
def on_close_command(self) -> Optional[Message]:
|
||||||
self.emit('close')
|
self.emit(self.EVENT_CLOSE)
|
||||||
|
return None
|
||||||
|
|
||||||
def on_abort_command(self):
|
def on_abort_command(self) -> Optional[Message]:
|
||||||
self.emit('abort')
|
self.emit(self.EVENT_ABORT)
|
||||||
|
return None
|
||||||
|
|
||||||
def on_delayreport_command(self, delay: int):
|
def on_delayreport_command(self, delay: int) -> Optional[Message]:
|
||||||
self.emit('delay_report', delay)
|
self.emit(self.EVENT_DELAY_REPORT, delay)
|
||||||
|
return None
|
||||||
|
|
||||||
def on_rtp_channel_open(self):
|
def on_security_control_command(self, data: bytes) -> Optional[Message]:
|
||||||
self.emit('rtp_channel_open')
|
self.emit(self.EVENT_SECURITY_CONTROL, data)
|
||||||
|
return None
|
||||||
|
|
||||||
def on_rtp_channel_close(self):
|
def on_rtp_channel_open(self) -> None:
|
||||||
self.emit('rtp_channel_close')
|
self.emit(self.EVENT_RTP_CHANNEL_OPEN)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def on_rtp_channel_close(self) -> None:
|
||||||
|
self.emit(self.EVENT_RTP_CHANNEL_CLOSE)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -2157,13 +2210,13 @@ class LocalSource(LocalStreamEndPoint):
|
|||||||
if self.packet_pump and self.stream and self.stream.rtp_channel:
|
if self.packet_pump and self.stream and self.stream.rtp_channel:
|
||||||
return await self.packet_pump.start(self.stream.rtp_channel)
|
return await self.packet_pump.start(self.stream.rtp_channel)
|
||||||
|
|
||||||
self.emit('start')
|
self.emit(self.EVENT_START)
|
||||||
|
|
||||||
async def stop(self) -> None:
|
async def stop(self) -> None:
|
||||||
if self.packet_pump:
|
if self.packet_pump:
|
||||||
return await self.packet_pump.stop()
|
return await self.packet_pump.stop()
|
||||||
|
|
||||||
self.emit('stop')
|
self.emit(self.EVENT_STOP)
|
||||||
|
|
||||||
def on_start_command(self):
|
def on_start_command(self):
|
||||||
asyncio.create_task(self.start())
|
asyncio.create_task(self.start())
|
||||||
@@ -2204,4 +2257,4 @@ class LocalSink(LocalStreamEndPoint):
|
|||||||
f'{color("<<< RTP Packet:", "green")} '
|
f'{color("<<< RTP Packet:", "green")} '
|
||||||
f'{rtp_packet} {rtp_packet.payload[:16].hex()}'
|
f'{rtp_packet} {rtp_packet.payload[:16].hex()}'
|
||||||
)
|
)
|
||||||
self.emit('rtp_packet', rtp_packet)
|
self.emit(self.EVENT_RTP_PACKET, rtp_packet)
|
||||||
|
|||||||
+29
-24
@@ -38,7 +38,6 @@ from typing import (
|
|||||||
Union,
|
Union,
|
||||||
)
|
)
|
||||||
|
|
||||||
import pyee
|
|
||||||
|
|
||||||
from bumble.colors import color
|
from bumble.colors import color
|
||||||
from bumble.device import Device, Connection
|
from bumble.device import Device, Connection
|
||||||
@@ -53,7 +52,7 @@ from bumble.sdp import (
|
|||||||
DataElement,
|
DataElement,
|
||||||
ServiceAttribute,
|
ServiceAttribute,
|
||||||
)
|
)
|
||||||
from bumble.utils import AsyncRunner, OpenIntEnum
|
from bumble import utils
|
||||||
from bumble.core import (
|
from bumble.core import (
|
||||||
InvalidArgumentError,
|
InvalidArgumentError,
|
||||||
ProtocolError,
|
ProtocolError,
|
||||||
@@ -307,7 +306,7 @@ class Command:
|
|||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class GetCapabilitiesCommand(Command):
|
class GetCapabilitiesCommand(Command):
|
||||||
class CapabilityId(OpenIntEnum):
|
class CapabilityId(utils.OpenIntEnum):
|
||||||
COMPANY_ID = 0x02
|
COMPANY_ID = 0x02
|
||||||
EVENTS_SUPPORTED = 0x03
|
EVENTS_SUPPORTED = 0x03
|
||||||
|
|
||||||
@@ -637,7 +636,7 @@ class RegisterNotificationResponse(Response):
|
|||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class EventId(OpenIntEnum):
|
class EventId(utils.OpenIntEnum):
|
||||||
PLAYBACK_STATUS_CHANGED = 0x01
|
PLAYBACK_STATUS_CHANGED = 0x01
|
||||||
TRACK_CHANGED = 0x02
|
TRACK_CHANGED = 0x02
|
||||||
TRACK_REACHED_END = 0x03
|
TRACK_REACHED_END = 0x03
|
||||||
@@ -657,12 +656,12 @@ class EventId(OpenIntEnum):
|
|||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class CharacterSetId(OpenIntEnum):
|
class CharacterSetId(utils.OpenIntEnum):
|
||||||
UTF_8 = 0x06
|
UTF_8 = 0x06
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class MediaAttributeId(OpenIntEnum):
|
class MediaAttributeId(utils.OpenIntEnum):
|
||||||
TITLE = 0x01
|
TITLE = 0x01
|
||||||
ARTIST_NAME = 0x02
|
ARTIST_NAME = 0x02
|
||||||
ALBUM_NAME = 0x03
|
ALBUM_NAME = 0x03
|
||||||
@@ -682,7 +681,7 @@ class MediaAttribute:
|
|||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class PlayStatus(OpenIntEnum):
|
class PlayStatus(utils.OpenIntEnum):
|
||||||
STOPPED = 0x00
|
STOPPED = 0x00
|
||||||
PLAYING = 0x01
|
PLAYING = 0x01
|
||||||
PAUSED = 0x02
|
PAUSED = 0x02
|
||||||
@@ -701,33 +700,33 @@ class SongAndPlayStatus:
|
|||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class ApplicationSetting:
|
class ApplicationSetting:
|
||||||
class AttributeId(OpenIntEnum):
|
class AttributeId(utils.OpenIntEnum):
|
||||||
EQUALIZER_ON_OFF = 0x01
|
EQUALIZER_ON_OFF = 0x01
|
||||||
REPEAT_MODE = 0x02
|
REPEAT_MODE = 0x02
|
||||||
SHUFFLE_ON_OFF = 0x03
|
SHUFFLE_ON_OFF = 0x03
|
||||||
SCAN_ON_OFF = 0x04
|
SCAN_ON_OFF = 0x04
|
||||||
|
|
||||||
class EqualizerOnOffStatus(OpenIntEnum):
|
class EqualizerOnOffStatus(utils.OpenIntEnum):
|
||||||
OFF = 0x01
|
OFF = 0x01
|
||||||
ON = 0x02
|
ON = 0x02
|
||||||
|
|
||||||
class RepeatModeStatus(OpenIntEnum):
|
class RepeatModeStatus(utils.OpenIntEnum):
|
||||||
OFF = 0x01
|
OFF = 0x01
|
||||||
SINGLE_TRACK_REPEAT = 0x02
|
SINGLE_TRACK_REPEAT = 0x02
|
||||||
ALL_TRACK_REPEAT = 0x03
|
ALL_TRACK_REPEAT = 0x03
|
||||||
GROUP_REPEAT = 0x04
|
GROUP_REPEAT = 0x04
|
||||||
|
|
||||||
class ShuffleOnOffStatus(OpenIntEnum):
|
class ShuffleOnOffStatus(utils.OpenIntEnum):
|
||||||
OFF = 0x01
|
OFF = 0x01
|
||||||
ALL_TRACKS_SHUFFLE = 0x02
|
ALL_TRACKS_SHUFFLE = 0x02
|
||||||
GROUP_SHUFFLE = 0x03
|
GROUP_SHUFFLE = 0x03
|
||||||
|
|
||||||
class ScanOnOffStatus(OpenIntEnum):
|
class ScanOnOffStatus(utils.OpenIntEnum):
|
||||||
OFF = 0x01
|
OFF = 0x01
|
||||||
ALL_TRACKS_SCAN = 0x02
|
ALL_TRACKS_SCAN = 0x02
|
||||||
GROUP_SCAN = 0x03
|
GROUP_SCAN = 0x03
|
||||||
|
|
||||||
class GenericValue(OpenIntEnum):
|
class GenericValue(utils.OpenIntEnum):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -816,7 +815,7 @@ class PlayerApplicationSettingChangedEvent(Event):
|
|||||||
@dataclass
|
@dataclass
|
||||||
class Setting:
|
class Setting:
|
||||||
attribute_id: ApplicationSetting.AttributeId
|
attribute_id: ApplicationSetting.AttributeId
|
||||||
value_id: OpenIntEnum
|
value_id: utils.OpenIntEnum
|
||||||
|
|
||||||
player_application_settings: List[Setting]
|
player_application_settings: List[Setting]
|
||||||
|
|
||||||
@@ -824,7 +823,7 @@ class PlayerApplicationSettingChangedEvent(Event):
|
|||||||
def from_bytes(cls, pdu: bytes) -> PlayerApplicationSettingChangedEvent:
|
def from_bytes(cls, pdu: bytes) -> PlayerApplicationSettingChangedEvent:
|
||||||
def setting(attribute_id_int: int, value_id_int: int):
|
def setting(attribute_id_int: int, value_id_int: int):
|
||||||
attribute_id = ApplicationSetting.AttributeId(attribute_id_int)
|
attribute_id = ApplicationSetting.AttributeId(attribute_id_int)
|
||||||
value_id: OpenIntEnum
|
value_id: utils.OpenIntEnum
|
||||||
if attribute_id == ApplicationSetting.AttributeId.EQUALIZER_ON_OFF:
|
if attribute_id == ApplicationSetting.AttributeId.EQUALIZER_ON_OFF:
|
||||||
value_id = ApplicationSetting.EqualizerOnOffStatus(value_id_int)
|
value_id = ApplicationSetting.EqualizerOnOffStatus(value_id_int)
|
||||||
elif attribute_id == ApplicationSetting.AttributeId.REPEAT_MODE:
|
elif attribute_id == ApplicationSetting.AttributeId.REPEAT_MODE:
|
||||||
@@ -994,16 +993,20 @@ class Delegate:
|
|||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class Protocol(pyee.EventEmitter):
|
class Protocol(utils.EventEmitter):
|
||||||
"""AVRCP Controller and Target protocol."""
|
"""AVRCP Controller and Target protocol."""
|
||||||
|
|
||||||
|
EVENT_CONNECTION = "connection"
|
||||||
|
EVENT_START = "start"
|
||||||
|
EVENT_STOP = "stop"
|
||||||
|
|
||||||
class PacketType(enum.IntEnum):
|
class PacketType(enum.IntEnum):
|
||||||
SINGLE = 0b00
|
SINGLE = 0b00
|
||||||
START = 0b01
|
START = 0b01
|
||||||
CONTINUE = 0b10
|
CONTINUE = 0b10
|
||||||
END = 0b11
|
END = 0b11
|
||||||
|
|
||||||
class PduId(OpenIntEnum):
|
class PduId(utils.OpenIntEnum):
|
||||||
GET_CAPABILITIES = 0x10
|
GET_CAPABILITIES = 0x10
|
||||||
LIST_PLAYER_APPLICATION_SETTING_ATTRIBUTES = 0x11
|
LIST_PLAYER_APPLICATION_SETTING_ATTRIBUTES = 0x11
|
||||||
LIST_PLAYER_APPLICATION_SETTING_VALUES = 0x12
|
LIST_PLAYER_APPLICATION_SETTING_VALUES = 0x12
|
||||||
@@ -1024,7 +1027,7 @@ class Protocol(pyee.EventEmitter):
|
|||||||
GET_FOLDER_ITEMS = 0x71
|
GET_FOLDER_ITEMS = 0x71
|
||||||
GET_TOTAL_NUMBER_OF_ITEMS = 0x75
|
GET_TOTAL_NUMBER_OF_ITEMS = 0x75
|
||||||
|
|
||||||
class StatusCode(OpenIntEnum):
|
class StatusCode(utils.OpenIntEnum):
|
||||||
INVALID_COMMAND = 0x00
|
INVALID_COMMAND = 0x00
|
||||||
INVALID_PARAMETER = 0x01
|
INVALID_PARAMETER = 0x01
|
||||||
PARAMETER_CONTENT_ERROR = 0x02
|
PARAMETER_CONTENT_ERROR = 0x02
|
||||||
@@ -1457,16 +1460,18 @@ class Protocol(pyee.EventEmitter):
|
|||||||
|
|
||||||
def _on_avctp_connection(self, l2cap_channel: l2cap.ClassicChannel) -> None:
|
def _on_avctp_connection(self, l2cap_channel: l2cap.ClassicChannel) -> None:
|
||||||
logger.debug("AVCTP connection established")
|
logger.debug("AVCTP connection established")
|
||||||
l2cap_channel.on("open", lambda: self._on_avctp_channel_open(l2cap_channel))
|
l2cap_channel.on(
|
||||||
|
l2cap_channel.EVENT_OPEN, lambda: self._on_avctp_channel_open(l2cap_channel)
|
||||||
|
)
|
||||||
|
|
||||||
self.emit("connection")
|
self.emit(self.EVENT_CONNECTION)
|
||||||
|
|
||||||
def _on_avctp_channel_open(self, l2cap_channel: l2cap.ClassicChannel) -> None:
|
def _on_avctp_channel_open(self, l2cap_channel: l2cap.ClassicChannel) -> None:
|
||||||
logger.debug("AVCTP channel open")
|
logger.debug("AVCTP channel open")
|
||||||
if self.avctp_protocol is not None:
|
if self.avctp_protocol is not None:
|
||||||
# TODO: find a better strategy instead of just closing
|
# TODO: find a better strategy instead of just closing
|
||||||
logger.warning("AVCTP protocol already active, closing connection")
|
logger.warning("AVCTP protocol already active, closing connection")
|
||||||
AsyncRunner.spawn(l2cap_channel.disconnect())
|
utils.AsyncRunner.spawn(l2cap_channel.disconnect())
|
||||||
return
|
return
|
||||||
|
|
||||||
self.avctp_protocol = avctp.Protocol(l2cap_channel)
|
self.avctp_protocol = avctp.Protocol(l2cap_channel)
|
||||||
@@ -1474,15 +1479,15 @@ class Protocol(pyee.EventEmitter):
|
|||||||
self.avctp_protocol.register_response_handler(
|
self.avctp_protocol.register_response_handler(
|
||||||
AVRCP_PID, self._on_avctp_response
|
AVRCP_PID, self._on_avctp_response
|
||||||
)
|
)
|
||||||
l2cap_channel.on("close", self._on_avctp_channel_close)
|
l2cap_channel.on(l2cap_channel.EVENT_CLOSE, self._on_avctp_channel_close)
|
||||||
|
|
||||||
self.emit("start")
|
self.emit(self.EVENT_START)
|
||||||
|
|
||||||
def _on_avctp_channel_close(self) -> None:
|
def _on_avctp_channel_close(self) -> None:
|
||||||
logger.debug("AVCTP channel closed")
|
logger.debug("AVCTP channel closed")
|
||||||
self.avctp_protocol = None
|
self.avctp_protocol = None
|
||||||
|
|
||||||
self.emit("stop")
|
self.emit(self.EVENT_STOP)
|
||||||
|
|
||||||
def _on_avctp_command(
|
def _on_avctp_command(
|
||||||
self, transaction_label: int, command: avc.CommandFrame
|
self, transaction_label: int, command: avc.CommandFrame
|
||||||
|
|||||||
+2
-2
@@ -17,8 +17,8 @@
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from .hci import HCI_Packet
|
from bumble.hci import HCI_Packet
|
||||||
from .helpers import PacketTracer
|
from bumble.helpers import PacketTracer
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Logging
|
# Logging
|
||||||
|
|||||||
@@ -25,8 +25,7 @@ import random
|
|||||||
import struct
|
import struct
|
||||||
from bumble.colors import color
|
from bumble.colors import color
|
||||||
from bumble.core import (
|
from bumble.core import (
|
||||||
BT_LE_TRANSPORT,
|
PhysicalTransport,
|
||||||
BT_BR_EDR_TRANSPORT,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from bumble.hci import (
|
from bumble.hci import (
|
||||||
@@ -392,7 +391,7 @@ class Controller:
|
|||||||
role=Role.PERIPHERAL,
|
role=Role.PERIPHERAL,
|
||||||
peer_address=peer_address,
|
peer_address=peer_address,
|
||||||
link=self.link,
|
link=self.link,
|
||||||
transport=BT_LE_TRANSPORT,
|
transport=PhysicalTransport.LE,
|
||||||
link_type=HCI_Connection_Complete_Event.ACL_LINK_TYPE,
|
link_type=HCI_Connection_Complete_Event.ACL_LINK_TYPE,
|
||||||
)
|
)
|
||||||
self.peripheral_connections[peer_address] = connection
|
self.peripheral_connections[peer_address] = connection
|
||||||
@@ -452,7 +451,7 @@ class Controller:
|
|||||||
role=Role.CENTRAL,
|
role=Role.CENTRAL,
|
||||||
peer_address=peer_address,
|
peer_address=peer_address,
|
||||||
link=self.link,
|
link=self.link,
|
||||||
transport=BT_LE_TRANSPORT,
|
transport=PhysicalTransport.LE,
|
||||||
link_type=HCI_Connection_Complete_Event.ACL_LINK_TYPE,
|
link_type=HCI_Connection_Complete_Event.ACL_LINK_TYPE,
|
||||||
)
|
)
|
||||||
self.central_connections[peer_address] = connection
|
self.central_connections[peer_address] = connection
|
||||||
@@ -530,7 +529,7 @@ class Controller:
|
|||||||
|
|
||||||
def on_link_acl_data(self, sender_address, transport, data):
|
def on_link_acl_data(self, sender_address, transport, data):
|
||||||
# Look for the connection to which this data belongs
|
# Look for the connection to which this data belongs
|
||||||
if transport == BT_LE_TRANSPORT:
|
if transport == PhysicalTransport.LE:
|
||||||
connection = self.find_le_connection_by_address(sender_address)
|
connection = self.find_le_connection_by_address(sender_address)
|
||||||
else:
|
else:
|
||||||
connection = self.find_classic_connection_by_address(sender_address)
|
connection = self.find_classic_connection_by_address(sender_address)
|
||||||
@@ -695,7 +694,7 @@ class Controller:
|
|||||||
role=Role.CENTRAL,
|
role=Role.CENTRAL,
|
||||||
peer_address=peer_address,
|
peer_address=peer_address,
|
||||||
link=self.link,
|
link=self.link,
|
||||||
transport=BT_BR_EDR_TRANSPORT,
|
transport=PhysicalTransport.BR_EDR,
|
||||||
link_type=HCI_Connection_Complete_Event.ACL_LINK_TYPE,
|
link_type=HCI_Connection_Complete_Event.ACL_LINK_TYPE,
|
||||||
)
|
)
|
||||||
self.classic_connections[peer_address] = connection
|
self.classic_connections[peer_address] = connection
|
||||||
@@ -763,7 +762,7 @@ class Controller:
|
|||||||
role=Role.CENTRAL,
|
role=Role.CENTRAL,
|
||||||
peer_address=peer_address,
|
peer_address=peer_address,
|
||||||
link=self.link,
|
link=self.link,
|
||||||
transport=BT_BR_EDR_TRANSPORT,
|
transport=PhysicalTransport.BR_EDR,
|
||||||
link_type=link_type,
|
link_type=link_type,
|
||||||
)
|
)
|
||||||
self.classic_connections[peer_address] = connection
|
self.classic_connections[peer_address] = connection
|
||||||
|
|||||||
+69
-63
@@ -23,7 +23,7 @@ from typing import cast, overload, Literal, Union, Optional
|
|||||||
from typing_extensions import Self
|
from typing_extensions import Self
|
||||||
|
|
||||||
from bumble.company_ids import COMPANY_IDENTIFIERS
|
from bumble.company_ids import COMPANY_IDENTIFIERS
|
||||||
from bumble.utils import OpenIntEnum
|
from bumble import utils
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -730,7 +730,7 @@ class DeviceClass:
|
|||||||
# Appearance
|
# Appearance
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class Appearance:
|
class Appearance:
|
||||||
class Category(OpenIntEnum):
|
class Category(utils.OpenIntEnum):
|
||||||
UNKNOWN = 0x0000
|
UNKNOWN = 0x0000
|
||||||
PHONE = 0x0001
|
PHONE = 0x0001
|
||||||
COMPUTER = 0x0002
|
COMPUTER = 0x0002
|
||||||
@@ -784,13 +784,13 @@ class Appearance:
|
|||||||
SPIROMETER = 0x0037
|
SPIROMETER = 0x0037
|
||||||
OUTDOOR_SPORTS_ACTIVITY = 0x0051
|
OUTDOOR_SPORTS_ACTIVITY = 0x0051
|
||||||
|
|
||||||
class UnknownSubcategory(OpenIntEnum):
|
class UnknownSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_UNKNOWN = 0x00
|
GENERIC_UNKNOWN = 0x00
|
||||||
|
|
||||||
class PhoneSubcategory(OpenIntEnum):
|
class PhoneSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_PHONE = 0x00
|
GENERIC_PHONE = 0x00
|
||||||
|
|
||||||
class ComputerSubcategory(OpenIntEnum):
|
class ComputerSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_COMPUTER = 0x00
|
GENERIC_COMPUTER = 0x00
|
||||||
DESKTOP_WORKSTATION = 0x01
|
DESKTOP_WORKSTATION = 0x01
|
||||||
SERVER_CLASS_COMPUTER = 0x02
|
SERVER_CLASS_COMPUTER = 0x02
|
||||||
@@ -808,49 +808,49 @@ class Appearance:
|
|||||||
MINI_PC = 0x0E
|
MINI_PC = 0x0E
|
||||||
STICK_PC = 0x0F
|
STICK_PC = 0x0F
|
||||||
|
|
||||||
class WatchSubcategory(OpenIntEnum):
|
class WatchSubcategory(utils.OpenIntEnum):
|
||||||
GENENERIC_WATCH = 0x00
|
GENERIC_WATCH = 0x00
|
||||||
SPORTS_WATCH = 0x01
|
SPORTS_WATCH = 0x01
|
||||||
SMARTWATCH = 0x02
|
SMARTWATCH = 0x02
|
||||||
|
|
||||||
class ClockSubcategory(OpenIntEnum):
|
class ClockSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_CLOCK = 0x00
|
GENERIC_CLOCK = 0x00
|
||||||
|
|
||||||
class DisplaySubcategory(OpenIntEnum):
|
class DisplaySubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_DISPLAY = 0x00
|
GENERIC_DISPLAY = 0x00
|
||||||
|
|
||||||
class RemoteControlSubcategory(OpenIntEnum):
|
class RemoteControlSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_REMOTE_CONTROL = 0x00
|
GENERIC_REMOTE_CONTROL = 0x00
|
||||||
|
|
||||||
class EyeglassesSubcategory(OpenIntEnum):
|
class EyeglassesSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_EYEGLASSES = 0x00
|
GENERIC_EYEGLASSES = 0x00
|
||||||
|
|
||||||
class TagSubcategory(OpenIntEnum):
|
class TagSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_TAG = 0x00
|
GENERIC_TAG = 0x00
|
||||||
|
|
||||||
class KeyringSubcategory(OpenIntEnum):
|
class KeyringSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_KEYRING = 0x00
|
GENERIC_KEYRING = 0x00
|
||||||
|
|
||||||
class MediaPlayerSubcategory(OpenIntEnum):
|
class MediaPlayerSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_MEDIA_PLAYER = 0x00
|
GENERIC_MEDIA_PLAYER = 0x00
|
||||||
|
|
||||||
class BarcodeScannerSubcategory(OpenIntEnum):
|
class BarcodeScannerSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_BARCODE_SCANNER = 0x00
|
GENERIC_BARCODE_SCANNER = 0x00
|
||||||
|
|
||||||
class ThermometerSubcategory(OpenIntEnum):
|
class ThermometerSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_THERMOMETER = 0x00
|
GENERIC_THERMOMETER = 0x00
|
||||||
EAR_THERMOMETER = 0x01
|
EAR_THERMOMETER = 0x01
|
||||||
|
|
||||||
class HeartRateSensorSubcategory(OpenIntEnum):
|
class HeartRateSensorSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_HEART_RATE_SENSOR = 0x00
|
GENERIC_HEART_RATE_SENSOR = 0x00
|
||||||
HEART_RATE_BELT = 0x01
|
HEART_RATE_BELT = 0x01
|
||||||
|
|
||||||
class BloodPressureSubcategory(OpenIntEnum):
|
class BloodPressureSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_BLOOD_PRESSURE = 0x00
|
GENERIC_BLOOD_PRESSURE = 0x00
|
||||||
ARM_BLOOD_PRESSURE = 0x01
|
ARM_BLOOD_PRESSURE = 0x01
|
||||||
WRIST_BLOOD_PRESSURE = 0x02
|
WRIST_BLOOD_PRESSURE = 0x02
|
||||||
|
|
||||||
class HumanInterfaceDeviceSubcategory(OpenIntEnum):
|
class HumanInterfaceDeviceSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_HUMAN_INTERFACE_DEVICE = 0x00
|
GENERIC_HUMAN_INTERFACE_DEVICE = 0x00
|
||||||
KEYBOARD = 0x01
|
KEYBOARD = 0x01
|
||||||
MOUSE = 0x02
|
MOUSE = 0x02
|
||||||
@@ -863,16 +863,16 @@ class Appearance:
|
|||||||
TOUCHPAD = 0x09
|
TOUCHPAD = 0x09
|
||||||
PRESENTATION_REMOTE = 0x0A
|
PRESENTATION_REMOTE = 0x0A
|
||||||
|
|
||||||
class GlucoseMeterSubcategory(OpenIntEnum):
|
class GlucoseMeterSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_GLUCOSE_METER = 0x00
|
GENERIC_GLUCOSE_METER = 0x00
|
||||||
|
|
||||||
class RunningWalkingSensorSubcategory(OpenIntEnum):
|
class RunningWalkingSensorSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_RUNNING_WALKING_SENSOR = 0x00
|
GENERIC_RUNNING_WALKING_SENSOR = 0x00
|
||||||
IN_SHOE_RUNNING_WALKING_SENSOR = 0x01
|
IN_SHOE_RUNNING_WALKING_SENSOR = 0x01
|
||||||
ON_SHOW_RUNNING_WALKING_SENSOR = 0x02
|
ON_SHOW_RUNNING_WALKING_SENSOR = 0x02
|
||||||
ON_HIP_RUNNING_WALKING_SENSOR = 0x03
|
ON_HIP_RUNNING_WALKING_SENSOR = 0x03
|
||||||
|
|
||||||
class CyclingSubcategory(OpenIntEnum):
|
class CyclingSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_CYCLING = 0x00
|
GENERIC_CYCLING = 0x00
|
||||||
CYCLING_COMPUTER = 0x01
|
CYCLING_COMPUTER = 0x01
|
||||||
SPEED_SENSOR = 0x02
|
SPEED_SENSOR = 0x02
|
||||||
@@ -880,7 +880,7 @@ class Appearance:
|
|||||||
POWER_SENSOR = 0x04
|
POWER_SENSOR = 0x04
|
||||||
SPEED_AND_CADENCE_SENSOR = 0x05
|
SPEED_AND_CADENCE_SENSOR = 0x05
|
||||||
|
|
||||||
class ControlDeviceSubcategory(OpenIntEnum):
|
class ControlDeviceSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_CONTROL_DEVICE = 0x00
|
GENERIC_CONTROL_DEVICE = 0x00
|
||||||
SWITCH = 0x01
|
SWITCH = 0x01
|
||||||
MULTI_SWITCH = 0x02
|
MULTI_SWITCH = 0x02
|
||||||
@@ -895,13 +895,13 @@ class Appearance:
|
|||||||
ENERGY_HARVESTING_SWITCH = 0x0B
|
ENERGY_HARVESTING_SWITCH = 0x0B
|
||||||
PUSH_BUTTON = 0x0C
|
PUSH_BUTTON = 0x0C
|
||||||
|
|
||||||
class NetworkDeviceSubcategory(OpenIntEnum):
|
class NetworkDeviceSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_NETWORK_DEVICE = 0x00
|
GENERIC_NETWORK_DEVICE = 0x00
|
||||||
ACCESS_POINT = 0x01
|
ACCESS_POINT = 0x01
|
||||||
MESH_DEVICE = 0x02
|
MESH_DEVICE = 0x02
|
||||||
MESH_NETWORK_PROXY = 0x03
|
MESH_NETWORK_PROXY = 0x03
|
||||||
|
|
||||||
class SensorSubcategory(OpenIntEnum):
|
class SensorSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_SENSOR = 0x00
|
GENERIC_SENSOR = 0x00
|
||||||
MOTION_SENSOR = 0x01
|
MOTION_SENSOR = 0x01
|
||||||
AIR_QUALITY_SENSOR = 0x02
|
AIR_QUALITY_SENSOR = 0x02
|
||||||
@@ -929,7 +929,7 @@ class Appearance:
|
|||||||
FLAME_DETECTOR = 0x18
|
FLAME_DETECTOR = 0x18
|
||||||
VEHICLE_TIRE_PRESSURE_SENSOR = 0x19
|
VEHICLE_TIRE_PRESSURE_SENSOR = 0x19
|
||||||
|
|
||||||
class LightFixturesSubcategory(OpenIntEnum):
|
class LightFixturesSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_LIGHT_FIXTURES = 0x00
|
GENERIC_LIGHT_FIXTURES = 0x00
|
||||||
WALL_LIGHT = 0x01
|
WALL_LIGHT = 0x01
|
||||||
CEILING_LIGHT = 0x02
|
CEILING_LIGHT = 0x02
|
||||||
@@ -957,7 +957,7 @@ class Appearance:
|
|||||||
LOW_BAY_LIGHT = 0x18
|
LOW_BAY_LIGHT = 0x18
|
||||||
HIGH_BAY_LIGHT = 0x19
|
HIGH_BAY_LIGHT = 0x19
|
||||||
|
|
||||||
class FanSubcategory(OpenIntEnum):
|
class FanSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_FAN = 0x00
|
GENERIC_FAN = 0x00
|
||||||
CEILING_FAN = 0x01
|
CEILING_FAN = 0x01
|
||||||
AXIAL_FAN = 0x02
|
AXIAL_FAN = 0x02
|
||||||
@@ -966,7 +966,7 @@ class Appearance:
|
|||||||
DESK_FAN = 0x05
|
DESK_FAN = 0x05
|
||||||
WALL_FAN = 0x06
|
WALL_FAN = 0x06
|
||||||
|
|
||||||
class HvacSubcategory(OpenIntEnum):
|
class HvacSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_HVAC = 0x00
|
GENERIC_HVAC = 0x00
|
||||||
THERMOSTAT = 0x01
|
THERMOSTAT = 0x01
|
||||||
HUMIDIFIER = 0x02
|
HUMIDIFIER = 0x02
|
||||||
@@ -980,13 +980,13 @@ class Appearance:
|
|||||||
FAN_HEATER = 0x0A
|
FAN_HEATER = 0x0A
|
||||||
AIR_CURTAIN = 0x0B
|
AIR_CURTAIN = 0x0B
|
||||||
|
|
||||||
class AirConditioningSubcategory(OpenIntEnum):
|
class AirConditioningSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_AIR_CONDITIONING = 0x00
|
GENERIC_AIR_CONDITIONING = 0x00
|
||||||
|
|
||||||
class HumidifierSubcategory(OpenIntEnum):
|
class HumidifierSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_HUMIDIFIER = 0x00
|
GENERIC_HUMIDIFIER = 0x00
|
||||||
|
|
||||||
class HeatingSubcategory(OpenIntEnum):
|
class HeatingSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_HEATING = 0x00
|
GENERIC_HEATING = 0x00
|
||||||
RADIATOR = 0x01
|
RADIATOR = 0x01
|
||||||
BOILER = 0x02
|
BOILER = 0x02
|
||||||
@@ -996,7 +996,7 @@ class Appearance:
|
|||||||
FAN_HEATER = 0x06
|
FAN_HEATER = 0x06
|
||||||
AIR_CURTAIN = 0x07
|
AIR_CURTAIN = 0x07
|
||||||
|
|
||||||
class AccessControlSubcategory(OpenIntEnum):
|
class AccessControlSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_ACCESS_CONTROL = 0x00
|
GENERIC_ACCESS_CONTROL = 0x00
|
||||||
ACCESS_DOOR = 0x01
|
ACCESS_DOOR = 0x01
|
||||||
GARAGE_DOOR = 0x02
|
GARAGE_DOOR = 0x02
|
||||||
@@ -1008,7 +1008,7 @@ class Appearance:
|
|||||||
DOOR_LOCK = 0x08
|
DOOR_LOCK = 0x08
|
||||||
LOCKER = 0x09
|
LOCKER = 0x09
|
||||||
|
|
||||||
class MotorizedDeviceSubcategory(OpenIntEnum):
|
class MotorizedDeviceSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_MOTORIZED_DEVICE = 0x00
|
GENERIC_MOTORIZED_DEVICE = 0x00
|
||||||
MOTORIZED_GATE = 0x01
|
MOTORIZED_GATE = 0x01
|
||||||
AWNING = 0x02
|
AWNING = 0x02
|
||||||
@@ -1016,7 +1016,7 @@ class Appearance:
|
|||||||
CURTAINS = 0x04
|
CURTAINS = 0x04
|
||||||
SCREEN = 0x05
|
SCREEN = 0x05
|
||||||
|
|
||||||
class PowerDeviceSubcategory(OpenIntEnum):
|
class PowerDeviceSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_POWER_DEVICE = 0x00
|
GENERIC_POWER_DEVICE = 0x00
|
||||||
POWER_OUTLET = 0x01
|
POWER_OUTLET = 0x01
|
||||||
POWER_STRIP = 0x02
|
POWER_STRIP = 0x02
|
||||||
@@ -1028,7 +1028,7 @@ class Appearance:
|
|||||||
CHARGE_CASE = 0x08
|
CHARGE_CASE = 0x08
|
||||||
POWER_BANK = 0x09
|
POWER_BANK = 0x09
|
||||||
|
|
||||||
class LightSourceSubcategory(OpenIntEnum):
|
class LightSourceSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_LIGHT_SOURCE = 0x00
|
GENERIC_LIGHT_SOURCE = 0x00
|
||||||
INCANDESCENT_LIGHT_BULB = 0x01
|
INCANDESCENT_LIGHT_BULB = 0x01
|
||||||
LED_LAMP = 0x02
|
LED_LAMP = 0x02
|
||||||
@@ -1039,7 +1039,7 @@ class Appearance:
|
|||||||
LOW_VOLTAGE_HALOGEN = 0x07
|
LOW_VOLTAGE_HALOGEN = 0x07
|
||||||
ORGANIC_LIGHT_EMITTING_DIODE = 0x08
|
ORGANIC_LIGHT_EMITTING_DIODE = 0x08
|
||||||
|
|
||||||
class WindowCoveringSubcategory(OpenIntEnum):
|
class WindowCoveringSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_WINDOW_COVERING = 0x00
|
GENERIC_WINDOW_COVERING = 0x00
|
||||||
WINDOW_SHADES = 0x01
|
WINDOW_SHADES = 0x01
|
||||||
WINDOW_BLINDS = 0x02
|
WINDOW_BLINDS = 0x02
|
||||||
@@ -1048,7 +1048,7 @@ class Appearance:
|
|||||||
EXTERIOR_SHUTTER = 0x05
|
EXTERIOR_SHUTTER = 0x05
|
||||||
EXTERIOR_SCREEN = 0x06
|
EXTERIOR_SCREEN = 0x06
|
||||||
|
|
||||||
class AudioSinkSubcategory(OpenIntEnum):
|
class AudioSinkSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_AUDIO_SINK = 0x00
|
GENERIC_AUDIO_SINK = 0x00
|
||||||
STANDALONE_SPEAKER = 0x01
|
STANDALONE_SPEAKER = 0x01
|
||||||
SOUNDBAR = 0x02
|
SOUNDBAR = 0x02
|
||||||
@@ -1056,7 +1056,7 @@ class Appearance:
|
|||||||
STANDMOUNTED_SPEAKER = 0x04
|
STANDMOUNTED_SPEAKER = 0x04
|
||||||
SPEAKERPHONE = 0x05
|
SPEAKERPHONE = 0x05
|
||||||
|
|
||||||
class AudioSourceSubcategory(OpenIntEnum):
|
class AudioSourceSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_AUDIO_SOURCE = 0x00
|
GENERIC_AUDIO_SOURCE = 0x00
|
||||||
MICROPHONE = 0x01
|
MICROPHONE = 0x01
|
||||||
ALARM = 0x02
|
ALARM = 0x02
|
||||||
@@ -1068,7 +1068,7 @@ class Appearance:
|
|||||||
BROADCASTING_ROOM = 0x08
|
BROADCASTING_ROOM = 0x08
|
||||||
AUDITORIUM = 0x09
|
AUDITORIUM = 0x09
|
||||||
|
|
||||||
class MotorizedVehicleSubcategory(OpenIntEnum):
|
class MotorizedVehicleSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_MOTORIZED_VEHICLE = 0x00
|
GENERIC_MOTORIZED_VEHICLE = 0x00
|
||||||
CAR = 0x01
|
CAR = 0x01
|
||||||
LARGE_GOODS_VEHICLE = 0x02
|
LARGE_GOODS_VEHICLE = 0x02
|
||||||
@@ -1086,7 +1086,7 @@ class Appearance:
|
|||||||
CAMPER_CARAVAN = 0x0E
|
CAMPER_CARAVAN = 0x0E
|
||||||
RECREATIONAL_VEHICLE_MOTOR_HOME = 0x0F
|
RECREATIONAL_VEHICLE_MOTOR_HOME = 0x0F
|
||||||
|
|
||||||
class DomesticApplianceSubcategory(OpenIntEnum):
|
class DomesticApplianceSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_DOMESTIC_APPLIANCE = 0x00
|
GENERIC_DOMESTIC_APPLIANCE = 0x00
|
||||||
REFRIGERATOR = 0x01
|
REFRIGERATOR = 0x01
|
||||||
FREEZER = 0x02
|
FREEZER = 0x02
|
||||||
@@ -1104,21 +1104,21 @@ class Appearance:
|
|||||||
RICE_COOKER = 0x0E
|
RICE_COOKER = 0x0E
|
||||||
CLOTHES_STEAMER = 0x0F
|
CLOTHES_STEAMER = 0x0F
|
||||||
|
|
||||||
class WearableAudioDeviceSubcategory(OpenIntEnum):
|
class WearableAudioDeviceSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_WEARABLE_AUDIO_DEVICE = 0x00
|
GENERIC_WEARABLE_AUDIO_DEVICE = 0x00
|
||||||
EARBUD = 0x01
|
EARBUD = 0x01
|
||||||
HEADSET = 0x02
|
HEADSET = 0x02
|
||||||
HEADPHONES = 0x03
|
HEADPHONES = 0x03
|
||||||
NECK_BAND = 0x04
|
NECK_BAND = 0x04
|
||||||
|
|
||||||
class AircraftSubcategory(OpenIntEnum):
|
class AircraftSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_AIRCRAFT = 0x00
|
GENERIC_AIRCRAFT = 0x00
|
||||||
LIGHT_AIRCRAFT = 0x01
|
LIGHT_AIRCRAFT = 0x01
|
||||||
MICROLIGHT = 0x02
|
MICROLIGHT = 0x02
|
||||||
PARAGLIDER = 0x03
|
PARAGLIDER = 0x03
|
||||||
LARGE_PASSENGER_AIRCRAFT = 0x04
|
LARGE_PASSENGER_AIRCRAFT = 0x04
|
||||||
|
|
||||||
class AvEquipmentSubcategory(OpenIntEnum):
|
class AvEquipmentSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_AV_EQUIPMENT = 0x00
|
GENERIC_AV_EQUIPMENT = 0x00
|
||||||
AMPLIFIER = 0x01
|
AMPLIFIER = 0x01
|
||||||
RECEIVER = 0x02
|
RECEIVER = 0x02
|
||||||
@@ -1127,69 +1127,69 @@ class Appearance:
|
|||||||
TURNTABLE = 0x05
|
TURNTABLE = 0x05
|
||||||
CD_PLAYER = 0x06
|
CD_PLAYER = 0x06
|
||||||
DVD_PLAYER = 0x07
|
DVD_PLAYER = 0x07
|
||||||
BLUERAY_PLAYER = 0x08
|
BLURAY_PLAYER = 0x08
|
||||||
OPTICAL_DISC_PLAYER = 0x09
|
OPTICAL_DISC_PLAYER = 0x09
|
||||||
SET_TOP_BOX = 0x0A
|
SET_TOP_BOX = 0x0A
|
||||||
|
|
||||||
class DisplayEquipmentSubcategory(OpenIntEnum):
|
class DisplayEquipmentSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_DISPLAY_EQUIPMENT = 0x00
|
GENERIC_DISPLAY_EQUIPMENT = 0x00
|
||||||
TELEVISION = 0x01
|
TELEVISION = 0x01
|
||||||
MONITOR = 0x02
|
MONITOR = 0x02
|
||||||
PROJECTOR = 0x03
|
PROJECTOR = 0x03
|
||||||
|
|
||||||
class HearingAidSubcategory(OpenIntEnum):
|
class HearingAidSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_HEARING_AID = 0x00
|
GENERIC_HEARING_AID = 0x00
|
||||||
IN_EAR_HEARING_AID = 0x01
|
IN_EAR_HEARING_AID = 0x01
|
||||||
BEHIND_EAR_HEARING_AID = 0x02
|
BEHIND_EAR_HEARING_AID = 0x02
|
||||||
COCHLEAR_IMPLANT = 0x03
|
COCHLEAR_IMPLANT = 0x03
|
||||||
|
|
||||||
class GamingSubcategory(OpenIntEnum):
|
class GamingSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_GAMING = 0x00
|
GENERIC_GAMING = 0x00
|
||||||
HOME_VIDEO_GAME_CONSOLE = 0x01
|
HOME_VIDEO_GAME_CONSOLE = 0x01
|
||||||
PORTABLE_HANDHELD_CONSOLE = 0x02
|
PORTABLE_HANDHELD_CONSOLE = 0x02
|
||||||
|
|
||||||
class SignageSubcategory(OpenIntEnum):
|
class SignageSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_SIGNAGE = 0x00
|
GENERIC_SIGNAGE = 0x00
|
||||||
DIGITAL_SIGNAGE = 0x01
|
DIGITAL_SIGNAGE = 0x01
|
||||||
ELECTRONIC_LABEL = 0x02
|
ELECTRONIC_LABEL = 0x02
|
||||||
|
|
||||||
class PulseOximeterSubcategory(OpenIntEnum):
|
class PulseOximeterSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_PULSE_OXIMETER = 0x00
|
GENERIC_PULSE_OXIMETER = 0x00
|
||||||
FINGERTIP_PULSE_OXIMETER = 0x01
|
FINGERTIP_PULSE_OXIMETER = 0x01
|
||||||
WRIST_WORN_PULSE_OXIMETER = 0x02
|
WRIST_WORN_PULSE_OXIMETER = 0x02
|
||||||
|
|
||||||
class WeightScaleSubcategory(OpenIntEnum):
|
class WeightScaleSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_WEIGHT_SCALE = 0x00
|
GENERIC_WEIGHT_SCALE = 0x00
|
||||||
|
|
||||||
class PersonalMobilityDeviceSubcategory(OpenIntEnum):
|
class PersonalMobilityDeviceSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_PERSONAL_MOBILITY_DEVICE = 0x00
|
GENERIC_PERSONAL_MOBILITY_DEVICE = 0x00
|
||||||
POWERED_WHEELCHAIR = 0x01
|
POWERED_WHEELCHAIR = 0x01
|
||||||
MOBILITY_SCOOTER = 0x02
|
MOBILITY_SCOOTER = 0x02
|
||||||
|
|
||||||
class ContinuousGlucoseMonitorSubcategory(OpenIntEnum):
|
class ContinuousGlucoseMonitorSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_CONTINUOUS_GLUCOSE_MONITOR = 0x00
|
GENERIC_CONTINUOUS_GLUCOSE_MONITOR = 0x00
|
||||||
|
|
||||||
class InsulinPumpSubcategory(OpenIntEnum):
|
class InsulinPumpSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_INSULIN_PUMP = 0x00
|
GENERIC_INSULIN_PUMP = 0x00
|
||||||
INSULIN_PUMP_DURABLE_PUMP = 0x01
|
INSULIN_PUMP_DURABLE_PUMP = 0x01
|
||||||
INSULIN_PUMP_PATCH_PUMP = 0x02
|
INSULIN_PUMP_PATCH_PUMP = 0x02
|
||||||
INSULIN_PEN = 0x03
|
INSULIN_PEN = 0x03
|
||||||
|
|
||||||
class MedicationDeliverySubcategory(OpenIntEnum):
|
class MedicationDeliverySubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_MEDICATION_DELIVERY = 0x00
|
GENERIC_MEDICATION_DELIVERY = 0x00
|
||||||
|
|
||||||
class SpirometerSubcategory(OpenIntEnum):
|
class SpirometerSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_SPIROMETER = 0x00
|
GENERIC_SPIROMETER = 0x00
|
||||||
HANDHELD_SPIROMETER = 0x01
|
HANDHELD_SPIROMETER = 0x01
|
||||||
|
|
||||||
class OutdoorSportsActivitySubcategory(OpenIntEnum):
|
class OutdoorSportsActivitySubcategory(utils.OpenIntEnum):
|
||||||
GENERIC_OUTDOOR_SPORTS_ACTIVITY = 0x00
|
GENERIC_OUTDOOR_SPORTS_ACTIVITY = 0x00
|
||||||
LOCATION_DISPLAY = 0x01
|
LOCATION_DISPLAY = 0x01
|
||||||
LOCATION_AND_NAVIGATION_DISPLAY = 0x02
|
LOCATION_AND_NAVIGATION_DISPLAY = 0x02
|
||||||
LOCATION_POD = 0x03
|
LOCATION_POD = 0x03
|
||||||
LOCATION_AND_NAVIGATION_POD = 0x04
|
LOCATION_AND_NAVIGATION_POD = 0x04
|
||||||
|
|
||||||
class _OpenSubcategory(OpenIntEnum):
|
class _OpenSubcategory(utils.OpenIntEnum):
|
||||||
GENERIC = 0x00
|
GENERIC = 0x00
|
||||||
|
|
||||||
SUBCATEGORY_CLASSES = {
|
SUBCATEGORY_CLASSES = {
|
||||||
@@ -1296,7 +1296,7 @@ class AdvertisingData:
|
|||||||
# fmt: off
|
# fmt: off
|
||||||
# pylint: disable=line-too-long
|
# pylint: disable=line-too-long
|
||||||
|
|
||||||
class Type(OpenIntEnum):
|
class Type(utils.OpenIntEnum):
|
||||||
FLAGS = 0x01
|
FLAGS = 0x01
|
||||||
INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS = 0x02
|
INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS = 0x02
|
||||||
COMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS = 0x03
|
COMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS = 0x03
|
||||||
@@ -1351,6 +1351,12 @@ class AdvertisingData:
|
|||||||
THREE_D_INFORMATION_DATA = 0x3D
|
THREE_D_INFORMATION_DATA = 0x3D
|
||||||
MANUFACTURER_SPECIFIC_DATA = 0xFF
|
MANUFACTURER_SPECIFIC_DATA = 0xFF
|
||||||
|
|
||||||
|
class Flags(enum.IntFlag):
|
||||||
|
LE_LIMITED_DISCOVERABLE_MODE = 1 << 0
|
||||||
|
LE_GENERAL_DISCOVERABLE_MODE = 1 << 1
|
||||||
|
BR_EDR_NOT_SUPPORTED = 1 << 2
|
||||||
|
SIMULTANEOUS_LE_BR_EDR_CAPABLE = 1 << 3
|
||||||
|
|
||||||
# For backward-compatibility
|
# For backward-compatibility
|
||||||
FLAGS = Type.FLAGS
|
FLAGS = Type.FLAGS
|
||||||
INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS = Type.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS
|
INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS = Type.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS
|
||||||
@@ -1407,11 +1413,11 @@ class AdvertisingData:
|
|||||||
THREE_D_INFORMATION_DATA = Type.THREE_D_INFORMATION_DATA
|
THREE_D_INFORMATION_DATA = Type.THREE_D_INFORMATION_DATA
|
||||||
MANUFACTURER_SPECIFIC_DATA = Type.MANUFACTURER_SPECIFIC_DATA
|
MANUFACTURER_SPECIFIC_DATA = Type.MANUFACTURER_SPECIFIC_DATA
|
||||||
|
|
||||||
LE_LIMITED_DISCOVERABLE_MODE_FLAG = 0x01
|
LE_LIMITED_DISCOVERABLE_MODE_FLAG = Flags.LE_LIMITED_DISCOVERABLE_MODE
|
||||||
LE_GENERAL_DISCOVERABLE_MODE_FLAG = 0x02
|
LE_GENERAL_DISCOVERABLE_MODE_FLAG = Flags.LE_GENERAL_DISCOVERABLE_MODE
|
||||||
BR_EDR_NOT_SUPPORTED_FLAG = 0x04
|
BR_EDR_NOT_SUPPORTED_FLAG = Flags.BR_EDR_NOT_SUPPORTED
|
||||||
BR_EDR_CONTROLLER_FLAG = 0x08
|
BR_EDR_CONTROLLER_FLAG = Flags.SIMULTANEOUS_LE_BR_EDR_CAPABLE
|
||||||
BR_EDR_HOST_FLAG = 0x10
|
BR_EDR_HOST_FLAG = 0x10 # Deprecated
|
||||||
|
|
||||||
ad_structures: list[tuple[int, bytes]]
|
ad_structures: list[tuple[int, bytes]]
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Copyright 2021-2022 Google LLC
|
# Copyright 2021-2025 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 not use this file except in compliance with the License.
|
||||||
# You may obtain a copy of the License at
|
# You may obtain a copy of the License at
|
||||||
#
|
#
|
||||||
@@ -12,12 +12,6 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Crypto support
|
|
||||||
#
|
|
||||||
# See Bluetooth spec Vol 3, Part H - 2.2 CRYPTOGRAPHIC TOOLBOX
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Imports
|
# Imports
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -25,19 +19,15 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import operator
|
import operator
|
||||||
|
|
||||||
import secrets
|
import secrets
|
||||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
||||||
from cryptography.hazmat.primitives.asymmetric.ec import (
|
try:
|
||||||
generate_private_key,
|
from bumble.crypto.cryptography import EccKey, e, aes_cmac
|
||||||
ECDH,
|
except ImportError:
|
||||||
EllipticCurvePrivateKey,
|
logging.getLogger(__name__).debug(
|
||||||
EllipticCurvePublicNumbers,
|
"Unable to import cryptography, use built-in primitives."
|
||||||
EllipticCurvePrivateNumbers,
|
)
|
||||||
SECP256R1,
|
from bumble.crypto.builtin import EccKey, e, aes_cmac # type: ignore[assignment]
|
||||||
)
|
|
||||||
from cryptography.hazmat.primitives import cmac
|
|
||||||
from typing import Tuple
|
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -46,55 +36,6 @@ from typing import Tuple
|
|||||||
logger = logging.getLogger(__name__)
|
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
|
# Functions
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -132,19 +73,6 @@ def r() -> bytes:
|
|||||||
return secrets.token_bytes(16)
|
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
|
def ah(k: bytes, r: bytes) -> bytes: # pylint: disable=redefined-outer-name
|
||||||
'''
|
'''
|
||||||
@@ -187,18 +115,6 @@ def s1(k: bytes, r1: bytes, r2: bytes) -> bytes:
|
|||||||
return e(k, r2[0:8] + r1[0:8])
|
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:
|
def f4(u: bytes, v: bytes, x: bytes, z: bytes) -> bytes:
|
||||||
'''
|
'''
|
||||||
@@ -209,7 +125,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
|
See Bluetooth spec, Vol 3, Part H - 2.2.7 LE Secure Connections Key Generation
|
||||||
Function f5
|
Function f5
|
||||||
@@ -0,0 +1,652 @@
|
|||||||
|
# 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 dataclasses
|
||||||
|
import functools
|
||||||
|
import copy
|
||||||
|
import secrets
|
||||||
|
import struct
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
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: Optional[bytes] = 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: Optional[bytes] = 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()
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# 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
|
||||||
|
from cryptography.hazmat.primitives.ciphers import algorithms
|
||||||
|
from cryptography.hazmat.primitives.ciphers import modes
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import ec
|
||||||
|
from cryptography.hazmat.primitives import cmac
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
+514
-302
File diff suppressed because it is too large
Load Diff
@@ -25,8 +25,8 @@ import pathlib
|
|||||||
import platform
|
import platform
|
||||||
from typing import Dict, Iterable, Optional, Type, TYPE_CHECKING
|
from typing import Dict, Iterable, Optional, Type, TYPE_CHECKING
|
||||||
|
|
||||||
from . import rtk, intel
|
from bumble.drivers import rtk, intel
|
||||||
from .common import Driver
|
from bumble.drivers.common import Driver
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from bumble.host import Host
|
from bumble.host import Host
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
INTEL_USB_PRODUCTS = {
|
INTEL_USB_PRODUCTS = {
|
||||||
(0x8087, 0x0032), # AX210
|
(0x8087, 0x0032), # AX210
|
||||||
|
(0x8087, 0x0033), # AX211
|
||||||
(0x8087, 0x0036), # BE200
|
(0x8087, 0x0036), # BE200
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,6 +294,7 @@ class HardwareVariant(utils.OpenIntEnum):
|
|||||||
# This is a just a partial list.
|
# This is a just a partial list.
|
||||||
# Add other constants here as new hardware is encountered and tested.
|
# Add other constants here as new hardware is encountered and tested.
|
||||||
TYPHOON_PEAK = 0x17
|
TYPHOON_PEAK = 0x17
|
||||||
|
GARFIELD_PEAK = 0x19
|
||||||
GALE_PEAK = 0x1C
|
GALE_PEAK = 0x1C
|
||||||
|
|
||||||
|
|
||||||
@@ -471,6 +473,7 @@ class Driver(common.Driver):
|
|||||||
raise DriverError("hardware platform not supported")
|
raise DriverError("hardware platform not supported")
|
||||||
if hardware_info.variant not in (
|
if hardware_info.variant not in (
|
||||||
HardwareVariant.TYPHOON_PEAK,
|
HardwareVariant.TYPHOON_PEAK,
|
||||||
|
HardwareVariant.GARFIELD_PEAK,
|
||||||
HardwareVariant.GALE_PEAK,
|
HardwareVariant.GALE_PEAK,
|
||||||
):
|
):
|
||||||
raise DriverError("hardware variant not supported")
|
raise DriverError("hardware variant not supported")
|
||||||
|
|||||||
+1
-1
@@ -18,7 +18,7 @@
|
|||||||
import logging
|
import logging
|
||||||
import struct
|
import struct
|
||||||
|
|
||||||
from .gatt import (
|
from bumble.gatt import (
|
||||||
Service,
|
Service,
|
||||||
Characteristic,
|
Characteristic,
|
||||||
GATT_GENERIC_ACCESS_SERVICE,
|
GATT_GENERIC_ACCESS_SERVICE,
|
||||||
|
|||||||
+3
-5
@@ -448,6 +448,8 @@ class Characteristic(Attribute[_T]):
|
|||||||
uuid: UUID
|
uuid: UUID
|
||||||
properties: Characteristic.Properties
|
properties: Characteristic.Properties
|
||||||
|
|
||||||
|
EVENT_SUBSCRIPTION = "subscription"
|
||||||
|
|
||||||
class Properties(enum.IntFlag):
|
class Properties(enum.IntFlag):
|
||||||
"""Property flags"""
|
"""Property flags"""
|
||||||
|
|
||||||
@@ -577,11 +579,7 @@ class Descriptor(Attribute):
|
|||||||
if isinstance(self.value, bytes):
|
if isinstance(self.value, bytes):
|
||||||
value_str = self.value.hex()
|
value_str = self.value.hex()
|
||||||
elif isinstance(self.value, CharacteristicValue):
|
elif isinstance(self.value, CharacteristicValue):
|
||||||
value = self.value.read(None)
|
value_str = '<dynamic>'
|
||||||
if isinstance(value, bytes):
|
|
||||||
value_str = value.hex()
|
|
||||||
else:
|
|
||||||
value_str = '<async>'
|
|
||||||
else:
|
else:
|
||||||
value_str = '<...>'
|
value_str = '<...>'
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -35,15 +35,15 @@ from typing import (
|
|||||||
from bumble.core import InvalidOperationError
|
from bumble.core import InvalidOperationError
|
||||||
from bumble.gatt import Characteristic
|
from bumble.gatt import Characteristic
|
||||||
from bumble.gatt_client import CharacteristicProxy
|
from bumble.gatt_client import CharacteristicProxy
|
||||||
from bumble.utils import ByteSerializable, IntConvertible
|
from bumble import utils
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Typing
|
# Typing
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
_T = TypeVar('_T')
|
_T = TypeVar('_T')
|
||||||
_T2 = TypeVar('_T2', bound=ByteSerializable)
|
_T2 = TypeVar('_T2', bound=utils.ByteSerializable)
|
||||||
_T3 = TypeVar('_T3', bound=IntConvertible)
|
_T3 = TypeVar('_T3', bound=utils.IntConvertible)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|||||||
+32
-24
@@ -44,11 +44,10 @@ from typing import (
|
|||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
)
|
)
|
||||||
|
|
||||||
from pyee import EventEmitter
|
|
||||||
|
|
||||||
from .colors import color
|
from bumble.colors import color
|
||||||
from .hci import HCI_Constant
|
from bumble.hci import HCI_Constant
|
||||||
from .att import (
|
from bumble.att import (
|
||||||
ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
||||||
ATT_ATTRIBUTE_NOT_LONG_ERROR,
|
ATT_ATTRIBUTE_NOT_LONG_ERROR,
|
||||||
ATT_CID,
|
ATT_CID,
|
||||||
@@ -69,9 +68,10 @@ from .att import (
|
|||||||
ATT_Write_Request,
|
ATT_Write_Request,
|
||||||
ATT_Error,
|
ATT_Error,
|
||||||
)
|
)
|
||||||
from . import core
|
from bumble import utils
|
||||||
from .core import UUID, InvalidStateError
|
from bumble import core
|
||||||
from .gatt import (
|
from bumble.core import UUID, InvalidStateError
|
||||||
|
from bumble.gatt import (
|
||||||
GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
|
GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
|
||||||
GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
|
GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
|
||||||
GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
|
GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
|
||||||
@@ -117,11 +117,11 @@ def show_services(services: Iterable[ServiceProxy]) -> None:
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Proxies
|
# Proxies
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class AttributeProxy(EventEmitter, Generic[_T]):
|
class AttributeProxy(utils.EventEmitter, Generic[_T]):
|
||||||
def __init__(
|
def __init__(
|
||||||
self, client: Client, handle: int, end_group_handle: int, attribute_type: UUID
|
self, client: Client, handle: int, end_group_handle: int, attribute_type: UUID
|
||||||
) -> None:
|
) -> None:
|
||||||
EventEmitter.__init__(self)
|
utils.EventEmitter.__init__(self)
|
||||||
self.client = client
|
self.client = client
|
||||||
self.handle = handle
|
self.handle = handle
|
||||||
self.end_group_handle = end_group_handle
|
self.end_group_handle = end_group_handle
|
||||||
@@ -149,7 +149,7 @@ class AttributeProxy(EventEmitter, Generic[_T]):
|
|||||||
|
|
||||||
class ServiceProxy(AttributeProxy):
|
class ServiceProxy(AttributeProxy):
|
||||||
uuid: UUID
|
uuid: UUID
|
||||||
characteristics: List[CharacteristicProxy]
|
characteristics: List[CharacteristicProxy[bytes]]
|
||||||
included_services: List[ServiceProxy]
|
included_services: List[ServiceProxy]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -170,14 +170,20 @@ class ServiceProxy(AttributeProxy):
|
|||||||
self.uuid = uuid
|
self.uuid = uuid
|
||||||
self.characteristics = []
|
self.characteristics = []
|
||||||
|
|
||||||
async def discover_characteristics(self, uuids=()) -> list[CharacteristicProxy]:
|
async def discover_characteristics(
|
||||||
|
self, uuids=()
|
||||||
|
) -> list[CharacteristicProxy[bytes]]:
|
||||||
return await self.client.discover_characteristics(uuids, self)
|
return await self.client.discover_characteristics(uuids, self)
|
||||||
|
|
||||||
def get_characteristics_by_uuid(self, uuid: UUID) -> list[CharacteristicProxy]:
|
def get_characteristics_by_uuid(
|
||||||
|
self, uuid: UUID
|
||||||
|
) -> list[CharacteristicProxy[bytes]]:
|
||||||
"""Get all the characteristics with a specified UUID."""
|
"""Get all the characteristics with a specified UUID."""
|
||||||
return self.client.get_characteristics_by_uuid(uuid, self)
|
return self.client.get_characteristics_by_uuid(uuid, self)
|
||||||
|
|
||||||
def get_required_characteristic_by_uuid(self, uuid: UUID) -> CharacteristicProxy:
|
def get_required_characteristic_by_uuid(
|
||||||
|
self, uuid: UUID
|
||||||
|
) -> CharacteristicProxy[bytes]:
|
||||||
"""
|
"""
|
||||||
Get the first characteristic with a specified UUID.
|
Get the first characteristic with a specified UUID.
|
||||||
|
|
||||||
@@ -196,6 +202,8 @@ class CharacteristicProxy(AttributeProxy[_T]):
|
|||||||
descriptors: List[DescriptorProxy]
|
descriptors: List[DescriptorProxy]
|
||||||
subscribers: Dict[Any, Callable[[_T], Any]]
|
subscribers: Dict[Any, Callable[[_T], Any]]
|
||||||
|
|
||||||
|
EVENT_UPDATE = "update"
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
client: Client,
|
client: Client,
|
||||||
@@ -256,7 +264,7 @@ class CharacteristicProxy(AttributeProxy[_T]):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class DescriptorProxy(AttributeProxy):
|
class DescriptorProxy(AttributeProxy[bytes]):
|
||||||
def __init__(self, client: Client, handle: int, descriptor_type: UUID) -> None:
|
def __init__(self, client: Client, handle: int, descriptor_type: UUID) -> None:
|
||||||
super().__init__(client, handle, 0, descriptor_type)
|
super().__init__(client, handle, 0, descriptor_type)
|
||||||
|
|
||||||
@@ -302,7 +310,7 @@ class Client:
|
|||||||
self.services = []
|
self.services = []
|
||||||
self.cached_values = {}
|
self.cached_values = {}
|
||||||
|
|
||||||
connection.on('disconnection', self.on_disconnection)
|
connection.on(connection.EVENT_DISCONNECTION, self.on_disconnection)
|
||||||
|
|
||||||
def send_gatt_pdu(self, pdu: bytes) -> None:
|
def send_gatt_pdu(self, pdu: bytes) -> None:
|
||||||
self.connection.send_l2cap_pdu(ATT_CID, pdu)
|
self.connection.send_l2cap_pdu(ATT_CID, pdu)
|
||||||
@@ -376,7 +384,7 @@ class Client:
|
|||||||
|
|
||||||
def get_characteristics_by_uuid(
|
def get_characteristics_by_uuid(
|
||||||
self, uuid: UUID, service: Optional[ServiceProxy] = None
|
self, uuid: UUID, service: Optional[ServiceProxy] = None
|
||||||
) -> List[CharacteristicProxy]:
|
) -> List[CharacteristicProxy[bytes]]:
|
||||||
services = [service] if service else self.services
|
services = [service] if service else self.services
|
||||||
return [
|
return [
|
||||||
c
|
c
|
||||||
@@ -628,7 +636,7 @@ class Client:
|
|||||||
|
|
||||||
async def discover_characteristics(
|
async def discover_characteristics(
|
||||||
self, uuids, service: Optional[ServiceProxy]
|
self, uuids, service: Optional[ServiceProxy]
|
||||||
) -> List[CharacteristicProxy]:
|
) -> List[CharacteristicProxy[bytes]]:
|
||||||
'''
|
'''
|
||||||
See Vol 3, Part G - 4.6.1 Discover All Characteristics of a Service and 4.6.2
|
See Vol 3, Part G - 4.6.1 Discover All Characteristics of a Service and 4.6.2
|
||||||
Discover Characteristics by UUID
|
Discover Characteristics by UUID
|
||||||
@@ -641,12 +649,12 @@ class Client:
|
|||||||
services = [service] if service else self.services
|
services = [service] if service else self.services
|
||||||
|
|
||||||
# Perform characteristic discovery for each service
|
# Perform characteristic discovery for each service
|
||||||
discovered_characteristics: List[CharacteristicProxy] = []
|
discovered_characteristics: List[CharacteristicProxy[bytes]] = []
|
||||||
for service in services:
|
for service in services:
|
||||||
starting_handle = service.handle
|
starting_handle = service.handle
|
||||||
ending_handle = service.end_group_handle
|
ending_handle = service.end_group_handle
|
||||||
|
|
||||||
characteristics: List[CharacteristicProxy] = []
|
characteristics: List[CharacteristicProxy[bytes]] = []
|
||||||
while starting_handle <= ending_handle:
|
while starting_handle <= ending_handle:
|
||||||
response = await self.send_request(
|
response = await self.send_request(
|
||||||
ATT_Read_By_Type_Request(
|
ATT_Read_By_Type_Request(
|
||||||
@@ -686,7 +694,7 @@ class Client:
|
|||||||
|
|
||||||
properties, handle = struct.unpack_from('<BH', attribute_value)
|
properties, handle = struct.unpack_from('<BH', attribute_value)
|
||||||
characteristic_uuid = UUID.from_bytes(attribute_value[3:])
|
characteristic_uuid = UUID.from_bytes(attribute_value[3:])
|
||||||
characteristic: CharacteristicProxy = CharacteristicProxy(
|
characteristic = CharacteristicProxy[bytes](
|
||||||
self, handle, 0, characteristic_uuid, properties
|
self, handle, 0, characteristic_uuid, properties
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -779,7 +787,7 @@ class Client:
|
|||||||
|
|
||||||
return descriptors
|
return descriptors
|
||||||
|
|
||||||
async def discover_attributes(self) -> List[AttributeProxy]:
|
async def discover_attributes(self) -> List[AttributeProxy[bytes]]:
|
||||||
'''
|
'''
|
||||||
Discover all attributes, regardless of type
|
Discover all attributes, regardless of type
|
||||||
'''
|
'''
|
||||||
@@ -812,7 +820,7 @@ class Client:
|
|||||||
logger.warning(f'bogus handle value: {attribute_handle}')
|
logger.warning(f'bogus handle value: {attribute_handle}')
|
||||||
return []
|
return []
|
||||||
|
|
||||||
attribute: AttributeProxy = AttributeProxy(
|
attribute = AttributeProxy[bytes](
|
||||||
self, attribute_handle, 0, UUID.from_bytes(attribute_uuid)
|
self, attribute_handle, 0, UUID.from_bytes(attribute_uuid)
|
||||||
)
|
)
|
||||||
attributes.append(attribute)
|
attributes.append(attribute)
|
||||||
@@ -1136,7 +1144,7 @@ class Client:
|
|||||||
if callable(subscriber):
|
if callable(subscriber):
|
||||||
subscriber(notification.attribute_value)
|
subscriber(notification.attribute_value)
|
||||||
else:
|
else:
|
||||||
subscriber.emit('update', notification.attribute_value)
|
subscriber.emit(subscriber.EVENT_UPDATE, notification.attribute_value)
|
||||||
|
|
||||||
def on_att_handle_value_indication(self, indication):
|
def on_att_handle_value_indication(self, indication):
|
||||||
# Call all subscribers
|
# Call all subscribers
|
||||||
@@ -1151,7 +1159,7 @@ class Client:
|
|||||||
if callable(subscriber):
|
if callable(subscriber):
|
||||||
subscriber(indication.attribute_value)
|
subscriber(indication.attribute_value)
|
||||||
else:
|
else:
|
||||||
subscriber.emit('update', indication.attribute_value)
|
subscriber.emit(subscriber.EVENT_UPDATE, indication.attribute_value)
|
||||||
|
|
||||||
# Confirm that we received the indication
|
# Confirm that we received the indication
|
||||||
self.send_confirmation(ATT_Handle_Value_Confirmation())
|
self.send_confirmation(ATT_Handle_Value_Confirmation())
|
||||||
|
|||||||
+17
-16
@@ -38,7 +38,6 @@ from typing import (
|
|||||||
Type,
|
Type,
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
)
|
)
|
||||||
from pyee import EventEmitter
|
|
||||||
|
|
||||||
from bumble.colors import color
|
from bumble.colors import color
|
||||||
from bumble.core import UUID
|
from bumble.core import UUID
|
||||||
@@ -83,7 +82,7 @@ from bumble.gatt import (
|
|||||||
Descriptor,
|
Descriptor,
|
||||||
Service,
|
Service,
|
||||||
)
|
)
|
||||||
from bumble.utils import AsyncRunner
|
from bumble import utils
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from bumble.device import Device, Connection
|
from bumble.device import Device, Connection
|
||||||
@@ -103,7 +102,7 @@ GATT_SERVER_DEFAULT_MAX_MTU = 517
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# GATT Server
|
# GATT Server
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class Server(EventEmitter):
|
class Server(utils.EventEmitter):
|
||||||
attributes: List[Attribute]
|
attributes: List[Attribute]
|
||||||
services: List[Service]
|
services: List[Service]
|
||||||
attributes_by_handle: Dict[int, Attribute]
|
attributes_by_handle: Dict[int, Attribute]
|
||||||
@@ -111,6 +110,8 @@ class Server(EventEmitter):
|
|||||||
indication_semaphores: defaultdict[int, asyncio.Semaphore]
|
indication_semaphores: defaultdict[int, asyncio.Semaphore]
|
||||||
pending_confirmations: defaultdict[int, Optional[asyncio.futures.Future]]
|
pending_confirmations: defaultdict[int, Optional[asyncio.futures.Future]]
|
||||||
|
|
||||||
|
EVENT_CHARACTERISTIC_SUBSCRIPTION = "characteristic_subscription"
|
||||||
|
|
||||||
def __init__(self, device: Device) -> None:
|
def __init__(self, device: Device) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.device = device
|
self.device = device
|
||||||
@@ -314,11 +315,8 @@ class Server(EventEmitter):
|
|||||||
self.add_service(service)
|
self.add_service(service)
|
||||||
|
|
||||||
def read_cccd(
|
def read_cccd(
|
||||||
self, connection: Optional[Connection], characteristic: Characteristic
|
self, connection: Connection, characteristic: Characteristic
|
||||||
) -> bytes:
|
) -> bytes:
|
||||||
if connection is None:
|
|
||||||
return bytes([0, 0])
|
|
||||||
|
|
||||||
subscribers = self.subscribers.get(connection.handle)
|
subscribers = self.subscribers.get(connection.handle)
|
||||||
cccd = None
|
cccd = None
|
||||||
if subscribers:
|
if subscribers:
|
||||||
@@ -348,10 +346,13 @@ class Server(EventEmitter):
|
|||||||
notify_enabled = value[0] & 0x01 != 0
|
notify_enabled = value[0] & 0x01 != 0
|
||||||
indicate_enabled = value[0] & 0x02 != 0
|
indicate_enabled = value[0] & 0x02 != 0
|
||||||
characteristic.emit(
|
characteristic.emit(
|
||||||
'subscription', connection, notify_enabled, indicate_enabled
|
characteristic.EVENT_SUBSCRIPTION,
|
||||||
|
connection,
|
||||||
|
notify_enabled,
|
||||||
|
indicate_enabled,
|
||||||
)
|
)
|
||||||
self.emit(
|
self.emit(
|
||||||
'characteristic_subscription',
|
self.EVENT_CHARACTERISTIC_SUBSCRIPTION,
|
||||||
connection,
|
connection,
|
||||||
characteristic,
|
characteristic,
|
||||||
notify_enabled,
|
notify_enabled,
|
||||||
@@ -662,7 +663,7 @@ class Server(EventEmitter):
|
|||||||
|
|
||||||
self.send_response(connection, response)
|
self.send_response(connection, response)
|
||||||
|
|
||||||
@AsyncRunner.run_in_task()
|
@utils.AsyncRunner.run_in_task()
|
||||||
async def on_att_find_by_type_value_request(self, connection, request):
|
async def on_att_find_by_type_value_request(self, connection, request):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 3, Part F - 3.4.3.3 Find By Type Value Request
|
See Bluetooth spec Vol 3, Part F - 3.4.3.3 Find By Type Value Request
|
||||||
@@ -715,7 +716,7 @@ class Server(EventEmitter):
|
|||||||
|
|
||||||
self.send_response(connection, response)
|
self.send_response(connection, response)
|
||||||
|
|
||||||
@AsyncRunner.run_in_task()
|
@utils.AsyncRunner.run_in_task()
|
||||||
async def on_att_read_by_type_request(self, connection, request):
|
async def on_att_read_by_type_request(self, connection, request):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 3, Part F - 3.4.4.1 Read By Type Request
|
See Bluetooth spec Vol 3, Part F - 3.4.4.1 Read By Type Request
|
||||||
@@ -781,7 +782,7 @@ class Server(EventEmitter):
|
|||||||
|
|
||||||
self.send_response(connection, response)
|
self.send_response(connection, response)
|
||||||
|
|
||||||
@AsyncRunner.run_in_task()
|
@utils.AsyncRunner.run_in_task()
|
||||||
async def on_att_read_request(self, connection, request):
|
async def on_att_read_request(self, connection, request):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 3, Part F - 3.4.4.3 Read Request
|
See Bluetooth spec Vol 3, Part F - 3.4.4.3 Read Request
|
||||||
@@ -807,7 +808,7 @@ class Server(EventEmitter):
|
|||||||
)
|
)
|
||||||
self.send_response(connection, response)
|
self.send_response(connection, response)
|
||||||
|
|
||||||
@AsyncRunner.run_in_task()
|
@utils.AsyncRunner.run_in_task()
|
||||||
async def on_att_read_blob_request(self, connection, request):
|
async def on_att_read_blob_request(self, connection, request):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 3, Part F - 3.4.4.5 Read Blob Request
|
See Bluetooth spec Vol 3, Part F - 3.4.4.5 Read Blob Request
|
||||||
@@ -852,7 +853,7 @@ class Server(EventEmitter):
|
|||||||
)
|
)
|
||||||
self.send_response(connection, response)
|
self.send_response(connection, response)
|
||||||
|
|
||||||
@AsyncRunner.run_in_task()
|
@utils.AsyncRunner.run_in_task()
|
||||||
async def on_att_read_by_group_type_request(self, connection, request):
|
async def on_att_read_by_group_type_request(self, connection, request):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 3, Part F - 3.4.4.9 Read by Group Type Request
|
See Bluetooth spec Vol 3, Part F - 3.4.4.9 Read by Group Type Request
|
||||||
@@ -920,7 +921,7 @@ class Server(EventEmitter):
|
|||||||
|
|
||||||
self.send_response(connection, response)
|
self.send_response(connection, response)
|
||||||
|
|
||||||
@AsyncRunner.run_in_task()
|
@utils.AsyncRunner.run_in_task()
|
||||||
async def on_att_write_request(self, connection, request):
|
async def on_att_write_request(self, connection, request):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 3, Part F - 3.4.5.1 Write Request
|
See Bluetooth spec Vol 3, Part F - 3.4.5.1 Write Request
|
||||||
@@ -967,7 +968,7 @@ class Server(EventEmitter):
|
|||||||
response = ATT_Write_Response()
|
response = ATT_Write_Response()
|
||||||
self.send_response(connection, response)
|
self.send_response(connection, response)
|
||||||
|
|
||||||
@AsyncRunner.run_in_task()
|
@utils.AsyncRunner.run_in_task()
|
||||||
async def on_att_write_command(self, connection, request):
|
async def on_att_write_command(self, connection, request):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 3, Part F - 3.4.5.3 Write Command
|
See Bluetooth spec Vol 3, Part F - 3.4.5.3 Write Command
|
||||||
|
|||||||
+114
-23
@@ -29,18 +29,17 @@ from typing_extensions import Self
|
|||||||
from bumble import crypto
|
from bumble import crypto
|
||||||
from bumble.colors import color
|
from bumble.colors import color
|
||||||
from bumble.core import (
|
from bumble.core import (
|
||||||
BT_BR_EDR_TRANSPORT,
|
|
||||||
AdvertisingData,
|
AdvertisingData,
|
||||||
DeviceClass,
|
DeviceClass,
|
||||||
InvalidArgumentError,
|
InvalidArgumentError,
|
||||||
InvalidPacketError,
|
InvalidPacketError,
|
||||||
ProtocolError,
|
|
||||||
PhysicalTransport,
|
PhysicalTransport,
|
||||||
|
ProtocolError,
|
||||||
bit_flags_to_strings,
|
bit_flags_to_strings,
|
||||||
name_or_number,
|
name_or_number,
|
||||||
padded_bytes,
|
padded_bytes,
|
||||||
)
|
)
|
||||||
from bumble.utils import OpenIntEnum
|
from bumble import utils
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -225,6 +224,7 @@ HCI_CONNECTIONLESS_PERIPHERAL_BROADCAST_CHANNEL_MAP_CHANGE_EVENT = 0X55
|
|||||||
HCI_INQUIRY_RESPONSE_NOTIFICATION_EVENT = 0X56
|
HCI_INQUIRY_RESPONSE_NOTIFICATION_EVENT = 0X56
|
||||||
HCI_AUTHENTICATED_PAYLOAD_TIMEOUT_EXPIRED_EVENT = 0X57
|
HCI_AUTHENTICATED_PAYLOAD_TIMEOUT_EXPIRED_EVENT = 0X57
|
||||||
HCI_SAM_STATUS_CHANGE_EVENT = 0X58
|
HCI_SAM_STATUS_CHANGE_EVENT = 0X58
|
||||||
|
HCI_ENCRYPTION_CHANGE_V2_EVENT = 0x59
|
||||||
|
|
||||||
HCI_VENDOR_EVENT = 0xFF
|
HCI_VENDOR_EVENT = 0xFF
|
||||||
|
|
||||||
@@ -739,7 +739,7 @@ class PhyBit(enum.IntFlag):
|
|||||||
LE_CODED = 1 << HCI_LE_CODED_PHY_BIT
|
LE_CODED = 1 << HCI_LE_CODED_PHY_BIT
|
||||||
|
|
||||||
|
|
||||||
class CsRole(OpenIntEnum):
|
class CsRole(utils.OpenIntEnum):
|
||||||
INITIATOR = 0x00
|
INITIATOR = 0x00
|
||||||
REFLECTOR = 0x01
|
REFLECTOR = 0x01
|
||||||
|
|
||||||
@@ -749,7 +749,7 @@ class CsRoleMask(enum.IntFlag):
|
|||||||
REFLECTOR = 0x02
|
REFLECTOR = 0x02
|
||||||
|
|
||||||
|
|
||||||
class CsSyncPhy(OpenIntEnum):
|
class CsSyncPhy(utils.OpenIntEnum):
|
||||||
LE_1M = 1
|
LE_1M = 1
|
||||||
LE_2M = 2
|
LE_2M = 2
|
||||||
LE_2M_2BT = 3
|
LE_2M_2BT = 3
|
||||||
@@ -760,7 +760,7 @@ class CsSyncPhySupported(enum.IntFlag):
|
|||||||
LE_2M_2BT = 0x02
|
LE_2M_2BT = 0x02
|
||||||
|
|
||||||
|
|
||||||
class RttType(OpenIntEnum):
|
class RttType(utils.OpenIntEnum):
|
||||||
AA_ONLY = 0x00
|
AA_ONLY = 0x00
|
||||||
SOUNDING_SEQUENCE_32_BIT = 0x01
|
SOUNDING_SEQUENCE_32_BIT = 0x01
|
||||||
SOUNDING_SEQUENCE_96_BIT = 0x02
|
SOUNDING_SEQUENCE_96_BIT = 0x02
|
||||||
@@ -770,7 +770,7 @@ class RttType(OpenIntEnum):
|
|||||||
RANDOM_SEQUENCE_128_BIT = 0x06
|
RANDOM_SEQUENCE_128_BIT = 0x06
|
||||||
|
|
||||||
|
|
||||||
class CsSnr(OpenIntEnum):
|
class CsSnr(utils.OpenIntEnum):
|
||||||
SNR_18_DB = 0x00
|
SNR_18_DB = 0x00
|
||||||
SNR_21_DB = 0x01
|
SNR_21_DB = 0x01
|
||||||
SNR_24_DB = 0x02
|
SNR_24_DB = 0x02
|
||||||
@@ -779,20 +779,20 @@ class CsSnr(OpenIntEnum):
|
|||||||
NOT_APPLIED = 0xFF
|
NOT_APPLIED = 0xFF
|
||||||
|
|
||||||
|
|
||||||
class CsDoneStatus(OpenIntEnum):
|
class CsDoneStatus(utils.OpenIntEnum):
|
||||||
ALL_RESULTS_COMPLETED = 0x00
|
ALL_RESULTS_COMPLETED = 0x00
|
||||||
PARTIAL = 0x01
|
PARTIAL = 0x01
|
||||||
ABORTED = 0x0F
|
ABORTED = 0x0F
|
||||||
|
|
||||||
|
|
||||||
class CsProcedureAbortReason(OpenIntEnum):
|
class CsProcedureAbortReason(utils.OpenIntEnum):
|
||||||
NO_ABORT = 0x00
|
NO_ABORT = 0x00
|
||||||
LOCAL_HOST_OR_REMOTE_REQUEST = 0x01
|
LOCAL_HOST_OR_REMOTE_REQUEST = 0x01
|
||||||
CHANNEL_MAP_UPDATE_INSTANT_PASSED = 0x02
|
CHANNEL_MAP_UPDATE_INSTANT_PASSED = 0x02
|
||||||
UNSPECIFIED = 0x0F
|
UNSPECIFIED = 0x0F
|
||||||
|
|
||||||
|
|
||||||
class CsSubeventAbortReason(OpenIntEnum):
|
class CsSubeventAbortReason(utils.OpenIntEnum):
|
||||||
NO_ABORT = 0x00
|
NO_ABORT = 0x00
|
||||||
LOCAL_HOST_OR_REMOTE_REQUEST = 0x01
|
LOCAL_HOST_OR_REMOTE_REQUEST = 0x01
|
||||||
NO_CS_SYNC_RECEIVED = 0x02
|
NO_CS_SYNC_RECEIVED = 0x02
|
||||||
@@ -890,7 +890,7 @@ HCI_LINK_TYPE_NAMES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Address types
|
# Address types
|
||||||
class AddressType(OpenIntEnum):
|
class AddressType(utils.OpenIntEnum):
|
||||||
PUBLIC_DEVICE = 0x00
|
PUBLIC_DEVICE = 0x00
|
||||||
RANDOM_DEVICE = 0x01
|
RANDOM_DEVICE = 0x01
|
||||||
PUBLIC_IDENTITY = 0x02
|
PUBLIC_IDENTITY = 0x02
|
||||||
@@ -1239,7 +1239,7 @@ HCI_SUPPORTED_COMMANDS_MASKS = {
|
|||||||
|
|
||||||
# LE Supported Features
|
# LE Supported Features
|
||||||
# See Bluetooth spec @ Vol 6, Part B, 4.6 FEATURE SUPPORT
|
# See Bluetooth spec @ Vol 6, Part B, 4.6 FEATURE SUPPORT
|
||||||
class LeFeature(OpenIntEnum):
|
class LeFeature(utils.OpenIntEnum):
|
||||||
LE_ENCRYPTION = 0
|
LE_ENCRYPTION = 0
|
||||||
CONNECTION_PARAMETERS_REQUEST_PROCEDURE = 1
|
CONNECTION_PARAMETERS_REQUEST_PROCEDURE = 1
|
||||||
EXTENDED_REJECT_INDICATION = 2
|
EXTENDED_REJECT_INDICATION = 2
|
||||||
@@ -1537,7 +1537,7 @@ RTT_TYPE_SPEC = {'size': 1, 'mapper': lambda x: RttType(x).name}
|
|||||||
CS_SNR_SPEC = {'size': 1, 'mapper': lambda x: CsSnr(x).name}
|
CS_SNR_SPEC = {'size': 1, 'mapper': lambda x: CsSnr(x).name}
|
||||||
|
|
||||||
|
|
||||||
class CodecID(OpenIntEnum):
|
class CodecID(utils.OpenIntEnum):
|
||||||
# fmt: off
|
# fmt: off
|
||||||
U_LOG = 0x00
|
U_LOG = 0x00
|
||||||
A_LOG = 0x01
|
A_LOG = 0x01
|
||||||
@@ -1976,7 +1976,7 @@ class Address:
|
|||||||
def from_string_for_transport(
|
def from_string_for_transport(
|
||||||
cls: type[Self], string: str, transport: PhysicalTransport
|
cls: type[Self], string: str, transport: PhysicalTransport
|
||||||
) -> Self:
|
) -> Self:
|
||||||
if transport == BT_BR_EDR_TRANSPORT:
|
if transport == PhysicalTransport.BR_EDR:
|
||||||
address_type = Address.PUBLIC_DEVICE_ADDRESS
|
address_type = Address.PUBLIC_DEVICE_ADDRESS
|
||||||
else:
|
else:
|
||||||
address_type = Address.RANDOM_DEVICE_ADDRESS
|
address_type = Address.RANDOM_DEVICE_ADDRESS
|
||||||
@@ -3364,6 +3364,20 @@ class HCI_Set_Event_Mask_Page_2_Command(HCI_Command):
|
|||||||
See Bluetooth spec @ 7.3.69 Set Event Mask Page 2 Command
|
See Bluetooth spec @ 7.3.69 Set Event Mask Page 2 Command
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def mask(event_codes: Iterable[int]) -> bytes:
|
||||||
|
'''
|
||||||
|
Compute the event mask value for a list of events.
|
||||||
|
'''
|
||||||
|
# NOTE: this implementation takes advantage of the fact that as of version 6.0
|
||||||
|
# of the core specification, the bit number for each event code is equal to 64
|
||||||
|
# less than the event code.
|
||||||
|
# If future versions of the specification deviate from that, a different
|
||||||
|
# implementation would be needed.
|
||||||
|
return sum((1 << event_code - 64) for event_code in event_codes).to_bytes(
|
||||||
|
8, 'little'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@HCI_Command.command(
|
@HCI_Command.command(
|
||||||
@@ -4883,6 +4897,20 @@ class HCI_LE_Periodic_Advertising_Sync_Transfer_Command(HCI_Command):
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
@HCI_Command.command(
|
||||||
|
fields=[('connection_handle', 2), ('service_data', 2), ('advertising_handle', 1)],
|
||||||
|
return_parameters_fields=[
|
||||||
|
('status', STATUS_SPEC),
|
||||||
|
('connection_handle', 2),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
class HCI_LE_Periodic_Advertising_Set_Info_Transfer_Command(HCI_Command):
|
||||||
|
'''
|
||||||
|
See Bluetooth spec @ 7.8.90 LE Periodic Advertising Set Info Transfer Command
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@HCI_Command.command(
|
@HCI_Command.command(
|
||||||
fields=[
|
fields=[
|
||||||
@@ -5356,11 +5384,11 @@ class HCI_LE_CS_Create_Config_Command(HCI_Command):
|
|||||||
See Bluetooth spec @ 7.8.137 LE CS Create Config command
|
See Bluetooth spec @ 7.8.137 LE CS Create Config command
|
||||||
'''
|
'''
|
||||||
|
|
||||||
class ChannelSelectionType(OpenIntEnum):
|
class ChannelSelectionType(utils.OpenIntEnum):
|
||||||
ALGO_3B = 0
|
ALGO_3B = 0
|
||||||
ALGO_3C = 1
|
ALGO_3C = 1
|
||||||
|
|
||||||
class Ch3cShape(OpenIntEnum):
|
class Ch3cShape(utils.OpenIntEnum):
|
||||||
HAT = 0x00
|
HAT = 0x00
|
||||||
X = 0x01
|
X = 0x01
|
||||||
|
|
||||||
@@ -5811,12 +5839,18 @@ class HCI_LE_Advertising_Report_Event(HCI_LE_Meta_Event):
|
|||||||
return HCI_LE_Advertising_Report_Event.event_type_name(self.event_type)
|
return HCI_LE_Advertising_Report_Event.event_type_name(self.event_type)
|
||||||
|
|
||||||
def to_string(self, indentation='', _=None):
|
def to_string(self, indentation='', _=None):
|
||||||
|
def data_to_str(data):
|
||||||
|
try:
|
||||||
|
return data.hex() + ': ' + str(AdvertisingData.from_bytes(data))
|
||||||
|
except Exception:
|
||||||
|
return data.hex()
|
||||||
|
|
||||||
return super().to_string(
|
return super().to_string(
|
||||||
indentation,
|
indentation,
|
||||||
{
|
{
|
||||||
'event_type': HCI_LE_Advertising_Report_Event.event_type_name,
|
'event_type': HCI_LE_Advertising_Report_Event.event_type_name,
|
||||||
'address_type': Address.address_type_name,
|
'address_type': Address.address_type_name,
|
||||||
'data': lambda x: str(AdvertisingData.from_bytes(x)),
|
'data': data_to_str,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -5951,6 +5985,33 @@ class HCI_LE_Enhanced_Connection_Complete_Event(HCI_LE_Meta_Event):
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
@HCI_LE_Meta_Event.event(
|
||||||
|
[
|
||||||
|
('status', STATUS_SPEC),
|
||||||
|
('connection_handle', 2),
|
||||||
|
(
|
||||||
|
'role',
|
||||||
|
{'size': 1, 'mapper': lambda x: 'CENTRAL' if x == 0 else 'PERIPHERAL'},
|
||||||
|
),
|
||||||
|
('peer_address_type', Address.ADDRESS_TYPE_SPEC),
|
||||||
|
('peer_address', Address.parse_address_preceded_by_type),
|
||||||
|
('local_resolvable_private_address', Address.parse_random_address),
|
||||||
|
('peer_resolvable_private_address', Address.parse_random_address),
|
||||||
|
('connection_interval', 2),
|
||||||
|
('peripheral_latency', 2),
|
||||||
|
('supervision_timeout', 2),
|
||||||
|
('central_clock_accuracy', 1),
|
||||||
|
('advertising_handle', 1),
|
||||||
|
('sync_handle', 2),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
class HCI_LE_Enhanced_Connection_Complete_V2_Event(HCI_LE_Meta_Event):
|
||||||
|
'''
|
||||||
|
See Bluetooth spec @ 7.7.65.10 LE Enhanced Connection Complete Event
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@HCI_LE_Meta_Event.event(
|
@HCI_LE_Meta_Event.event(
|
||||||
[
|
[
|
||||||
@@ -6041,12 +6102,18 @@ class HCI_LE_Extended_Advertising_Report_Event(HCI_LE_Meta_Event):
|
|||||||
|
|
||||||
def to_string(self, indentation='', _=None):
|
def to_string(self, indentation='', _=None):
|
||||||
# pylint: disable=line-too-long
|
# pylint: disable=line-too-long
|
||||||
|
def data_to_str(data):
|
||||||
|
try:
|
||||||
|
return data.hex() + ': ' + str(AdvertisingData.from_bytes(data))
|
||||||
|
except Exception:
|
||||||
|
return data.hex()
|
||||||
|
|
||||||
return super().to_string(
|
return super().to_string(
|
||||||
indentation,
|
indentation,
|
||||||
{
|
{
|
||||||
'event_type': HCI_LE_Extended_Advertising_Report_Event.event_type_string,
|
'event_type': HCI_LE_Extended_Advertising_Report_Event.event_type_string,
|
||||||
'address_type': Address.address_type_name,
|
'address_type': Address.address_type_name,
|
||||||
'data': lambda x: str(AdvertisingData.from_bytes(x)),
|
'data': data_to_str,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -6189,13 +6256,13 @@ class HCI_LE_Periodic_Advertising_Report_Event(HCI_LE_Meta_Event):
|
|||||||
TX_POWER_INFORMATION_NOT_AVAILABLE = 0x7F
|
TX_POWER_INFORMATION_NOT_AVAILABLE = 0x7F
|
||||||
RSSI_NOT_AVAILABLE = 0x7F
|
RSSI_NOT_AVAILABLE = 0x7F
|
||||||
|
|
||||||
class CteType(OpenIntEnum):
|
class CteType(utils.OpenIntEnum):
|
||||||
AOA_CONSTANT_TONE_EXTENSION = 0x00
|
AOA_CONSTANT_TONE_EXTENSION = 0x00
|
||||||
AOD_CONSTANT_TONE_EXTENSION_1US = 0x01
|
AOD_CONSTANT_TONE_EXTENSION_1US = 0x01
|
||||||
AOD_CONSTANT_TONE_EXTENSION_2US = 0x02
|
AOD_CONSTANT_TONE_EXTENSION_2US = 0x02
|
||||||
NO_CONSTANT_TONE_EXTENSION = 0xFF
|
NO_CONSTANT_TONE_EXTENSION = 0xFF
|
||||||
|
|
||||||
class DataStatus(OpenIntEnum):
|
class DataStatus(utils.OpenIntEnum):
|
||||||
DATA_COMPLETE = 0x00
|
DATA_COMPLETE = 0x00
|
||||||
DATA_INCOMPLETE_MORE_TO_COME = 0x01
|
DATA_INCOMPLETE_MORE_TO_COME = 0x01
|
||||||
DATA_INCOMPLETE_TRUNCATED_NO_MORE_TO_COME = 0x02
|
DATA_INCOMPLETE_TRUNCATED_NO_MORE_TO_COME = 0x02
|
||||||
@@ -6576,7 +6643,7 @@ class HCI_LE_CS_Config_Complete_Event(HCI_LE_Meta_Event):
|
|||||||
See Bluetooth spec @ 7.7.65.42 LE CS Config Complete event
|
See Bluetooth spec @ 7.7.65.42 LE CS Config Complete event
|
||||||
'''
|
'''
|
||||||
|
|
||||||
class Action(OpenIntEnum):
|
class Action(utils.OpenIntEnum):
|
||||||
REMOVED = 0
|
REMOVED = 0
|
||||||
CREATED = 1
|
CREATED = 1
|
||||||
|
|
||||||
@@ -6628,7 +6695,7 @@ class HCI_LE_CS_Procedure_Enable_Complete_Event(HCI_LE_Meta_Event):
|
|||||||
See Bluetooth spec @ 7.7.65.43 LE CS Procedure Enable Complete event
|
See Bluetooth spec @ 7.7.65.43 LE CS Procedure Enable Complete event
|
||||||
'''
|
'''
|
||||||
|
|
||||||
class State(OpenIntEnum):
|
class State(utils.OpenIntEnum):
|
||||||
DISABLED = 0
|
DISABLED = 0
|
||||||
ENABLED = 1
|
ENABLED = 1
|
||||||
|
|
||||||
@@ -6924,6 +6991,30 @@ class HCI_Encryption_Change_Event(HCI_Event):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
@HCI_Event.event(
|
||||||
|
[
|
||||||
|
('status', STATUS_SPEC),
|
||||||
|
('connection_handle', 2),
|
||||||
|
(
|
||||||
|
'encryption_enabled',
|
||||||
|
{
|
||||||
|
'size': 1,
|
||||||
|
# pylint: disable-next=unnecessary-lambda
|
||||||
|
'mapper': lambda x: HCI_Encryption_Change_Event.encryption_enabled_name(
|
||||||
|
x
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
('encryption_key_size', 1),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
class HCI_Encryption_Change_V2_Event(HCI_Event):
|
||||||
|
'''
|
||||||
|
See Bluetooth spec @ 7.7.8 Encryption Change Event
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@HCI_Event.event(
|
@HCI_Event.event(
|
||||||
[('status', STATUS_SPEC), ('connection_handle', 2), ('lmp_features', 8)]
|
[('status', STATUS_SPEC), ('connection_handle', 2), ('lmp_features', 8)]
|
||||||
@@ -6970,7 +7061,7 @@ class HCI_QOS_Setup_Complete_Event(HCI_Event):
|
|||||||
See Bluetooth spec @ 7.7.13 QoS Setup Complete Event
|
See Bluetooth spec @ 7.7.13 QoS Setup Complete Event
|
||||||
'''
|
'''
|
||||||
|
|
||||||
class ServiceType(OpenIntEnum):
|
class ServiceType(utils.OpenIntEnum):
|
||||||
NO_TRAFFIC_AVAILABLE = 0x00
|
NO_TRAFFIC_AVAILABLE = 0x00
|
||||||
BEST_EFFORT_AVAILABLE = 0x01
|
BEST_EFFORT_AVAILABLE = 0x01
|
||||||
GUARANTEED_AVAILABLE = 0x02
|
GUARANTEED_AVAILABLE = 0x02
|
||||||
|
|||||||
+47
-23
@@ -24,7 +24,6 @@ import asyncio
|
|||||||
import dataclasses
|
import dataclasses
|
||||||
import enum
|
import enum
|
||||||
import traceback
|
import traceback
|
||||||
import pyee
|
|
||||||
import re
|
import re
|
||||||
from typing import (
|
from typing import (
|
||||||
Dict,
|
Dict,
|
||||||
@@ -45,6 +44,7 @@ from bumble import at
|
|||||||
from bumble import device
|
from bumble import device
|
||||||
from bumble import rfcomm
|
from bumble import rfcomm
|
||||||
from bumble import sdp
|
from bumble import sdp
|
||||||
|
from bumble import utils
|
||||||
from bumble.colors import color
|
from bumble.colors import color
|
||||||
from bumble.core import (
|
from bumble.core import (
|
||||||
ProtocolError,
|
ProtocolError,
|
||||||
@@ -690,7 +690,7 @@ class HfIndicatorState:
|
|||||||
current_status: int = 0
|
current_status: int = 0
|
||||||
|
|
||||||
|
|
||||||
class HfProtocol(pyee.EventEmitter):
|
class HfProtocol(utils.EventEmitter):
|
||||||
"""
|
"""
|
||||||
Implementation for the Hands-Free side of the Hands-Free profile.
|
Implementation for the Hands-Free side of the Hands-Free profile.
|
||||||
|
|
||||||
@@ -720,6 +720,14 @@ class HfProtocol(pyee.EventEmitter):
|
|||||||
vrec: VoiceRecognitionState
|
vrec: VoiceRecognitionState
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
EVENT_CODEC_NEGOTIATION = "codec_negotiation"
|
||||||
|
EVENT_AG_INDICATOR = "ag_indicator"
|
||||||
|
EVENT_SPEAKER_VOLUME = "speaker_volume"
|
||||||
|
EVENT_MICROPHONE_VOLUME = "microphone_volume"
|
||||||
|
EVENT_RING = "ring"
|
||||||
|
EVENT_CLI_NOTIFICATION = "cli_notification"
|
||||||
|
EVENT_VOICE_RECOGNITION = "voice_recognition"
|
||||||
|
|
||||||
class HfLoopTermination(HfpProtocolError):
|
class HfLoopTermination(HfpProtocolError):
|
||||||
"""Termination signal for run() loop."""
|
"""Termination signal for run() loop."""
|
||||||
|
|
||||||
@@ -777,7 +785,8 @@ class HfProtocol(pyee.EventEmitter):
|
|||||||
self.dlc.sink = self._read_at
|
self.dlc.sink = self._read_at
|
||||||
# Stop the run() loop when L2CAP is closed.
|
# Stop the run() loop when L2CAP is closed.
|
||||||
self.dlc.multiplexer.l2cap_channel.on(
|
self.dlc.multiplexer.l2cap_channel.on(
|
||||||
'close', lambda: self.unsolicited_queue.put_nowait(None)
|
self.dlc.multiplexer.l2cap_channel.EVENT_CLOSE,
|
||||||
|
lambda: self.unsolicited_queue.put_nowait(None),
|
||||||
)
|
)
|
||||||
|
|
||||||
def supports_hf_feature(self, feature: HfFeature) -> bool:
|
def supports_hf_feature(self, feature: HfFeature) -> bool:
|
||||||
@@ -1034,7 +1043,7 @@ class HfProtocol(pyee.EventEmitter):
|
|||||||
# ID. The HF shall be ready to accept the synchronous connection
|
# ID. The HF shall be ready to accept the synchronous connection
|
||||||
# establishment as soon as it has sent the AT commands AT+BCS=<Codec ID>.
|
# establishment as soon as it has sent the AT commands AT+BCS=<Codec ID>.
|
||||||
self.active_codec = AudioCodec(codec_id)
|
self.active_codec = AudioCodec(codec_id)
|
||||||
self.emit('codec_negotiation', self.active_codec)
|
self.emit(self.EVENT_CODEC_NEGOTIATION, self.active_codec)
|
||||||
|
|
||||||
logger.info("codec connection setup completed")
|
logger.info("codec connection setup completed")
|
||||||
|
|
||||||
@@ -1095,7 +1104,7 @@ class HfProtocol(pyee.EventEmitter):
|
|||||||
# CIEV is in 1-index, while ag_indicators is in 0-index.
|
# CIEV is in 1-index, while ag_indicators is in 0-index.
|
||||||
ag_indicator = self.ag_indicators[index - 1]
|
ag_indicator = self.ag_indicators[index - 1]
|
||||||
ag_indicator.current_status = value
|
ag_indicator.current_status = value
|
||||||
self.emit('ag_indicator', ag_indicator)
|
self.emit(self.EVENT_AG_INDICATOR, ag_indicator)
|
||||||
logger.info(f"AG indicator updated: {ag_indicator.indicator}, {value}")
|
logger.info(f"AG indicator updated: {ag_indicator.indicator}, {value}")
|
||||||
|
|
||||||
async def handle_unsolicited(self):
|
async def handle_unsolicited(self):
|
||||||
@@ -1110,19 +1119,21 @@ class HfProtocol(pyee.EventEmitter):
|
|||||||
int(result.parameters[0]), int(result.parameters[1])
|
int(result.parameters[0]), int(result.parameters[1])
|
||||||
)
|
)
|
||||||
elif result.code == "+VGS":
|
elif result.code == "+VGS":
|
||||||
self.emit('speaker_volume', int(result.parameters[0]))
|
self.emit(self.EVENT_SPEAKER_VOLUME, int(result.parameters[0]))
|
||||||
elif result.code == "+VGM":
|
elif result.code == "+VGM":
|
||||||
self.emit('microphone_volume', int(result.parameters[0]))
|
self.emit(self.EVENT_MICROPHONE_VOLUME, int(result.parameters[0]))
|
||||||
elif result.code == "RING":
|
elif result.code == "RING":
|
||||||
self.emit('ring')
|
self.emit(self.EVENT_RING)
|
||||||
elif result.code == "+CLIP":
|
elif result.code == "+CLIP":
|
||||||
self.emit(
|
self.emit(
|
||||||
'cli_notification', CallLineIdentification.parse_from(result.parameters)
|
self.EVENT_CLI_NOTIFICATION,
|
||||||
|
CallLineIdentification.parse_from(result.parameters),
|
||||||
)
|
)
|
||||||
elif result.code == "+BVRA":
|
elif result.code == "+BVRA":
|
||||||
# TODO: Support Enhanced Voice Recognition.
|
# TODO: Support Enhanced Voice Recognition.
|
||||||
self.emit(
|
self.emit(
|
||||||
'voice_recognition', VoiceRecognitionState(int(result.parameters[0]))
|
self.EVENT_VOICE_RECOGNITION,
|
||||||
|
VoiceRecognitionState(int(result.parameters[0])),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
logging.info(f"unhandled unsolicited response {result.code}")
|
logging.info(f"unhandled unsolicited response {result.code}")
|
||||||
@@ -1146,7 +1157,7 @@ class HfProtocol(pyee.EventEmitter):
|
|||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
class AgProtocol(pyee.EventEmitter):
|
class AgProtocol(utils.EventEmitter):
|
||||||
"""
|
"""
|
||||||
Implementation for the Audio-Gateway side of the Hands-Free profile.
|
Implementation for the Audio-Gateway side of the Hands-Free profile.
|
||||||
|
|
||||||
@@ -1179,6 +1190,19 @@ class AgProtocol(pyee.EventEmitter):
|
|||||||
volume: Int
|
volume: Int
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
EVENT_SLC_COMPLETE = "slc_complete"
|
||||||
|
EVENT_SUPPORTED_AUDIO_CODECS = "supported_audio_codecs"
|
||||||
|
EVENT_CODEC_NEGOTIATION = "codec_negotiation"
|
||||||
|
EVENT_VOICE_RECOGNITION = "voice_recognition"
|
||||||
|
EVENT_CALL_HOLD = "call_hold"
|
||||||
|
EVENT_HF_INDICATOR = "hf_indicator"
|
||||||
|
EVENT_CODEC_CONNECTION_REQUEST = "codec_connection_request"
|
||||||
|
EVENT_ANSWER = "answer"
|
||||||
|
EVENT_DIAL = "dial"
|
||||||
|
EVENT_HANG_UP = "hang_up"
|
||||||
|
EVENT_SPEAKER_VOLUME = "speaker_volume"
|
||||||
|
EVENT_MICROPHONE_VOLUME = "microphone_volume"
|
||||||
|
|
||||||
supported_hf_features: int
|
supported_hf_features: int
|
||||||
supported_hf_indicators: Set[HfIndicator]
|
supported_hf_indicators: Set[HfIndicator]
|
||||||
supported_audio_codecs: List[AudioCodec]
|
supported_audio_codecs: List[AudioCodec]
|
||||||
@@ -1371,7 +1395,7 @@ class AgProtocol(pyee.EventEmitter):
|
|||||||
|
|
||||||
def _check_remained_slc_commands(self) -> None:
|
def _check_remained_slc_commands(self) -> None:
|
||||||
if not self._remained_slc_setup_features:
|
if not self._remained_slc_setup_features:
|
||||||
self.emit('slc_complete')
|
self.emit(self.EVENT_SLC_COMPLETE)
|
||||||
|
|
||||||
def _on_brsf(self, hf_features: bytes) -> None:
|
def _on_brsf(self, hf_features: bytes) -> None:
|
||||||
self.supported_hf_features = int(hf_features)
|
self.supported_hf_features = int(hf_features)
|
||||||
@@ -1390,17 +1414,17 @@ class AgProtocol(pyee.EventEmitter):
|
|||||||
|
|
||||||
def _on_bac(self, *args) -> None:
|
def _on_bac(self, *args) -> None:
|
||||||
self.supported_audio_codecs = [AudioCodec(int(value)) for value in args]
|
self.supported_audio_codecs = [AudioCodec(int(value)) for value in args]
|
||||||
self.emit('supported_audio_codecs', self.supported_audio_codecs)
|
self.emit(self.EVENT_SUPPORTED_AUDIO_CODECS, self.supported_audio_codecs)
|
||||||
self.send_ok()
|
self.send_ok()
|
||||||
|
|
||||||
def _on_bcs(self, codec: bytes) -> None:
|
def _on_bcs(self, codec: bytes) -> None:
|
||||||
self.active_codec = AudioCodec(int(codec))
|
self.active_codec = AudioCodec(int(codec))
|
||||||
self.send_ok()
|
self.send_ok()
|
||||||
self.emit('codec_negotiation', self.active_codec)
|
self.emit(self.EVENT_CODEC_NEGOTIATION, self.active_codec)
|
||||||
|
|
||||||
def _on_bvra(self, vrec: bytes) -> None:
|
def _on_bvra(self, vrec: bytes) -> None:
|
||||||
self.send_ok()
|
self.send_ok()
|
||||||
self.emit('voice_recognition', VoiceRecognitionState(int(vrec)))
|
self.emit(self.EVENT_VOICE_RECOGNITION, VoiceRecognitionState(int(vrec)))
|
||||||
|
|
||||||
def _on_chld(self, operation_code: bytes) -> None:
|
def _on_chld(self, operation_code: bytes) -> None:
|
||||||
call_index: Optional[int] = None
|
call_index: Optional[int] = None
|
||||||
@@ -1427,7 +1451,7 @@ class AgProtocol(pyee.EventEmitter):
|
|||||||
# Real three-way calls have more complicated situations, but this is not a popular issue - let users to handle the remaining :)
|
# Real three-way calls have more complicated situations, but this is not a popular issue - let users to handle the remaining :)
|
||||||
|
|
||||||
self.send_ok()
|
self.send_ok()
|
||||||
self.emit('call_hold', operation, call_index)
|
self.emit(self.EVENT_CALL_HOLD, operation, call_index)
|
||||||
|
|
||||||
def _on_chld_test(self) -> None:
|
def _on_chld_test(self) -> None:
|
||||||
if not self.supports_ag_feature(AgFeature.THREE_WAY_CALLING):
|
if not self.supports_ag_feature(AgFeature.THREE_WAY_CALLING):
|
||||||
@@ -1553,7 +1577,7 @@ class AgProtocol(pyee.EventEmitter):
|
|||||||
return
|
return
|
||||||
|
|
||||||
self.hf_indicators[index].current_status = int(value_bytes)
|
self.hf_indicators[index].current_status = int(value_bytes)
|
||||||
self.emit('hf_indicator', self.hf_indicators[index])
|
self.emit(self.EVENT_HF_INDICATOR, self.hf_indicators[index])
|
||||||
self.send_ok()
|
self.send_ok()
|
||||||
|
|
||||||
def _on_bia(self, *args) -> None:
|
def _on_bia(self, *args) -> None:
|
||||||
@@ -1562,21 +1586,21 @@ class AgProtocol(pyee.EventEmitter):
|
|||||||
self.send_ok()
|
self.send_ok()
|
||||||
|
|
||||||
def _on_bcc(self) -> None:
|
def _on_bcc(self) -> None:
|
||||||
self.emit('codec_connection_request')
|
self.emit(self.EVENT_CODEC_CONNECTION_REQUEST)
|
||||||
self.send_ok()
|
self.send_ok()
|
||||||
|
|
||||||
def _on_a(self) -> None:
|
def _on_a(self) -> None:
|
||||||
"""ATA handler."""
|
"""ATA handler."""
|
||||||
self.emit('answer')
|
self.emit(self.EVENT_ANSWER)
|
||||||
self.send_ok()
|
self.send_ok()
|
||||||
|
|
||||||
def _on_d(self, number: bytes) -> None:
|
def _on_d(self, number: bytes) -> None:
|
||||||
"""ATD handler."""
|
"""ATD handler."""
|
||||||
self.emit('dial', number.decode())
|
self.emit(self.EVENT_DIAL, number.decode())
|
||||||
self.send_ok()
|
self.send_ok()
|
||||||
|
|
||||||
def _on_chup(self) -> None:
|
def _on_chup(self) -> None:
|
||||||
self.emit('hang_up')
|
self.emit(self.EVENT_HANG_UP)
|
||||||
self.send_ok()
|
self.send_ok()
|
||||||
|
|
||||||
def _on_clcc(self) -> None:
|
def _on_clcc(self) -> None:
|
||||||
@@ -1602,11 +1626,11 @@ class AgProtocol(pyee.EventEmitter):
|
|||||||
self.send_ok()
|
self.send_ok()
|
||||||
|
|
||||||
def _on_vgs(self, level: bytes) -> None:
|
def _on_vgs(self, level: bytes) -> None:
|
||||||
self.emit('speaker_volume', int(level))
|
self.emit(self.EVENT_SPEAKER_VOLUME, int(level))
|
||||||
self.send_ok()
|
self.send_ok()
|
||||||
|
|
||||||
def _on_vgm(self, level: bytes) -> None:
|
def _on_vgm(self, level: bytes) -> None:
|
||||||
self.emit('microphone_volume', int(level))
|
self.emit(self.EVENT_MICROPHONE_VOLUME, int(level))
|
||||||
self.send_ok()
|
self.send_ok()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+28
-15
@@ -22,11 +22,12 @@ import enum
|
|||||||
import struct
|
import struct
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from pyee import EventEmitter
|
|
||||||
from typing import Optional, Callable
|
from typing import Optional, Callable
|
||||||
from typing_extensions import override
|
from typing_extensions import override
|
||||||
|
|
||||||
from bumble import l2cap, device
|
from bumble import l2cap
|
||||||
|
from bumble import device
|
||||||
|
from bumble import utils
|
||||||
from bumble.core import InvalidStateError, ProtocolError
|
from bumble.core import InvalidStateError, ProtocolError
|
||||||
from bumble.hci import Address
|
from bumble.hci import Address
|
||||||
|
|
||||||
@@ -195,11 +196,18 @@ class SendHandshakeMessage(Message):
|
|||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class HID(ABC, EventEmitter):
|
class HID(ABC, utils.EventEmitter):
|
||||||
l2cap_ctrl_channel: Optional[l2cap.ClassicChannel] = None
|
l2cap_ctrl_channel: Optional[l2cap.ClassicChannel] = None
|
||||||
l2cap_intr_channel: Optional[l2cap.ClassicChannel] = None
|
l2cap_intr_channel: Optional[l2cap.ClassicChannel] = None
|
||||||
connection: Optional[device.Connection] = None
|
connection: Optional[device.Connection] = None
|
||||||
|
|
||||||
|
EVENT_INTERRUPT_DATA = "interrupt_data"
|
||||||
|
EVENT_CONTROL_DATA = "control_data"
|
||||||
|
EVENT_SUSPEND = "suspend"
|
||||||
|
EVENT_EXIT_SUSPEND = "exit_suspend"
|
||||||
|
EVENT_VIRTUAL_CABLE_UNPLUG = "virtual_cable_unplug"
|
||||||
|
EVENT_HANDSHAKE = "handshake"
|
||||||
|
|
||||||
class Role(enum.IntEnum):
|
class Role(enum.IntEnum):
|
||||||
HOST = 0x00
|
HOST = 0x00
|
||||||
DEVICE = 0x01
|
DEVICE = 0x01
|
||||||
@@ -214,7 +222,7 @@ class HID(ABC, EventEmitter):
|
|||||||
device.register_l2cap_server(HID_CONTROL_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.register_l2cap_server(HID_INTERRUPT_PSM, self.on_l2cap_connection)
|
||||||
|
|
||||||
device.on('connection', self.on_device_connection)
|
device.on(device.EVENT_CONNECTION, self.on_device_connection)
|
||||||
|
|
||||||
async def connect_control_channel(self) -> None:
|
async def connect_control_channel(self) -> None:
|
||||||
# Create a new L2CAP connection - control channel
|
# Create a new L2CAP connection - control channel
|
||||||
@@ -257,15 +265,20 @@ class HID(ABC, EventEmitter):
|
|||||||
def on_device_connection(self, connection: device.Connection) -> None:
|
def on_device_connection(self, connection: device.Connection) -> None:
|
||||||
self.connection = connection
|
self.connection = connection
|
||||||
self.remote_device_bd_address = connection.peer_address
|
self.remote_device_bd_address = connection.peer_address
|
||||||
connection.on('disconnection', self.on_device_disconnection)
|
connection.on(connection.EVENT_DISCONNECTION, self.on_device_disconnection)
|
||||||
|
|
||||||
def on_device_disconnection(self, reason: int) -> None:
|
def on_device_disconnection(self, reason: int) -> None:
|
||||||
self.connection = None
|
self.connection = None
|
||||||
|
|
||||||
def on_l2cap_connection(self, l2cap_channel: l2cap.ClassicChannel) -> None:
|
def on_l2cap_connection(self, l2cap_channel: l2cap.ClassicChannel) -> None:
|
||||||
logger.debug(f'+++ New L2CAP connection: {l2cap_channel}')
|
logger.debug(f'+++ New L2CAP connection: {l2cap_channel}')
|
||||||
l2cap_channel.on('open', lambda: self.on_l2cap_channel_open(l2cap_channel))
|
l2cap_channel.on(
|
||||||
l2cap_channel.on('close', lambda: self.on_l2cap_channel_close(l2cap_channel))
|
l2cap_channel.EVENT_OPEN, lambda: self.on_l2cap_channel_open(l2cap_channel)
|
||||||
|
)
|
||||||
|
l2cap_channel.on(
|
||||||
|
l2cap_channel.EVENT_CLOSE,
|
||||||
|
lambda: self.on_l2cap_channel_close(l2cap_channel),
|
||||||
|
)
|
||||||
|
|
||||||
def on_l2cap_channel_open(self, l2cap_channel: l2cap.ClassicChannel) -> None:
|
def on_l2cap_channel_open(self, l2cap_channel: l2cap.ClassicChannel) -> None:
|
||||||
if l2cap_channel.psm == HID_CONTROL_PSM:
|
if l2cap_channel.psm == HID_CONTROL_PSM:
|
||||||
@@ -289,7 +302,7 @@ class HID(ABC, EventEmitter):
|
|||||||
|
|
||||||
def on_intr_pdu(self, pdu: bytes) -> None:
|
def on_intr_pdu(self, pdu: bytes) -> None:
|
||||||
logger.debug(f'<<< HID INTERRUPT PDU: {pdu.hex()}')
|
logger.debug(f'<<< HID INTERRUPT PDU: {pdu.hex()}')
|
||||||
self.emit("interrupt_data", pdu)
|
self.emit(self.EVENT_INTERRUPT_DATA, pdu)
|
||||||
|
|
||||||
def send_pdu_on_ctrl(self, msg: bytes) -> None:
|
def send_pdu_on_ctrl(self, msg: bytes) -> None:
|
||||||
assert self.l2cap_ctrl_channel
|
assert self.l2cap_ctrl_channel
|
||||||
@@ -362,17 +375,17 @@ class Device(HID):
|
|||||||
self.handle_set_protocol(pdu)
|
self.handle_set_protocol(pdu)
|
||||||
elif message_type == Message.MessageType.DATA:
|
elif message_type == Message.MessageType.DATA:
|
||||||
logger.debug('<<< HID CONTROL DATA')
|
logger.debug('<<< HID CONTROL DATA')
|
||||||
self.emit('control_data', pdu)
|
self.emit(self.EVENT_CONTROL_DATA, pdu)
|
||||||
elif message_type == Message.MessageType.CONTROL:
|
elif message_type == Message.MessageType.CONTROL:
|
||||||
if param == Message.ControlCommand.SUSPEND:
|
if param == Message.ControlCommand.SUSPEND:
|
||||||
logger.debug('<<< HID SUSPEND')
|
logger.debug('<<< HID SUSPEND')
|
||||||
self.emit('suspend')
|
self.emit(self.EVENT_SUSPEND)
|
||||||
elif param == Message.ControlCommand.EXIT_SUSPEND:
|
elif param == Message.ControlCommand.EXIT_SUSPEND:
|
||||||
logger.debug('<<< HID EXIT SUSPEND')
|
logger.debug('<<< HID EXIT SUSPEND')
|
||||||
self.emit('exit_suspend')
|
self.emit(self.EVENT_EXIT_SUSPEND)
|
||||||
elif param == Message.ControlCommand.VIRTUAL_CABLE_UNPLUG:
|
elif param == Message.ControlCommand.VIRTUAL_CABLE_UNPLUG:
|
||||||
logger.debug('<<< HID VIRTUAL CABLE UNPLUG')
|
logger.debug('<<< HID VIRTUAL CABLE UNPLUG')
|
||||||
self.emit('virtual_cable_unplug')
|
self.emit(self.EVENT_VIRTUAL_CABLE_UNPLUG)
|
||||||
else:
|
else:
|
||||||
logger.debug('<<< HID CONTROL OPERATION UNSUPPORTED')
|
logger.debug('<<< HID CONTROL OPERATION UNSUPPORTED')
|
||||||
else:
|
else:
|
||||||
@@ -537,14 +550,14 @@ class Host(HID):
|
|||||||
message_type = pdu[0] >> 4
|
message_type = pdu[0] >> 4
|
||||||
if message_type == Message.MessageType.HANDSHAKE:
|
if message_type == Message.MessageType.HANDSHAKE:
|
||||||
logger.debug(f'<<< HID HANDSHAKE: {Message.Handshake(param).name}')
|
logger.debug(f'<<< HID HANDSHAKE: {Message.Handshake(param).name}')
|
||||||
self.emit('handshake', Message.Handshake(param))
|
self.emit(self.EVENT_HANDSHAKE, Message.Handshake(param))
|
||||||
elif message_type == Message.MessageType.DATA:
|
elif message_type == Message.MessageType.DATA:
|
||||||
logger.debug('<<< HID CONTROL DATA')
|
logger.debug('<<< HID CONTROL DATA')
|
||||||
self.emit('control_data', pdu)
|
self.emit(self.EVENT_CONTROL_DATA, pdu)
|
||||||
elif message_type == Message.MessageType.CONTROL:
|
elif message_type == Message.MessageType.CONTROL:
|
||||||
if param == Message.ControlCommand.VIRTUAL_CABLE_UNPLUG:
|
if param == Message.ControlCommand.VIRTUAL_CABLE_UNPLUG:
|
||||||
logger.debug('<<< HID VIRTUAL CABLE UNPLUG')
|
logger.debug('<<< HID VIRTUAL CABLE UNPLUG')
|
||||||
self.emit('virtual_cable_unplug')
|
self.emit(self.EVENT_VIRTUAL_CABLE_UNPLUG)
|
||||||
else:
|
else:
|
||||||
logger.debug('<<< HID CONTROL OPERATION UNSUPPORTED')
|
logger.debug('<<< HID CONTROL OPERATION UNSUPPORTED')
|
||||||
else:
|
else:
|
||||||
|
|||||||
+46
-16
@@ -34,7 +34,6 @@ from typing import (
|
|||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
)
|
)
|
||||||
|
|
||||||
import pyee
|
|
||||||
|
|
||||||
from bumble.colors import color
|
from bumble.colors import color
|
||||||
from bumble.l2cap import L2CAP_PDU
|
from bumble.l2cap import L2CAP_PDU
|
||||||
@@ -42,17 +41,16 @@ from bumble.snoop import Snooper
|
|||||||
from bumble import drivers
|
from bumble import drivers
|
||||||
from bumble import hci
|
from bumble import hci
|
||||||
from bumble.core import (
|
from bumble.core import (
|
||||||
BT_BR_EDR_TRANSPORT,
|
PhysicalTransport,
|
||||||
BT_LE_TRANSPORT,
|
|
||||||
PhysicalTransport,
|
PhysicalTransport,
|
||||||
ConnectionPHY,
|
ConnectionPHY,
|
||||||
ConnectionParameters,
|
ConnectionParameters,
|
||||||
)
|
)
|
||||||
from bumble.utils import AbortableEventEmitter
|
from bumble import utils
|
||||||
from bumble.transport.common import TransportLostError
|
from bumble.transport.common import TransportLostError
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .transport.common import TransportSink, TransportSource
|
from bumble.transport.common import TransportSink, TransportSource
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -62,7 +60,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class DataPacketQueue(pyee.EventEmitter):
|
class DataPacketQueue(utils.EventEmitter):
|
||||||
"""
|
"""
|
||||||
Flow-control queue for host->controller data packets (ACL, ISO).
|
Flow-control queue for host->controller data packets (ACL, ISO).
|
||||||
|
|
||||||
@@ -200,7 +198,7 @@ class Connection:
|
|||||||
self.transport = transport
|
self.transport = transport
|
||||||
acl_packet_queue: Optional[DataPacketQueue] = (
|
acl_packet_queue: Optional[DataPacketQueue] = (
|
||||||
host.le_acl_packet_queue
|
host.le_acl_packet_queue
|
||||||
if transport == BT_LE_TRANSPORT
|
if transport == PhysicalTransport.LE
|
||||||
else host.acl_packet_queue
|
else host.acl_packet_queue
|
||||||
)
|
)
|
||||||
assert acl_packet_queue
|
assert acl_packet_queue
|
||||||
@@ -235,7 +233,7 @@ class IsoLink:
|
|||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class Host(AbortableEventEmitter):
|
class Host(utils.EventEmitter):
|
||||||
connections: Dict[int, Connection]
|
connections: Dict[int, Connection]
|
||||||
cis_links: Dict[int, IsoLink]
|
cis_links: Dict[int, IsoLink]
|
||||||
bis_links: Dict[int, IsoLink]
|
bis_links: Dict[int, IsoLink]
|
||||||
@@ -437,6 +435,14 @@ class Host(AbortableEventEmitter):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
if self.supports_command(hci.HCI_SET_EVENT_MASK_PAGE_2_COMMAND):
|
||||||
|
await self.send_command(
|
||||||
|
hci.HCI_Set_Event_Mask_Page_2_Command(
|
||||||
|
event_mask_page_2=hci.HCI_Set_Event_Mask_Page_2_Command.mask(
|
||||||
|
[hci.HCI_ENCRYPTION_CHANGE_V2_EVENT]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
self.local_version is not None
|
self.local_version is not None
|
||||||
@@ -458,6 +464,7 @@ class Host(AbortableEventEmitter):
|
|||||||
hci.HCI_LE_READ_LOCAL_P_256_PUBLIC_KEY_COMPLETE_EVENT,
|
hci.HCI_LE_READ_LOCAL_P_256_PUBLIC_KEY_COMPLETE_EVENT,
|
||||||
hci.HCI_LE_GENERATE_DHKEY_COMPLETE_EVENT,
|
hci.HCI_LE_GENERATE_DHKEY_COMPLETE_EVENT,
|
||||||
hci.HCI_LE_ENHANCED_CONNECTION_COMPLETE_EVENT,
|
hci.HCI_LE_ENHANCED_CONNECTION_COMPLETE_EVENT,
|
||||||
|
hci.HCI_LE_ENHANCED_CONNECTION_COMPLETE_V2_EVENT,
|
||||||
hci.HCI_LE_DIRECTED_ADVERTISING_REPORT_EVENT,
|
hci.HCI_LE_DIRECTED_ADVERTISING_REPORT_EVENT,
|
||||||
hci.HCI_LE_PHY_UPDATE_COMPLETE_EVENT,
|
hci.HCI_LE_PHY_UPDATE_COMPLETE_EVENT,
|
||||||
hci.HCI_LE_EXTENDED_ADVERTISING_REPORT_EVENT,
|
hci.HCI_LE_EXTENDED_ADVERTISING_REPORT_EVENT,
|
||||||
@@ -967,7 +974,7 @@ class Host(AbortableEventEmitter):
|
|||||||
self,
|
self,
|
||||||
event.connection_handle,
|
event.connection_handle,
|
||||||
event.peer_address,
|
event.peer_address,
|
||||||
BT_LE_TRANSPORT,
|
PhysicalTransport.LE,
|
||||||
)
|
)
|
||||||
self.connections[event.connection_handle] = connection
|
self.connections[event.connection_handle] = connection
|
||||||
|
|
||||||
@@ -980,7 +987,7 @@ class Host(AbortableEventEmitter):
|
|||||||
self.emit(
|
self.emit(
|
||||||
'connection',
|
'connection',
|
||||||
event.connection_handle,
|
event.connection_handle,
|
||||||
BT_LE_TRANSPORT,
|
PhysicalTransport.LE,
|
||||||
event.peer_address,
|
event.peer_address,
|
||||||
getattr(event, 'local_resolvable_private_address', None),
|
getattr(event, 'local_resolvable_private_address', None),
|
||||||
getattr(event, 'peer_resolvable_private_address', None),
|
getattr(event, 'peer_resolvable_private_address', None),
|
||||||
@@ -992,7 +999,10 @@ class Host(AbortableEventEmitter):
|
|||||||
|
|
||||||
# Notify the listeners
|
# Notify the listeners
|
||||||
self.emit(
|
self.emit(
|
||||||
'connection_failure', BT_LE_TRANSPORT, event.peer_address, event.status
|
'connection_failure',
|
||||||
|
PhysicalTransport.LE,
|
||||||
|
event.peer_address,
|
||||||
|
event.status,
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_hci_le_enhanced_connection_complete_event(self, event):
|
def on_hci_le_enhanced_connection_complete_event(self, event):
|
||||||
@@ -1017,7 +1027,7 @@ class Host(AbortableEventEmitter):
|
|||||||
self,
|
self,
|
||||||
event.connection_handle,
|
event.connection_handle,
|
||||||
event.bd_addr,
|
event.bd_addr,
|
||||||
BT_BR_EDR_TRANSPORT,
|
PhysicalTransport.BR_EDR,
|
||||||
)
|
)
|
||||||
self.connections[event.connection_handle] = connection
|
self.connections[event.connection_handle] = connection
|
||||||
|
|
||||||
@@ -1025,7 +1035,7 @@ class Host(AbortableEventEmitter):
|
|||||||
self.emit(
|
self.emit(
|
||||||
'connection',
|
'connection',
|
||||||
event.connection_handle,
|
event.connection_handle,
|
||||||
BT_BR_EDR_TRANSPORT,
|
PhysicalTransport.BR_EDR,
|
||||||
event.bd_addr,
|
event.bd_addr,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
@@ -1037,7 +1047,10 @@ class Host(AbortableEventEmitter):
|
|||||||
|
|
||||||
# Notify the client
|
# Notify the client
|
||||||
self.emit(
|
self.emit(
|
||||||
'connection_failure', BT_BR_EDR_TRANSPORT, event.bd_addr, event.status
|
'connection_failure',
|
||||||
|
PhysicalTransport.BR_EDR,
|
||||||
|
event.bd_addr,
|
||||||
|
event.status,
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_hci_disconnection_complete_event(self, event):
|
def on_hci_disconnection_complete_event(self, event):
|
||||||
@@ -1284,7 +1297,8 @@ class Host(AbortableEventEmitter):
|
|||||||
logger.debug('no long term key provider')
|
logger.debug('no long term key provider')
|
||||||
long_term_key = None
|
long_term_key = None
|
||||||
else:
|
else:
|
||||||
long_term_key = await self.abort_on(
|
long_term_key = await utils.cancel_on_event(
|
||||||
|
self,
|
||||||
'flush',
|
'flush',
|
||||||
# pylint: disable-next=not-callable
|
# pylint: disable-next=not-callable
|
||||||
self.long_term_key_provider(
|
self.long_term_key_provider(
|
||||||
@@ -1378,6 +1392,21 @@ class Host(AbortableEventEmitter):
|
|||||||
'connection_encryption_change',
|
'connection_encryption_change',
|
||||||
event.connection_handle,
|
event.connection_handle,
|
||||||
event.encryption_enabled,
|
event.encryption_enabled,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.emit(
|
||||||
|
'connection_encryption_failure', event.connection_handle, event.status
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_hci_encryption_change_v2_event(self, event):
|
||||||
|
# Notify the client
|
||||||
|
if event.status == hci.HCI_SUCCESS:
|
||||||
|
self.emit(
|
||||||
|
'connection_encryption_change',
|
||||||
|
event.connection_handle,
|
||||||
|
event.encryption_enabled,
|
||||||
|
event.encryption_key_size,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.emit(
|
self.emit(
|
||||||
@@ -1442,7 +1471,8 @@ class Host(AbortableEventEmitter):
|
|||||||
logger.debug('no link key provider')
|
logger.debug('no link key provider')
|
||||||
link_key = None
|
link_key = None
|
||||||
else:
|
else:
|
||||||
link_key = await self.abort_on(
|
link_key = await utils.cancel_on_event(
|
||||||
|
self,
|
||||||
'flush',
|
'flush',
|
||||||
# pylint: disable-next=not-callable
|
# pylint: disable-next=not-callable
|
||||||
self.link_key_provider(event.bd_addr),
|
self.link_key_provider(event.bd_addr),
|
||||||
|
|||||||
+64
-48
@@ -22,17 +22,18 @@
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import dataclasses
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type
|
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type, Any
|
||||||
from typing_extensions import Self
|
from typing_extensions import Self
|
||||||
|
|
||||||
from .colors import color
|
from bumble.colors import color
|
||||||
from .hci import Address
|
from bumble import hci
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .device import Device
|
from bumble.device import Device
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -42,16 +43,17 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
@dataclasses.dataclass
|
||||||
class PairingKeys:
|
class PairingKeys:
|
||||||
|
@dataclasses.dataclass
|
||||||
class Key:
|
class Key:
|
||||||
def __init__(self, value, authenticated=False, ediv=None, rand=None):
|
value: bytes
|
||||||
self.value = value
|
authenticated: bool = False
|
||||||
self.authenticated = authenticated
|
ediv: Optional[int] = None
|
||||||
self.ediv = ediv
|
rand: Optional[bytes] = None
|
||||||
self.rand = rand
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, key_dict):
|
def from_dict(cls, key_dict: dict[str, Any]) -> PairingKeys.Key:
|
||||||
value = bytes.fromhex(key_dict['value'])
|
value = bytes.fromhex(key_dict['value'])
|
||||||
authenticated = key_dict.get('authenticated', False)
|
authenticated = key_dict.get('authenticated', False)
|
||||||
ediv = key_dict.get('ediv')
|
ediv = key_dict.get('ediv')
|
||||||
@@ -61,7 +63,7 @@ class PairingKeys:
|
|||||||
|
|
||||||
return cls(value, authenticated, ediv, rand)
|
return cls(value, authenticated, ediv, rand)
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self) -> dict[str, Any]:
|
||||||
key_dict = {'value': self.value.hex(), 'authenticated': self.authenticated}
|
key_dict = {'value': self.value.hex(), 'authenticated': self.authenticated}
|
||||||
if self.ediv is not None:
|
if self.ediv is not None:
|
||||||
key_dict['ediv'] = self.ediv
|
key_dict['ediv'] = self.ediv
|
||||||
@@ -70,39 +72,42 @@ class PairingKeys:
|
|||||||
|
|
||||||
return key_dict
|
return key_dict
|
||||||
|
|
||||||
def __init__(self):
|
address_type: Optional[hci.AddressType] = None
|
||||||
self.address_type = None
|
ltk: Optional[Key] = None
|
||||||
self.ltk = None
|
ltk_central: Optional[Key] = None
|
||||||
self.ltk_central = None
|
ltk_peripheral: Optional[Key] = None
|
||||||
self.ltk_peripheral = None
|
irk: Optional[Key] = None
|
||||||
self.irk = None
|
csrk: Optional[Key] = None
|
||||||
self.csrk = None
|
link_key: Optional[Key] = None # Classic
|
||||||
self.link_key = None # Classic
|
link_key_type: Optional[int] = None # Classic
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def key_from_dict(keys_dict, key_name):
|
def key_from_dict(cls, keys_dict: dict[str, Any], key_name: str) -> Optional[Key]:
|
||||||
key_dict = keys_dict.get(key_name)
|
key_dict = keys_dict.get(key_name)
|
||||||
if key_dict is None:
|
if key_dict is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return PairingKeys.Key.from_dict(key_dict)
|
return PairingKeys.Key.from_dict(key_dict)
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def from_dict(keys_dict):
|
def from_dict(cls, keys_dict: dict[str, Any]) -> PairingKeys:
|
||||||
keys = PairingKeys()
|
return PairingKeys(
|
||||||
|
address_type=(
|
||||||
|
hci.AddressType(t)
|
||||||
|
if (t := keys_dict.get('address_type')) is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
ltk=PairingKeys.key_from_dict(keys_dict, 'ltk'),
|
||||||
|
ltk_central=PairingKeys.key_from_dict(keys_dict, 'ltk_central'),
|
||||||
|
ltk_peripheral=PairingKeys.key_from_dict(keys_dict, 'ltk_peripheral'),
|
||||||
|
irk=PairingKeys.key_from_dict(keys_dict, 'irk'),
|
||||||
|
csrk=PairingKeys.key_from_dict(keys_dict, 'csrk'),
|
||||||
|
link_key=PairingKeys.key_from_dict(keys_dict, 'link_key'),
|
||||||
|
link_key_type=keys_dict.get('link_key_type'),
|
||||||
|
)
|
||||||
|
|
||||||
keys.address_type = keys_dict.get('address_type')
|
def to_dict(self) -> dict[str, Any]:
|
||||||
keys.ltk = PairingKeys.key_from_dict(keys_dict, 'ltk')
|
keys: dict[str, Any] = {}
|
||||||
keys.ltk_central = PairingKeys.key_from_dict(keys_dict, 'ltk_central')
|
|
||||||
keys.ltk_peripheral = PairingKeys.key_from_dict(keys_dict, 'ltk_peripheral')
|
|
||||||
keys.irk = PairingKeys.key_from_dict(keys_dict, 'irk')
|
|
||||||
keys.csrk = PairingKeys.key_from_dict(keys_dict, 'csrk')
|
|
||||||
keys.link_key = PairingKeys.key_from_dict(keys_dict, 'link_key')
|
|
||||||
|
|
||||||
return keys
|
|
||||||
|
|
||||||
def to_dict(self):
|
|
||||||
keys = {}
|
|
||||||
|
|
||||||
if self.address_type is not None:
|
if self.address_type is not None:
|
||||||
keys['address_type'] = self.address_type
|
keys['address_type'] = self.address_type
|
||||||
@@ -125,9 +130,12 @@ class PairingKeys:
|
|||||||
if self.link_key is not None:
|
if self.link_key is not None:
|
||||||
keys['link_key'] = self.link_key.to_dict()
|
keys['link_key'] = self.link_key.to_dict()
|
||||||
|
|
||||||
|
if self.link_key_type is not None:
|
||||||
|
keys['link_key_type'] = self.link_key_type
|
||||||
|
|
||||||
return keys
|
return keys
|
||||||
|
|
||||||
def print(self, prefix=''):
|
def print(self, prefix: str = '') -> None:
|
||||||
keys_dict = self.to_dict()
|
keys_dict = self.to_dict()
|
||||||
for container_property, value in keys_dict.items():
|
for container_property, value in keys_dict.items():
|
||||||
if isinstance(value, dict):
|
if isinstance(value, dict):
|
||||||
@@ -156,20 +164,28 @@ class KeyStore:
|
|||||||
all_keys = await self.get_all()
|
all_keys = await self.get_all()
|
||||||
await asyncio.gather(*(self.delete(name) for (name, _) in all_keys))
|
await asyncio.gather(*(self.delete(name) for (name, _) in all_keys))
|
||||||
|
|
||||||
async def get_resolving_keys(self):
|
async def get_resolving_keys(self) -> list[tuple[bytes, hci.Address]]:
|
||||||
all_keys = await self.get_all()
|
all_keys = await self.get_all()
|
||||||
resolving_keys = []
|
resolving_keys = []
|
||||||
for name, keys in all_keys:
|
for name, keys in all_keys:
|
||||||
if keys.irk is not None:
|
if keys.irk is not None:
|
||||||
if keys.address_type is None:
|
resolving_keys.append(
|
||||||
address_type = Address.RANDOM_DEVICE_ADDRESS
|
(
|
||||||
else:
|
keys.irk.value,
|
||||||
address_type = keys.address_type
|
hci.Address(
|
||||||
resolving_keys.append((keys.irk.value, Address(name, address_type)))
|
name,
|
||||||
|
(
|
||||||
|
keys.address_type
|
||||||
|
if keys.address_type is not None
|
||||||
|
else hci.Address.RANDOM_DEVICE_ADDRESS
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return resolving_keys
|
return resolving_keys
|
||||||
|
|
||||||
async def print(self, prefix=''):
|
async def print(self, prefix: str = '') -> None:
|
||||||
entries = await self.get_all()
|
entries = await self.get_all()
|
||||||
separator = ''
|
separator = ''
|
||||||
for name, keys in entries:
|
for name, keys in entries:
|
||||||
@@ -177,8 +193,8 @@ class KeyStore:
|
|||||||
keys.print(prefix=prefix + ' ')
|
keys.print(prefix=prefix + ' ')
|
||||||
separator = '\n'
|
separator = '\n'
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def create_for_device(device: Device) -> KeyStore:
|
def create_for_device(cls, device: Device) -> KeyStore:
|
||||||
if device.config.keystore is None:
|
if device.config.keystore is None:
|
||||||
return MemoryKeyStore()
|
return MemoryKeyStore()
|
||||||
|
|
||||||
@@ -266,9 +282,9 @@ class JsonKeyStore(KeyStore):
|
|||||||
filename = params[0]
|
filename = params[0]
|
||||||
|
|
||||||
# Use a namespace based on the device address
|
# Use a namespace based on the device address
|
||||||
if device.public_address not in (Address.ANY, Address.ANY_RANDOM):
|
if device.public_address not in (hci.Address.ANY, hci.Address.ANY_RANDOM):
|
||||||
namespace = str(device.public_address)
|
namespace = str(device.public_address)
|
||||||
elif device.random_address != Address.ANY_RANDOM:
|
elif device.random_address != hci.Address.ANY_RANDOM:
|
||||||
namespace = str(device.random_address)
|
namespace = str(device.random_address)
|
||||||
else:
|
else:
|
||||||
namespace = JsonKeyStore.DEFAULT_NAMESPACE
|
namespace = JsonKeyStore.DEFAULT_NAMESPACE
|
||||||
|
|||||||
+38
-26
@@ -23,7 +23,6 @@ import logging
|
|||||||
import struct
|
import struct
|
||||||
|
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from pyee import EventEmitter
|
|
||||||
from typing import (
|
from typing import (
|
||||||
Dict,
|
Dict,
|
||||||
Type,
|
Type,
|
||||||
@@ -39,16 +38,16 @@ from typing import (
|
|||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .utils import deprecated
|
from bumble import utils
|
||||||
from .colors import color
|
from bumble.colors import color
|
||||||
from .core import (
|
from bumble.core import (
|
||||||
InvalidStateError,
|
InvalidStateError,
|
||||||
InvalidArgumentError,
|
InvalidArgumentError,
|
||||||
InvalidPacketError,
|
InvalidPacketError,
|
||||||
OutOfResourcesError,
|
OutOfResourcesError,
|
||||||
ProtocolError,
|
ProtocolError,
|
||||||
)
|
)
|
||||||
from .hci import (
|
from bumble.hci import (
|
||||||
HCI_LE_Connection_Update_Command,
|
HCI_LE_Connection_Update_Command,
|
||||||
HCI_Object,
|
HCI_Object,
|
||||||
Role,
|
Role,
|
||||||
@@ -720,7 +719,7 @@ class L2CAP_LE_Flow_Control_Credit(L2CAP_Control_Frame):
|
|||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class ClassicChannel(EventEmitter):
|
class ClassicChannel(utils.EventEmitter):
|
||||||
class State(enum.IntEnum):
|
class State(enum.IntEnum):
|
||||||
# States
|
# States
|
||||||
CLOSED = 0x00
|
CLOSED = 0x00
|
||||||
@@ -745,6 +744,9 @@ class ClassicChannel(EventEmitter):
|
|||||||
WAIT_FINAL_RSP = 0x16
|
WAIT_FINAL_RSP = 0x16
|
||||||
WAIT_CONTROL_IND = 0x17
|
WAIT_CONTROL_IND = 0x17
|
||||||
|
|
||||||
|
EVENT_OPEN = "open"
|
||||||
|
EVENT_CLOSE = "close"
|
||||||
|
|
||||||
connection_result: Optional[asyncio.Future[None]]
|
connection_result: Optional[asyncio.Future[None]]
|
||||||
disconnection_result: Optional[asyncio.Future[None]]
|
disconnection_result: Optional[asyncio.Future[None]]
|
||||||
response: Optional[asyncio.Future[bytes]]
|
response: Optional[asyncio.Future[bytes]]
|
||||||
@@ -821,8 +823,8 @@ class ClassicChannel(EventEmitter):
|
|||||||
|
|
||||||
# Wait for the connection to succeed or fail
|
# Wait for the connection to succeed or fail
|
||||||
try:
|
try:
|
||||||
return await self.connection.abort_on(
|
return await utils.cancel_on_event(
|
||||||
'disconnection', self.connection_result
|
self.connection, 'disconnection', self.connection_result
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
self.connection_result = None
|
self.connection_result = None
|
||||||
@@ -848,7 +850,7 @@ class ClassicChannel(EventEmitter):
|
|||||||
def abort(self) -> None:
|
def abort(self) -> None:
|
||||||
if self.state == self.State.OPEN:
|
if self.state == self.State.OPEN:
|
||||||
self._change_state(self.State.CLOSED)
|
self._change_state(self.State.CLOSED)
|
||||||
self.emit('close')
|
self.emit(self.EVENT_CLOSE)
|
||||||
|
|
||||||
def send_configure_request(self) -> None:
|
def send_configure_request(self) -> None:
|
||||||
options = L2CAP_Control_Frame.encode_configuration_options(
|
options = L2CAP_Control_Frame.encode_configuration_options(
|
||||||
@@ -941,7 +943,7 @@ class ClassicChannel(EventEmitter):
|
|||||||
if self.connection_result:
|
if self.connection_result:
|
||||||
self.connection_result.set_result(None)
|
self.connection_result.set_result(None)
|
||||||
self.connection_result = None
|
self.connection_result = None
|
||||||
self.emit('open')
|
self.emit(self.EVENT_OPEN)
|
||||||
elif self.state == self.State.WAIT_CONFIG_REQ_RSP:
|
elif self.state == self.State.WAIT_CONFIG_REQ_RSP:
|
||||||
self._change_state(self.State.WAIT_CONFIG_RSP)
|
self._change_state(self.State.WAIT_CONFIG_RSP)
|
||||||
|
|
||||||
@@ -957,7 +959,7 @@ class ClassicChannel(EventEmitter):
|
|||||||
if self.connection_result:
|
if self.connection_result:
|
||||||
self.connection_result.set_result(None)
|
self.connection_result.set_result(None)
|
||||||
self.connection_result = None
|
self.connection_result = None
|
||||||
self.emit('open')
|
self.emit(self.EVENT_OPEN)
|
||||||
else:
|
else:
|
||||||
logger.warning(color('invalid state', 'red'))
|
logger.warning(color('invalid state', 'red'))
|
||||||
elif (
|
elif (
|
||||||
@@ -992,7 +994,7 @@ class ClassicChannel(EventEmitter):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
self._change_state(self.State.CLOSED)
|
self._change_state(self.State.CLOSED)
|
||||||
self.emit('close')
|
self.emit(self.EVENT_CLOSE)
|
||||||
self.manager.on_channel_closed(self)
|
self.manager.on_channel_closed(self)
|
||||||
else:
|
else:
|
||||||
logger.warning(color('invalid state', 'red'))
|
logger.warning(color('invalid state', 'red'))
|
||||||
@@ -1013,7 +1015,7 @@ class ClassicChannel(EventEmitter):
|
|||||||
if self.disconnection_result:
|
if self.disconnection_result:
|
||||||
self.disconnection_result.set_result(None)
|
self.disconnection_result.set_result(None)
|
||||||
self.disconnection_result = None
|
self.disconnection_result = None
|
||||||
self.emit('close')
|
self.emit(self.EVENT_CLOSE)
|
||||||
self.manager.on_channel_closed(self)
|
self.manager.on_channel_closed(self)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
@@ -1026,7 +1028,7 @@ class ClassicChannel(EventEmitter):
|
|||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class LeCreditBasedChannel(EventEmitter):
|
class LeCreditBasedChannel(utils.EventEmitter):
|
||||||
"""
|
"""
|
||||||
LE Credit-based Connection Oriented Channel
|
LE Credit-based Connection Oriented Channel
|
||||||
"""
|
"""
|
||||||
@@ -1048,6 +1050,9 @@ class LeCreditBasedChannel(EventEmitter):
|
|||||||
connection: Connection
|
connection: Connection
|
||||||
sink: Optional[Callable[[bytes], Any]]
|
sink: Optional[Callable[[bytes], Any]]
|
||||||
|
|
||||||
|
EVENT_OPEN = "open"
|
||||||
|
EVENT_CLOSE = "close"
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
manager: ChannelManager,
|
manager: ChannelManager,
|
||||||
@@ -1099,9 +1104,9 @@ class LeCreditBasedChannel(EventEmitter):
|
|||||||
self.state = new_state
|
self.state = new_state
|
||||||
|
|
||||||
if new_state == self.State.CONNECTED:
|
if new_state == self.State.CONNECTED:
|
||||||
self.emit('open')
|
self.emit(self.EVENT_OPEN)
|
||||||
elif new_state == self.State.DISCONNECTED:
|
elif new_state == self.State.DISCONNECTED:
|
||||||
self.emit('close')
|
self.emit(self.EVENT_CLOSE)
|
||||||
|
|
||||||
def send_pdu(self, pdu: Union[SupportsBytes, bytes]) -> None:
|
def send_pdu(self, pdu: Union[SupportsBytes, bytes]) -> None:
|
||||||
self.manager.send_pdu(self.connection, self.destination_cid, pdu)
|
self.manager.send_pdu(self.connection, self.destination_cid, pdu)
|
||||||
@@ -1381,7 +1386,9 @@ class LeCreditBasedChannel(EventEmitter):
|
|||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class ClassicChannelServer(EventEmitter):
|
class ClassicChannelServer(utils.EventEmitter):
|
||||||
|
EVENT_CONNECTION = "connection"
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
manager: ChannelManager,
|
manager: ChannelManager,
|
||||||
@@ -1396,7 +1403,7 @@ class ClassicChannelServer(EventEmitter):
|
|||||||
self.mtu = mtu
|
self.mtu = mtu
|
||||||
|
|
||||||
def on_connection(self, channel: ClassicChannel) -> None:
|
def on_connection(self, channel: ClassicChannel) -> None:
|
||||||
self.emit('connection', channel)
|
self.emit(self.EVENT_CONNECTION, channel)
|
||||||
if self.handler:
|
if self.handler:
|
||||||
self.handler(channel)
|
self.handler(channel)
|
||||||
|
|
||||||
@@ -1406,7 +1413,9 @@ class ClassicChannelServer(EventEmitter):
|
|||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class LeCreditBasedChannelServer(EventEmitter):
|
class LeCreditBasedChannelServer(utils.EventEmitter):
|
||||||
|
EVENT_CONNECTION = "connection"
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
manager: ChannelManager,
|
manager: ChannelManager,
|
||||||
@@ -1425,7 +1434,7 @@ class LeCreditBasedChannelServer(EventEmitter):
|
|||||||
self.mps = mps
|
self.mps = mps
|
||||||
|
|
||||||
def on_connection(self, channel: LeCreditBasedChannel) -> None:
|
def on_connection(self, channel: LeCreditBasedChannel) -> None:
|
||||||
self.emit('connection', channel)
|
self.emit(self.EVENT_CONNECTION, channel)
|
||||||
if self.handler:
|
if self.handler:
|
||||||
self.handler(channel)
|
self.handler(channel)
|
||||||
|
|
||||||
@@ -1521,6 +1530,9 @@ class ChannelManager:
|
|||||||
|
|
||||||
def next_identifier(self, connection: Connection) -> int:
|
def next_identifier(self, connection: Connection) -> int:
|
||||||
identifier = (self.identifiers.setdefault(connection.handle, 0) + 1) % 256
|
identifier = (self.identifiers.setdefault(connection.handle, 0) + 1) % 256
|
||||||
|
# 0x00 is an invalid ID (BT Core Spec, Vol 3, Part A, Sect 4
|
||||||
|
if identifier == 0:
|
||||||
|
identifier = 1
|
||||||
self.identifiers[connection.handle] = identifier
|
self.identifiers[connection.handle] = identifier
|
||||||
return identifier
|
return identifier
|
||||||
|
|
||||||
@@ -1533,7 +1545,7 @@ class ChannelManager:
|
|||||||
if cid in self.fixed_channels:
|
if cid in self.fixed_channels:
|
||||||
del self.fixed_channels[cid]
|
del self.fixed_channels[cid]
|
||||||
|
|
||||||
@deprecated("Please use create_classic_server")
|
@utils.deprecated("Please use create_classic_server")
|
||||||
def register_server(
|
def register_server(
|
||||||
self,
|
self,
|
||||||
psm: int,
|
psm: int,
|
||||||
@@ -1579,7 +1591,7 @@ class ChannelManager:
|
|||||||
|
|
||||||
return self.servers[spec.psm]
|
return self.servers[spec.psm]
|
||||||
|
|
||||||
@deprecated("Please use create_le_credit_based_server()")
|
@utils.deprecated("Please use create_le_credit_based_server()")
|
||||||
def register_le_coc_server(
|
def register_le_coc_server(
|
||||||
self,
|
self,
|
||||||
psm: int,
|
psm: int,
|
||||||
@@ -2123,7 +2135,7 @@ class ChannelManager:
|
|||||||
if channel.source_cid in connection_channels:
|
if channel.source_cid in connection_channels:
|
||||||
del connection_channels[channel.source_cid]
|
del connection_channels[channel.source_cid]
|
||||||
|
|
||||||
@deprecated("Please use create_le_credit_based_channel()")
|
@utils.deprecated("Please use create_le_credit_based_channel()")
|
||||||
async def open_le_coc(
|
async def open_le_coc(
|
||||||
self, connection: Connection, psm: int, max_credits: int, mtu: int, mps: int
|
self, connection: Connection, psm: int, max_credits: int, mtu: int, mps: int
|
||||||
) -> LeCreditBasedChannel:
|
) -> LeCreditBasedChannel:
|
||||||
@@ -2180,7 +2192,7 @@ class ChannelManager:
|
|||||||
|
|
||||||
return channel
|
return channel
|
||||||
|
|
||||||
@deprecated("Please use create_classic_channel()")
|
@utils.deprecated("Please use create_classic_channel()")
|
||||||
async def connect(self, connection: Connection, psm: int) -> ClassicChannel:
|
async def connect(self, connection: Connection, psm: int) -> ClassicChannel:
|
||||||
return await self.create_classic_channel(
|
return await self.create_classic_channel(
|
||||||
connection=connection, spec=ClassicChannelSpec(psm=psm)
|
connection=connection, spec=ClassicChannelSpec(psm=psm)
|
||||||
@@ -2230,12 +2242,12 @@ class ChannelManager:
|
|||||||
|
|
||||||
|
|
||||||
class Channel(ClassicChannel):
|
class Channel(ClassicChannel):
|
||||||
@deprecated("Please use ClassicChannel")
|
@utils.deprecated("Please use ClassicChannel")
|
||||||
def __init__(self, *args, **kwargs) -> None:
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class LeConnectionOrientedChannel(LeCreditBasedChannel):
|
class LeConnectionOrientedChannel(LeCreditBasedChannel):
|
||||||
@deprecated("Please use LeCreditBasedChannel")
|
@utils.deprecated("Please use LeCreditBasedChannel")
|
||||||
def __init__(self, *args, **kwargs) -> None:
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|||||||
+3
-4
@@ -20,8 +20,7 @@ import asyncio
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from bumble.core import (
|
from bumble.core import (
|
||||||
BT_BR_EDR_TRANSPORT,
|
PhysicalTransport,
|
||||||
BT_LE_TRANSPORT,
|
|
||||||
InvalidStateError,
|
InvalidStateError,
|
||||||
)
|
)
|
||||||
from bumble.colors import color
|
from bumble.colors import color
|
||||||
@@ -116,10 +115,10 @@ class LocalLink:
|
|||||||
|
|
||||||
def send_acl_data(self, sender_controller, destination_address, transport, data):
|
def send_acl_data(self, sender_controller, destination_address, transport, data):
|
||||||
# Send the data to the first controller with a matching address
|
# Send the data to the first controller with a matching address
|
||||||
if transport == BT_LE_TRANSPORT:
|
if transport == PhysicalTransport.LE:
|
||||||
destination_controller = self.find_controller(destination_address)
|
destination_controller = self.find_controller(destination_address)
|
||||||
source_address = sender_controller.random_address
|
source_address = sender_controller.random_address
|
||||||
elif transport == BT_BR_EDR_TRANSPORT:
|
elif transport == PhysicalTransport.BR_EDR:
|
||||||
destination_controller = self.find_classic_controller(destination_address)
|
destination_controller = self.find_classic_controller(destination_address)
|
||||||
source_address = sender_controller.public_address
|
source_address = sender_controller.public_address
|
||||||
else:
|
else:
|
||||||
|
|||||||
+3
-3
@@ -20,14 +20,14 @@ import enum
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
from .hci import (
|
from bumble.hci import (
|
||||||
Address,
|
Address,
|
||||||
HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
|
HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
|
||||||
HCI_DISPLAY_ONLY_IO_CAPABILITY,
|
HCI_DISPLAY_ONLY_IO_CAPABILITY,
|
||||||
HCI_DISPLAY_YES_NO_IO_CAPABILITY,
|
HCI_DISPLAY_YES_NO_IO_CAPABILITY,
|
||||||
HCI_KEYBOARD_ONLY_IO_CAPABILITY,
|
HCI_KEYBOARD_ONLY_IO_CAPABILITY,
|
||||||
)
|
)
|
||||||
from .smp import (
|
from bumble.smp import (
|
||||||
SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
|
SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
|
||||||
SMP_KEYBOARD_ONLY_IO_CAPABILITY,
|
SMP_KEYBOARD_ONLY_IO_CAPABILITY,
|
||||||
SMP_DISPLAY_ONLY_IO_CAPABILITY,
|
SMP_DISPLAY_ONLY_IO_CAPABILITY,
|
||||||
@@ -41,7 +41,7 @@ from .smp import (
|
|||||||
OobLegacyContext,
|
OobLegacyContext,
|
||||||
OobSharedData,
|
OobSharedData,
|
||||||
)
|
)
|
||||||
from .core import AdvertisingData, LeRole
|
from bumble.core import AdvertisingData, LeRole
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -22,11 +22,11 @@ __version__ = "0.0.1"
|
|||||||
import grpc
|
import grpc
|
||||||
import grpc.aio
|
import grpc.aio
|
||||||
|
|
||||||
from .config import Config
|
from bumble.pandora.config import Config
|
||||||
from .device import PandoraDevice
|
from bumble.pandora.device import PandoraDevice
|
||||||
from .host import HostService
|
from bumble.pandora.host import HostService
|
||||||
from .l2cap import L2CAPService
|
from bumble.pandora.l2cap import L2CAPService
|
||||||
from .security import SecurityService, SecurityStorageService
|
from bumble.pandora.security import SecurityService, SecurityStorageService
|
||||||
from pandora.host_grpc_aio import add_HostServicer_to_server
|
from pandora.host_grpc_aio import add_HostServicer_to_server
|
||||||
from pandora.l2cap_grpc_aio import add_L2CAPServicer_to_server
|
from pandora.l2cap_grpc_aio import add_L2CAPServicer_to_server
|
||||||
from pandora.security_grpc_aio import (
|
from pandora.security_grpc_aio import (
|
||||||
|
|||||||
+29
-23
@@ -20,11 +20,11 @@ import grpc.aio
|
|||||||
import logging
|
import logging
|
||||||
import struct
|
import struct
|
||||||
|
|
||||||
from . import utils
|
import bumble.utils
|
||||||
from .config import Config
|
from bumble.pandora import utils
|
||||||
|
from bumble.pandora.config import Config
|
||||||
from bumble.core import (
|
from bumble.core import (
|
||||||
BT_BR_EDR_TRANSPORT,
|
PhysicalTransport,
|
||||||
BT_LE_TRANSPORT,
|
|
||||||
UUID,
|
UUID,
|
||||||
AdvertisingData,
|
AdvertisingData,
|
||||||
Appearance,
|
Appearance,
|
||||||
@@ -185,7 +185,7 @@ class HostService(HostServicer):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
connection = await self.device.connect(
|
connection = await self.device.connect(
|
||||||
address, transport=BT_BR_EDR_TRANSPORT
|
address, transport=PhysicalTransport.BR_EDR
|
||||||
)
|
)
|
||||||
except ConnectionError as e:
|
except ConnectionError as e:
|
||||||
if e.error_code == HCI_PAGE_TIMEOUT_ERROR:
|
if e.error_code == HCI_PAGE_TIMEOUT_ERROR:
|
||||||
@@ -218,7 +218,7 @@ class HostService(HostServicer):
|
|||||||
self.log.debug(f"WaitConnection from {address}...")
|
self.log.debug(f"WaitConnection from {address}...")
|
||||||
|
|
||||||
connection = self.device.find_connection_by_bd_addr(
|
connection = self.device.find_connection_by_bd_addr(
|
||||||
address, transport=BT_BR_EDR_TRANSPORT
|
address, transport=PhysicalTransport.BR_EDR
|
||||||
)
|
)
|
||||||
if connection and id(connection) in self.waited_connections:
|
if connection and id(connection) in self.waited_connections:
|
||||||
# this connection was already returned: wait for a new one.
|
# this connection was already returned: wait for a new one.
|
||||||
@@ -250,7 +250,7 @@ class HostService(HostServicer):
|
|||||||
try:
|
try:
|
||||||
connection = await self.device.connect(
|
connection = await self.device.connect(
|
||||||
address,
|
address,
|
||||||
transport=BT_LE_TRANSPORT,
|
transport=PhysicalTransport.LE,
|
||||||
own_address_type=OwnAddressType(request.own_address_type),
|
own_address_type=OwnAddressType(request.own_address_type),
|
||||||
)
|
)
|
||||||
except ConnectionError as e:
|
except ConnectionError as e:
|
||||||
@@ -296,12 +296,12 @@ class HostService(HostServicer):
|
|||||||
def on_disconnection(_: None) -> None:
|
def on_disconnection(_: None) -> None:
|
||||||
disconnection_future.set_result(None)
|
disconnection_future.set_result(None)
|
||||||
|
|
||||||
connection.on('disconnection', on_disconnection)
|
connection.on(connection.EVENT_DISCONNECTION, on_disconnection)
|
||||||
try:
|
try:
|
||||||
await disconnection_future
|
await disconnection_future
|
||||||
self.log.debug("Disconnected")
|
self.log.debug("Disconnected")
|
||||||
finally:
|
finally:
|
||||||
connection.remove_listener('disconnection', on_disconnection) # type: ignore
|
connection.remove_listener(connection.EVENT_DISCONNECTION, on_disconnection) # type: ignore
|
||||||
|
|
||||||
return empty_pb2.Empty()
|
return empty_pb2.Empty()
|
||||||
|
|
||||||
@@ -378,12 +378,12 @@ class HostService(HostServicer):
|
|||||||
|
|
||||||
def on_connection(connection: bumble.device.Connection) -> None:
|
def on_connection(connection: bumble.device.Connection) -> None:
|
||||||
if (
|
if (
|
||||||
connection.transport == BT_LE_TRANSPORT
|
connection.transport == PhysicalTransport.LE
|
||||||
and connection.role == Role.PERIPHERAL
|
and connection.role == Role.PERIPHERAL
|
||||||
):
|
):
|
||||||
connections.put_nowait(connection)
|
connections.put_nowait(connection)
|
||||||
|
|
||||||
self.device.on('connection', on_connection)
|
self.device.on(self.device.EVENT_CONNECTION, on_connection)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Advertise until RPC is canceled
|
# Advertise until RPC is canceled
|
||||||
@@ -496,12 +496,12 @@ class HostService(HostServicer):
|
|||||||
|
|
||||||
def on_connection(connection: bumble.device.Connection) -> None:
|
def on_connection(connection: bumble.device.Connection) -> None:
|
||||||
if (
|
if (
|
||||||
connection.transport == BT_LE_TRANSPORT
|
connection.transport == PhysicalTransport.LE
|
||||||
and connection.role == Role.PERIPHERAL
|
and connection.role == Role.PERIPHERAL
|
||||||
):
|
):
|
||||||
connections.put_nowait(connection)
|
connections.put_nowait(connection)
|
||||||
|
|
||||||
self.device.on('connection', on_connection)
|
self.device.on(self.device.EVENT_CONNECTION, on_connection)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
@@ -531,11 +531,13 @@ class HostService(HostServicer):
|
|||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
finally:
|
finally:
|
||||||
if request.connectable:
|
if request.connectable:
|
||||||
self.device.remove_listener('connection', on_connection) # type: ignore
|
self.device.remove_listener(self.device.EVENT_CONNECTION, on_connection) # type: ignore
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.log.debug('Stop advertising')
|
self.log.debug('Stop advertising')
|
||||||
await self.device.abort_on('flush', self.device.stop_advertising())
|
await bumble.utils.cancel_on_event(
|
||||||
|
self.device, 'flush', self.device.stop_advertising()
|
||||||
|
)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -555,7 +557,7 @@ class HostService(HostServicer):
|
|||||||
scanning_phys = [int(Phy.LE_1M), int(Phy.LE_CODED)]
|
scanning_phys = [int(Phy.LE_1M), int(Phy.LE_CODED)]
|
||||||
|
|
||||||
scan_queue: asyncio.Queue[Advertisement] = asyncio.Queue()
|
scan_queue: asyncio.Queue[Advertisement] = asyncio.Queue()
|
||||||
handler = self.device.on('advertisement', scan_queue.put_nowait)
|
handler = self.device.on(self.device.EVENT_ADVERTISEMENT, scan_queue.put_nowait)
|
||||||
await self.device.start_scanning(
|
await self.device.start_scanning(
|
||||||
legacy=request.legacy,
|
legacy=request.legacy,
|
||||||
active=not request.passive,
|
active=not request.passive,
|
||||||
@@ -600,10 +602,12 @@ class HostService(HostServicer):
|
|||||||
yield sr
|
yield sr
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
self.device.remove_listener('advertisement', handler) # type: ignore
|
self.device.remove_listener(self.device.EVENT_ADVERTISEMENT, handler) # type: ignore
|
||||||
try:
|
try:
|
||||||
self.log.debug('Stop scanning')
|
self.log.debug('Stop scanning')
|
||||||
await self.device.abort_on('flush', self.device.stop_scanning())
|
await bumble.utils.cancel_on_event(
|
||||||
|
self.device, 'flush', self.device.stop_scanning()
|
||||||
|
)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -617,10 +621,10 @@ class HostService(HostServicer):
|
|||||||
Optional[Tuple[Address, int, AdvertisingData, int]]
|
Optional[Tuple[Address, int, AdvertisingData, int]]
|
||||||
] = asyncio.Queue()
|
] = asyncio.Queue()
|
||||||
complete_handler = self.device.on(
|
complete_handler = self.device.on(
|
||||||
'inquiry_complete', lambda: inquiry_queue.put_nowait(None)
|
self.device.EVENT_INQUIRY_COMPLETE, lambda: inquiry_queue.put_nowait(None)
|
||||||
)
|
)
|
||||||
result_handler = self.device.on( # type: ignore
|
result_handler = self.device.on( # type: ignore
|
||||||
'inquiry_result',
|
self.device.EVENT_INQUIRY_RESULT,
|
||||||
lambda address, class_of_device, eir_data, rssi: inquiry_queue.put_nowait( # type: ignore
|
lambda address, class_of_device, eir_data, rssi: inquiry_queue.put_nowait( # type: ignore
|
||||||
(address, class_of_device, eir_data, rssi) # type: ignore
|
(address, class_of_device, eir_data, rssi) # type: ignore
|
||||||
),
|
),
|
||||||
@@ -639,11 +643,13 @@ class HostService(HostServicer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
self.device.remove_listener('inquiry_complete', complete_handler) # type: ignore
|
self.device.remove_listener(self.device.EVENT_INQUIRY_COMPLETE, complete_handler) # type: ignore
|
||||||
self.device.remove_listener('inquiry_result', result_handler) # type: ignore
|
self.device.remove_listener(self.device.EVENT_INQUIRY_RESULT, result_handler) # type: ignore
|
||||||
try:
|
try:
|
||||||
self.log.debug('Stop inquiry')
|
self.log.debug('Stop inquiry')
|
||||||
await self.device.abort_on('flush', self.device.stop_discovery())
|
await bumble.utils.cancel_on_event(
|
||||||
|
self.device, 'flush', self.device.stop_discovery()
|
||||||
|
)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ import logging
|
|||||||
|
|
||||||
from asyncio import Queue as AsyncQueue, Future
|
from asyncio import Queue as AsyncQueue, Future
|
||||||
|
|
||||||
from . import utils
|
from bumble.pandora import utils
|
||||||
from .config import Config
|
from bumble.pandora.config import Config
|
||||||
from bumble.core import OutOfResourcesError, InvalidArgumentError
|
from bumble.core import OutOfResourcesError, InvalidArgumentError
|
||||||
from bumble.device import Device
|
from bumble.device import Device
|
||||||
from bumble.l2cap import (
|
from bumble.l2cap import (
|
||||||
@@ -83,7 +83,7 @@ class L2CAPService(L2CAPServicer):
|
|||||||
close_future.set_result(None)
|
close_future.set_result(None)
|
||||||
|
|
||||||
l2cap_channel.sink = on_channel_sdu
|
l2cap_channel.sink = on_channel_sdu
|
||||||
l2cap_channel.on('close', on_close)
|
l2cap_channel.on(l2cap_channel.EVENT_CLOSE, on_close)
|
||||||
|
|
||||||
return ChannelContext(close_future, sdu_queue)
|
return ChannelContext(close_future, sdu_queue)
|
||||||
|
|
||||||
@@ -151,7 +151,7 @@ class L2CAPService(L2CAPServicer):
|
|||||||
spec=spec, handler=on_l2cap_channel
|
spec=spec, handler=on_l2cap_channel
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
l2cap_server.on('connection', on_l2cap_channel)
|
l2cap_server.on(l2cap_server.EVENT_CONNECTION, on_l2cap_channel)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.log.debug('Waiting for a channel connection.')
|
self.log.debug('Waiting for a channel connection.')
|
||||||
|
|||||||
+87
-72
@@ -15,20 +15,21 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import asyncio
|
import asyncio
|
||||||
import contextlib
|
import contextlib
|
||||||
|
from collections.abc import Awaitable
|
||||||
import grpc
|
import grpc
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from . import utils
|
from bumble.pandora import utils
|
||||||
from .config import Config
|
from bumble.pandora.config import Config
|
||||||
from bumble import hci
|
from bumble import hci
|
||||||
from bumble.core import (
|
from bumble.core import (
|
||||||
BT_BR_EDR_TRANSPORT,
|
PhysicalTransport,
|
||||||
BT_LE_TRANSPORT,
|
|
||||||
ProtocolError,
|
ProtocolError,
|
||||||
|
InvalidArgumentError,
|
||||||
)
|
)
|
||||||
|
import bumble.utils
|
||||||
from bumble.device import Connection as BumbleConnection, Device
|
from bumble.device import Connection as BumbleConnection, Device
|
||||||
from bumble.hci import HCI_Error, Role
|
from bumble.hci import HCI_Error, Role
|
||||||
from bumble.utils import EventWatcher
|
|
||||||
from bumble.pairing import PairingConfig, PairingDelegate as BasePairingDelegate
|
from bumble.pairing import PairingConfig, PairingDelegate as BasePairingDelegate
|
||||||
from google.protobuf import any_pb2 # pytype: disable=pyi-error
|
from google.protobuf import any_pb2 # pytype: disable=pyi-error
|
||||||
from google.protobuf import empty_pb2 # pytype: disable=pyi-error
|
from google.protobuf import empty_pb2 # pytype: disable=pyi-error
|
||||||
@@ -94,7 +95,7 @@ class PairingDelegate(BasePairingDelegate):
|
|||||||
else:
|
else:
|
||||||
# In BR/EDR, connection may not be complete,
|
# In BR/EDR, connection may not be complete,
|
||||||
# use address instead
|
# use address instead
|
||||||
assert self.connection.transport == BT_BR_EDR_TRANSPORT
|
assert self.connection.transport == PhysicalTransport.BR_EDR
|
||||||
ev.address = bytes(reversed(bytes(self.connection.peer_address)))
|
ev.address = bytes(reversed(bytes(self.connection.peer_address)))
|
||||||
|
|
||||||
return ev
|
return ev
|
||||||
@@ -173,7 +174,7 @@ class PairingDelegate(BasePairingDelegate):
|
|||||||
|
|
||||||
async def display_number(self, number: int, digits: int = 6) -> None:
|
async def display_number(self, number: int, digits: int = 6) -> None:
|
||||||
if (
|
if (
|
||||||
self.connection.transport == BT_BR_EDR_TRANSPORT
|
self.connection.transport == PhysicalTransport.BR_EDR
|
||||||
and self.io_capability == BasePairingDelegate.DISPLAY_OUTPUT_ONLY
|
and self.io_capability == BasePairingDelegate.DISPLAY_OUTPUT_ONLY
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
@@ -189,35 +190,6 @@ class PairingDelegate(BasePairingDelegate):
|
|||||||
self.service.event_queue.put_nowait(event)
|
self.service.event_queue.put_nowait(event)
|
||||||
|
|
||||||
|
|
||||||
BR_LEVEL_REACHED: Dict[SecurityLevel, Callable[[BumbleConnection], bool]] = {
|
|
||||||
LEVEL0: lambda connection: True,
|
|
||||||
LEVEL1: lambda connection: connection.encryption == 0 or connection.authenticated,
|
|
||||||
LEVEL2: lambda connection: connection.encryption != 0 and connection.authenticated,
|
|
||||||
LEVEL3: lambda connection: connection.encryption != 0
|
|
||||||
and connection.authenticated
|
|
||||||
and connection.link_key_type
|
|
||||||
in (
|
|
||||||
hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_192_TYPE,
|
|
||||||
hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE,
|
|
||||||
),
|
|
||||||
LEVEL4: lambda connection: connection.encryption
|
|
||||||
== hci.HCI_Encryption_Change_Event.AES_CCM
|
|
||||||
and connection.authenticated
|
|
||||||
and connection.link_key_type
|
|
||||||
== hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE,
|
|
||||||
}
|
|
||||||
|
|
||||||
LE_LEVEL_REACHED: Dict[LESecurityLevel, Callable[[BumbleConnection], bool]] = {
|
|
||||||
LE_LEVEL1: lambda connection: True,
|
|
||||||
LE_LEVEL2: lambda connection: connection.encryption != 0,
|
|
||||||
LE_LEVEL3: lambda connection: connection.encryption != 0
|
|
||||||
and connection.authenticated,
|
|
||||||
LE_LEVEL4: lambda connection: connection.encryption != 0
|
|
||||||
and connection.authenticated
|
|
||||||
and connection.sc,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class SecurityService(SecurityServicer):
|
class SecurityService(SecurityServicer):
|
||||||
def __init__(self, device: Device, config: Config) -> None:
|
def __init__(self, device: Device, config: Config) -> None:
|
||||||
self.log = utils.BumbleServerLoggerAdapter(
|
self.log = utils.BumbleServerLoggerAdapter(
|
||||||
@@ -249,6 +221,59 @@ class SecurityService(SecurityServicer):
|
|||||||
|
|
||||||
self.device.pairing_config_factory = pairing_config_factory
|
self.device.pairing_config_factory = pairing_config_factory
|
||||||
|
|
||||||
|
async def _classic_level_reached(
|
||||||
|
self, level: SecurityLevel, connection: BumbleConnection
|
||||||
|
) -> bool:
|
||||||
|
if level == LEVEL0:
|
||||||
|
return True
|
||||||
|
if level == LEVEL1:
|
||||||
|
return connection.encryption == 0 or connection.authenticated
|
||||||
|
if level == LEVEL2:
|
||||||
|
return connection.encryption != 0 and connection.authenticated
|
||||||
|
|
||||||
|
link_key_type: Optional[int] = None
|
||||||
|
if (keystore := connection.device.keystore) and (
|
||||||
|
keys := await keystore.get(str(connection.peer_address))
|
||||||
|
):
|
||||||
|
link_key_type = keys.link_key_type
|
||||||
|
self.log.debug("link_key_type: %d", link_key_type)
|
||||||
|
|
||||||
|
if level == LEVEL3:
|
||||||
|
return (
|
||||||
|
connection.encryption != 0
|
||||||
|
and connection.authenticated
|
||||||
|
and link_key_type
|
||||||
|
in (
|
||||||
|
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.AES_CCM
|
||||||
|
and connection.authenticated
|
||||||
|
and link_key_type
|
||||||
|
== hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE
|
||||||
|
)
|
||||||
|
raise InvalidArgumentError(f"Unexpected level {level}")
|
||||||
|
|
||||||
|
def _le_level_reached(
|
||||||
|
self, level: LESecurityLevel, connection: BumbleConnection
|
||||||
|
) -> bool:
|
||||||
|
if level == LE_LEVEL1:
|
||||||
|
return True
|
||||||
|
if level == LE_LEVEL2:
|
||||||
|
return connection.encryption != 0
|
||||||
|
if level == LE_LEVEL3:
|
||||||
|
return connection.encryption != 0 and connection.authenticated
|
||||||
|
if level == LE_LEVEL4:
|
||||||
|
return (
|
||||||
|
connection.encryption != 0
|
||||||
|
and connection.authenticated
|
||||||
|
and connection.sc
|
||||||
|
)
|
||||||
|
raise InvalidArgumentError(f"Unexpected level {level}")
|
||||||
|
|
||||||
@utils.rpc
|
@utils.rpc
|
||||||
async def OnPairing(
|
async def OnPairing(
|
||||||
self, request: AsyncIterator[PairingEventAnswer], context: grpc.ServicerContext
|
self, request: AsyncIterator[PairingEventAnswer], context: grpc.ServicerContext
|
||||||
@@ -286,12 +311,12 @@ class SecurityService(SecurityServicer):
|
|||||||
|
|
||||||
oneof = request.WhichOneof('level')
|
oneof = request.WhichOneof('level')
|
||||||
level = getattr(request, oneof)
|
level = getattr(request, oneof)
|
||||||
assert {BT_BR_EDR_TRANSPORT: 'classic', BT_LE_TRANSPORT: 'le'}[
|
assert {PhysicalTransport.BR_EDR: 'classic', PhysicalTransport.LE: 'le'}[
|
||||||
connection.transport
|
connection.transport
|
||||||
] == oneof
|
] == oneof
|
||||||
|
|
||||||
# security level already reached
|
# security level already reached
|
||||||
if self.reached_security_level(connection, level):
|
if await self.reached_security_level(connection, level):
|
||||||
return SecureResponse(success=empty_pb2.Empty())
|
return SecureResponse(success=empty_pb2.Empty())
|
||||||
|
|
||||||
# trigger pairing if needed
|
# trigger pairing if needed
|
||||||
@@ -301,22 +326,22 @@ class SecurityService(SecurityServicer):
|
|||||||
|
|
||||||
security_result = asyncio.get_running_loop().create_future()
|
security_result = asyncio.get_running_loop().create_future()
|
||||||
|
|
||||||
with contextlib.closing(EventWatcher()) as watcher:
|
with contextlib.closing(bumble.utils.EventWatcher()) as watcher:
|
||||||
|
|
||||||
@watcher.on(connection, 'pairing')
|
@watcher.on(connection, connection.EVENT_PAIRING)
|
||||||
def on_pairing(*_: Any) -> None:
|
def on_pairing(*_: Any) -> None:
|
||||||
security_result.set_result('success')
|
security_result.set_result('success')
|
||||||
|
|
||||||
@watcher.on(connection, 'pairing_failure')
|
@watcher.on(connection, connection.EVENT_PAIRING_FAILURE)
|
||||||
def on_pairing_failure(*_: Any) -> None:
|
def on_pairing_failure(*_: Any) -> None:
|
||||||
security_result.set_result('pairing_failure')
|
security_result.set_result('pairing_failure')
|
||||||
|
|
||||||
@watcher.on(connection, 'disconnection')
|
@watcher.on(connection, connection.EVENT_DISCONNECTION)
|
||||||
def on_disconnection(*_: Any) -> None:
|
def on_disconnection(*_: Any) -> None:
|
||||||
security_result.set_result('connection_died')
|
security_result.set_result('connection_died')
|
||||||
|
|
||||||
if (
|
if (
|
||||||
connection.transport == BT_LE_TRANSPORT
|
connection.transport == PhysicalTransport.LE
|
||||||
and connection.role == Role.PERIPHERAL
|
and connection.role == Role.PERIPHERAL
|
||||||
):
|
):
|
||||||
connection.request_pairing()
|
connection.request_pairing()
|
||||||
@@ -362,7 +387,7 @@ class SecurityService(SecurityServicer):
|
|||||||
return SecureResponse(encryption_failure=empty_pb2.Empty())
|
return SecureResponse(encryption_failure=empty_pb2.Empty())
|
||||||
|
|
||||||
# security level has been reached ?
|
# security level has been reached ?
|
||||||
if self.reached_security_level(connection, level):
|
if await self.reached_security_level(connection, level):
|
||||||
return SecureResponse(success=empty_pb2.Empty())
|
return SecureResponse(success=empty_pb2.Empty())
|
||||||
return SecureResponse(not_reached=empty_pb2.Empty())
|
return SecureResponse(not_reached=empty_pb2.Empty())
|
||||||
|
|
||||||
@@ -378,7 +403,7 @@ class SecurityService(SecurityServicer):
|
|||||||
|
|
||||||
assert request.level
|
assert request.level
|
||||||
level = request.level
|
level = request.level
|
||||||
assert {BT_BR_EDR_TRANSPORT: 'classic', BT_LE_TRANSPORT: 'le'}[
|
assert {PhysicalTransport.BR_EDR: 'classic', PhysicalTransport.LE: 'le'}[
|
||||||
connection.transport
|
connection.transport
|
||||||
] == request.level_variant()
|
] == request.level_variant()
|
||||||
|
|
||||||
@@ -389,13 +414,10 @@ class SecurityService(SecurityServicer):
|
|||||||
pair_task: Optional[asyncio.Future[None]] = None
|
pair_task: Optional[asyncio.Future[None]] = None
|
||||||
|
|
||||||
async def authenticate() -> None:
|
async def authenticate() -> None:
|
||||||
assert connection
|
|
||||||
if (encryption := connection.encryption) != 0:
|
if (encryption := connection.encryption) != 0:
|
||||||
self.log.debug('Disable encryption...')
|
self.log.debug('Disable encryption...')
|
||||||
try:
|
with contextlib.suppress(Exception):
|
||||||
await connection.encrypt(enable=False)
|
await connection.encrypt(enable=False)
|
||||||
except:
|
|
||||||
pass
|
|
||||||
self.log.debug('Disable encryption: done')
|
self.log.debug('Disable encryption: done')
|
||||||
|
|
||||||
self.log.debug('Authenticate...')
|
self.log.debug('Authenticate...')
|
||||||
@@ -414,19 +436,17 @@ class SecurityService(SecurityServicer):
|
|||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
def try_set_success(*_: Any) -> None:
|
async def try_set_success(*_: Any) -> None:
|
||||||
assert connection
|
if await self.reached_security_level(connection, level):
|
||||||
if self.reached_security_level(connection, level):
|
|
||||||
self.log.debug('Wait for security: done')
|
self.log.debug('Wait for security: done')
|
||||||
wait_for_security.set_result('success')
|
wait_for_security.set_result('success')
|
||||||
|
|
||||||
def on_encryption_change(*_: Any) -> None:
|
async def on_encryption_change(*_: Any) -> None:
|
||||||
assert connection
|
if await self.reached_security_level(connection, level):
|
||||||
if self.reached_security_level(connection, level):
|
|
||||||
self.log.debug('Wait for security: done')
|
self.log.debug('Wait for security: done')
|
||||||
wait_for_security.set_result('success')
|
wait_for_security.set_result('success')
|
||||||
elif (
|
elif (
|
||||||
connection.transport == BT_BR_EDR_TRANSPORT
|
connection.transport == PhysicalTransport.BR_EDR
|
||||||
and self.need_authentication(connection, level)
|
and self.need_authentication(connection, level)
|
||||||
):
|
):
|
||||||
nonlocal authenticate_task
|
nonlocal authenticate_task
|
||||||
@@ -437,7 +457,7 @@ class SecurityService(SecurityServicer):
|
|||||||
if self.need_pairing(connection, level):
|
if self.need_pairing(connection, level):
|
||||||
pair_task = asyncio.create_task(connection.pair())
|
pair_task = asyncio.create_task(connection.pair())
|
||||||
|
|
||||||
listeners: Dict[str, Callable[..., None]] = {
|
listeners: Dict[str, Callable[..., Union[None, Awaitable[None]]]] = {
|
||||||
'disconnection': set_failure('connection_died'),
|
'disconnection': set_failure('connection_died'),
|
||||||
'pairing_failure': set_failure('pairing_failure'),
|
'pairing_failure': set_failure('pairing_failure'),
|
||||||
'connection_authentication_failure': set_failure('authentication_failure'),
|
'connection_authentication_failure': set_failure('authentication_failure'),
|
||||||
@@ -450,13 +470,13 @@ class SecurityService(SecurityServicer):
|
|||||||
'security_request': pair,
|
'security_request': pair,
|
||||||
}
|
}
|
||||||
|
|
||||||
with contextlib.closing(EventWatcher()) as watcher:
|
with contextlib.closing(bumble.utils.EventWatcher()) as watcher:
|
||||||
# register event handlers
|
# register event handlers
|
||||||
for event, listener in listeners.items():
|
for event, listener in listeners.items():
|
||||||
watcher.on(connection, event, listener)
|
watcher.on(connection, event, listener)
|
||||||
|
|
||||||
# security level already reached
|
# security level already reached
|
||||||
if self.reached_security_level(connection, level):
|
if await self.reached_security_level(connection, level):
|
||||||
return WaitSecurityResponse(success=empty_pb2.Empty())
|
return WaitSecurityResponse(success=empty_pb2.Empty())
|
||||||
|
|
||||||
self.log.debug('Wait for security...')
|
self.log.debug('Wait for security...')
|
||||||
@@ -466,24 +486,20 @@ class SecurityService(SecurityServicer):
|
|||||||
# wait for `authenticate` to finish if any
|
# wait for `authenticate` to finish if any
|
||||||
if authenticate_task is not None:
|
if authenticate_task is not None:
|
||||||
self.log.debug('Wait for authentication...')
|
self.log.debug('Wait for authentication...')
|
||||||
try:
|
with contextlib.suppress(Exception):
|
||||||
await authenticate_task # type: ignore
|
await authenticate_task # type: ignore
|
||||||
except:
|
|
||||||
pass
|
|
||||||
self.log.debug('Authenticated')
|
self.log.debug('Authenticated')
|
||||||
|
|
||||||
# wait for `pair` to finish if any
|
# wait for `pair` to finish if any
|
||||||
if pair_task is not None:
|
if pair_task is not None:
|
||||||
self.log.debug('Wait for authentication...')
|
self.log.debug('Wait for authentication...')
|
||||||
try:
|
with contextlib.suppress(Exception):
|
||||||
await pair_task # type: ignore
|
await pair_task # type: ignore
|
||||||
except:
|
|
||||||
pass
|
|
||||||
self.log.debug('paired')
|
self.log.debug('paired')
|
||||||
|
|
||||||
return WaitSecurityResponse(**kwargs)
|
return WaitSecurityResponse(**kwargs)
|
||||||
|
|
||||||
def reached_security_level(
|
async def reached_security_level(
|
||||||
self, connection: BumbleConnection, level: Union[SecurityLevel, LESecurityLevel]
|
self, connection: BumbleConnection, level: Union[SecurityLevel, LESecurityLevel]
|
||||||
) -> bool:
|
) -> bool:
|
||||||
self.log.debug(
|
self.log.debug(
|
||||||
@@ -493,23 +509,22 @@ class SecurityService(SecurityServicer):
|
|||||||
'encryption': connection.encryption,
|
'encryption': connection.encryption,
|
||||||
'authenticated': connection.authenticated,
|
'authenticated': connection.authenticated,
|
||||||
'sc': connection.sc,
|
'sc': connection.sc,
|
||||||
'link_key_type': connection.link_key_type,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if isinstance(level, LESecurityLevel):
|
if isinstance(level, LESecurityLevel):
|
||||||
return LE_LEVEL_REACHED[level](connection)
|
return self._le_level_reached(level, connection)
|
||||||
|
|
||||||
return BR_LEVEL_REACHED[level](connection)
|
return await self._classic_level_reached(level, connection)
|
||||||
|
|
||||||
def need_pairing(self, connection: BumbleConnection, level: int) -> bool:
|
def need_pairing(self, connection: BumbleConnection, level: int) -> bool:
|
||||||
if connection.transport == BT_LE_TRANSPORT:
|
if connection.transport == PhysicalTransport.LE:
|
||||||
return level >= LE_LEVEL3 and not connection.authenticated
|
return level >= LE_LEVEL3 and not connection.authenticated
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def need_authentication(self, connection: BumbleConnection, level: int) -> bool:
|
def need_authentication(self, connection: BumbleConnection, level: int) -> bool:
|
||||||
if connection.transport == BT_LE_TRANSPORT:
|
if connection.transport == PhysicalTransport.LE:
|
||||||
return False
|
return False
|
||||||
if level == LEVEL2 and connection.encryption != 0:
|
if level == LEVEL2 and connection.encryption != 0:
|
||||||
return not connection.authenticated
|
return not connection.authenticated
|
||||||
@@ -517,7 +532,7 @@ class SecurityService(SecurityServicer):
|
|||||||
|
|
||||||
def need_encryption(self, connection: BumbleConnection, level: int) -> bool:
|
def need_encryption(self, connection: BumbleConnection, level: int) -> bool:
|
||||||
# TODO(abel): need to support MITM
|
# TODO(abel): need to support MITM
|
||||||
if connection.transport == BT_LE_TRANSPORT:
|
if connection.transport == PhysicalTransport.LE:
|
||||||
return level == LE_LEVEL2 and not connection.encryption
|
return level == LE_LEVEL2 and not connection.encryption
|
||||||
return level >= LEVEL2 and not connection.encryption
|
return level >= LEVEL2 and not connection.encryption
|
||||||
|
|
||||||
|
|||||||
+9
-11
@@ -48,7 +48,7 @@ from bumble.gatt_adapters import (
|
|||||||
UTF8CharacteristicProxyAdapter,
|
UTF8CharacteristicProxyAdapter,
|
||||||
)
|
)
|
||||||
from bumble.gatt_client import ProfileServiceProxy, ServiceProxy
|
from bumble.gatt_client import ProfileServiceProxy, ServiceProxy
|
||||||
from bumble.utils import OpenIntEnum
|
from bumble import utils
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Logging
|
# Logging
|
||||||
@@ -64,7 +64,7 @@ GAIN_SETTINGS_MIN_VALUE = 0
|
|||||||
GAIN_SETTINGS_MAX_VALUE = 255
|
GAIN_SETTINGS_MAX_VALUE = 255
|
||||||
|
|
||||||
|
|
||||||
class ErrorCode(OpenIntEnum):
|
class ErrorCode(utils.OpenIntEnum):
|
||||||
'''
|
'''
|
||||||
Cf. 1.6 Application error codes
|
Cf. 1.6 Application error codes
|
||||||
'''
|
'''
|
||||||
@@ -76,7 +76,7 @@ class ErrorCode(OpenIntEnum):
|
|||||||
GAIN_MODE_CHANGE_NOT_ALLOWED = 0x84
|
GAIN_MODE_CHANGE_NOT_ALLOWED = 0x84
|
||||||
|
|
||||||
|
|
||||||
class Mute(OpenIntEnum):
|
class Mute(utils.OpenIntEnum):
|
||||||
'''
|
'''
|
||||||
Cf. 2.2.1.2 Mute Field
|
Cf. 2.2.1.2 Mute Field
|
||||||
'''
|
'''
|
||||||
@@ -86,7 +86,7 @@ class Mute(OpenIntEnum):
|
|||||||
DISABLED = 0x02
|
DISABLED = 0x02
|
||||||
|
|
||||||
|
|
||||||
class GainMode(OpenIntEnum):
|
class GainMode(utils.OpenIntEnum):
|
||||||
'''
|
'''
|
||||||
Cf. 2.2.1.3 Gain Mode
|
Cf. 2.2.1.3 Gain Mode
|
||||||
'''
|
'''
|
||||||
@@ -97,7 +97,7 @@ class GainMode(OpenIntEnum):
|
|||||||
AUTOMATIC = 0x03
|
AUTOMATIC = 0x03
|
||||||
|
|
||||||
|
|
||||||
class AudioInputStatus(OpenIntEnum):
|
class AudioInputStatus(utils.OpenIntEnum):
|
||||||
'''
|
'''
|
||||||
Cf. 3.4 Audio Input Status
|
Cf. 3.4 Audio Input Status
|
||||||
'''
|
'''
|
||||||
@@ -106,7 +106,7 @@ class AudioInputStatus(OpenIntEnum):
|
|||||||
ACTIVE = 0x01
|
ACTIVE = 0x01
|
||||||
|
|
||||||
|
|
||||||
class AudioInputControlPointOpCode(OpenIntEnum):
|
class AudioInputControlPointOpCode(utils.OpenIntEnum):
|
||||||
'''
|
'''
|
||||||
Cf. 3.5.1 Audio Input Control Point procedure requirements
|
Cf. 3.5.1 Audio Input Control Point procedure requirements
|
||||||
'''
|
'''
|
||||||
@@ -198,8 +198,7 @@ class AudioInputControlPoint:
|
|||||||
audio_input_state: AudioInputState
|
audio_input_state: AudioInputState
|
||||||
gain_settings_properties: GainSettingsProperties
|
gain_settings_properties: GainSettingsProperties
|
||||||
|
|
||||||
async def on_write(self, connection: Optional[Connection], value: bytes) -> None:
|
async def on_write(self, connection: Connection, value: bytes) -> None:
|
||||||
assert connection
|
|
||||||
|
|
||||||
opcode = AudioInputControlPointOpCode(value[0])
|
opcode = AudioInputControlPointOpCode(value[0])
|
||||||
|
|
||||||
@@ -320,11 +319,10 @@ class AudioInputDescription:
|
|||||||
audio_input_description: str = "Bluetooth"
|
audio_input_description: str = "Bluetooth"
|
||||||
attribute: Optional[Attribute] = None
|
attribute: Optional[Attribute] = None
|
||||||
|
|
||||||
def on_read(self, _connection: Optional[Connection]) -> str:
|
def on_read(self, _connection: Connection) -> str:
|
||||||
return self.audio_input_description
|
return self.audio_input_description
|
||||||
|
|
||||||
async def on_write(self, connection: Optional[Connection], value: str) -> None:
|
async def on_write(self, connection: Connection, value: str) -> None:
|
||||||
assert connection
|
|
||||||
assert self.attribute
|
assert self.attribute
|
||||||
|
|
||||||
self.audio_input_description = value
|
self.audio_input_description = value
|
||||||
|
|||||||
+12
-11
@@ -28,7 +28,6 @@ import logging
|
|||||||
import struct
|
import struct
|
||||||
from typing import Optional, Sequence, Union
|
from typing import Optional, Sequence, Union
|
||||||
|
|
||||||
from pyee import EventEmitter
|
|
||||||
|
|
||||||
from bumble.att import ATT_Error
|
from bumble.att import ATT_Error
|
||||||
from bumble.device import Peer
|
from bumble.device import Peer
|
||||||
@@ -42,7 +41,7 @@ from bumble.gatt import (
|
|||||||
)
|
)
|
||||||
from bumble.gatt_client import CharacteristicProxy, ProfileServiceProxy, ServiceProxy
|
from bumble.gatt_client import CharacteristicProxy, ProfileServiceProxy, ServiceProxy
|
||||||
from bumble.gatt_adapters import SerializableCharacteristicProxyAdapter
|
from bumble.gatt_adapters import SerializableCharacteristicProxyAdapter
|
||||||
from bumble.utils import OpenIntEnum
|
from bumble import utils
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -60,16 +59,16 @@ logger = logging.getLogger(__name__)
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Protocol
|
# Protocol
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class ActionId(OpenIntEnum):
|
class ActionId(utils.OpenIntEnum):
|
||||||
POSITIVE = 0
|
POSITIVE = 0
|
||||||
NEGATIVE = 1
|
NEGATIVE = 1
|
||||||
|
|
||||||
|
|
||||||
class AppAttributeId(OpenIntEnum):
|
class AppAttributeId(utils.OpenIntEnum):
|
||||||
DISPLAY_NAME = 0
|
DISPLAY_NAME = 0
|
||||||
|
|
||||||
|
|
||||||
class CategoryId(OpenIntEnum):
|
class CategoryId(utils.OpenIntEnum):
|
||||||
OTHER = 0
|
OTHER = 0
|
||||||
INCOMING_CALL = 1
|
INCOMING_CALL = 1
|
||||||
MISSED_CALL = 2
|
MISSED_CALL = 2
|
||||||
@@ -84,13 +83,13 @@ class CategoryId(OpenIntEnum):
|
|||||||
ENTERTAINMENT = 11
|
ENTERTAINMENT = 11
|
||||||
|
|
||||||
|
|
||||||
class CommandId(OpenIntEnum):
|
class CommandId(utils.OpenIntEnum):
|
||||||
GET_NOTIFICATION_ATTRIBUTES = 0
|
GET_NOTIFICATION_ATTRIBUTES = 0
|
||||||
GET_APP_ATTRIBUTES = 1
|
GET_APP_ATTRIBUTES = 1
|
||||||
PERFORM_NOTIFICATION_ACTION = 2
|
PERFORM_NOTIFICATION_ACTION = 2
|
||||||
|
|
||||||
|
|
||||||
class EventId(OpenIntEnum):
|
class EventId(utils.OpenIntEnum):
|
||||||
NOTIFICATION_ADDED = 0
|
NOTIFICATION_ADDED = 0
|
||||||
NOTIFICATION_MODIFIED = 1
|
NOTIFICATION_MODIFIED = 1
|
||||||
NOTIFICATION_REMOVED = 2
|
NOTIFICATION_REMOVED = 2
|
||||||
@@ -104,7 +103,7 @@ class EventFlags(enum.IntFlag):
|
|||||||
NEGATIVE_ACTION = 1 << 4
|
NEGATIVE_ACTION = 1 << 4
|
||||||
|
|
||||||
|
|
||||||
class NotificationAttributeId(OpenIntEnum):
|
class NotificationAttributeId(utils.OpenIntEnum):
|
||||||
APP_IDENTIFIER = 0
|
APP_IDENTIFIER = 0
|
||||||
TITLE = 1
|
TITLE = 1
|
||||||
SUBTITLE = 2
|
SUBTITLE = 2
|
||||||
@@ -156,7 +155,7 @@ class Notification:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ErrorCode(OpenIntEnum):
|
class ErrorCode(utils.OpenIntEnum):
|
||||||
UNKNOWN_COMMAND = 0xA0
|
UNKNOWN_COMMAND = 0xA0
|
||||||
INVALID_COMMAND = 0xA1
|
INVALID_COMMAND = 0xA1
|
||||||
INVALID_PARAMETER = 0xA2
|
INVALID_PARAMETER = 0xA2
|
||||||
@@ -243,7 +242,7 @@ class AncsProxy(ProfileServiceProxy):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class AncsClient(EventEmitter):
|
class AncsClient(utils.EventEmitter):
|
||||||
_expected_response_command_id: Optional[CommandId]
|
_expected_response_command_id: Optional[CommandId]
|
||||||
_expected_response_notification_uid: Optional[int]
|
_expected_response_notification_uid: Optional[int]
|
||||||
_expected_response_app_identifier: Optional[str]
|
_expected_response_app_identifier: Optional[str]
|
||||||
@@ -251,6 +250,8 @@ class AncsClient(EventEmitter):
|
|||||||
_expected_response_tuples: int
|
_expected_response_tuples: int
|
||||||
_response_accumulator: bytes
|
_response_accumulator: bytes
|
||||||
|
|
||||||
|
EVENT_NOTIFICATION = "notification"
|
||||||
|
|
||||||
def __init__(self, ancs_proxy: AncsProxy) -> None:
|
def __init__(self, ancs_proxy: AncsProxy) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._ancs_proxy = ancs_proxy
|
self._ancs_proxy = ancs_proxy
|
||||||
@@ -285,7 +286,7 @@ class AncsClient(EventEmitter):
|
|||||||
|
|
||||||
def _on_notification(self, notification: Notification) -> None:
|
def _on_notification(self, notification: Notification) -> None:
|
||||||
logger.debug(f"ANCS NOTIFICATION: {notification}")
|
logger.debug(f"ANCS NOTIFICATION: {notification}")
|
||||||
self.emit("notification", notification)
|
self.emit(self.EVENT_NOTIFICATION, notification)
|
||||||
|
|
||||||
def _on_data(self, data: bytes) -> None:
|
def _on_data(self, data: bytes) -> None:
|
||||||
logger.debug(f"ANCS DATA: {data.hex()}")
|
logger.debug(f"ANCS DATA: {data.hex()}")
|
||||||
|
|||||||
+28
-15
@@ -23,6 +23,7 @@ import logging
|
|||||||
import struct
|
import struct
|
||||||
from typing import Any, Dict, List, Optional, Sequence, Tuple, Type, Union
|
from typing import Any, Dict, List, Optional, Sequence, Tuple, Type, Union
|
||||||
|
|
||||||
|
from bumble import utils
|
||||||
from bumble import colors
|
from bumble import colors
|
||||||
from bumble.profiles.bap import CodecSpecificConfiguration
|
from bumble.profiles.bap import CodecSpecificConfiguration
|
||||||
from bumble.profiles import le_audio
|
from bumble.profiles import le_audio
|
||||||
@@ -275,6 +276,8 @@ class AseStateMachine(gatt.Characteristic):
|
|||||||
DISABLING = 0x05
|
DISABLING = 0x05
|
||||||
RELEASING = 0x06
|
RELEASING = 0x06
|
||||||
|
|
||||||
|
EVENT_STATE_CHANGE = "state_change"
|
||||||
|
|
||||||
cis_link: Optional[device.CisLink] = None
|
cis_link: Optional[device.CisLink] = None
|
||||||
|
|
||||||
# Additional parameters in CODEC_CONFIGURED State
|
# Additional parameters in CODEC_CONFIGURED State
|
||||||
@@ -328,8 +331,12 @@ class AseStateMachine(gatt.Characteristic):
|
|||||||
value=gatt.CharacteristicValue(read=self.on_read),
|
value=gatt.CharacteristicValue(read=self.on_read),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.service.device.on('cis_request', self.on_cis_request)
|
self.service.device.on(
|
||||||
self.service.device.on('cis_establishment', self.on_cis_establishment)
|
self.service.device.EVENT_CIS_REQUEST, self.on_cis_request
|
||||||
|
)
|
||||||
|
self.service.device.on(
|
||||||
|
self.service.device.EVENT_CIS_ESTABLISHMENT, self.on_cis_establishment
|
||||||
|
)
|
||||||
|
|
||||||
def on_cis_request(
|
def on_cis_request(
|
||||||
self,
|
self,
|
||||||
@@ -343,8 +350,10 @@ class AseStateMachine(gatt.Characteristic):
|
|||||||
and cis_id == self.cis_id
|
and cis_id == self.cis_id
|
||||||
and self.state == self.State.ENABLING
|
and self.state == self.State.ENABLING
|
||||||
):
|
):
|
||||||
acl_connection.abort_on(
|
utils.cancel_on_event(
|
||||||
'flush', self.service.device.accept_cis_request(cis_handle)
|
acl_connection,
|
||||||
|
'flush',
|
||||||
|
self.service.device.accept_cis_request(cis_handle),
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_cis_establishment(self, cis_link: device.CisLink) -> None:
|
def on_cis_establishment(self, cis_link: device.CisLink) -> None:
|
||||||
@@ -353,7 +362,7 @@ class AseStateMachine(gatt.Characteristic):
|
|||||||
and cis_link.cis_id == self.cis_id
|
and cis_link.cis_id == self.cis_id
|
||||||
and self.state == self.State.ENABLING
|
and self.state == self.State.ENABLING
|
||||||
):
|
):
|
||||||
cis_link.on('disconnection', self.on_cis_disconnection)
|
cis_link.on(cis_link.EVENT_DISCONNECTION, self.on_cis_disconnection)
|
||||||
|
|
||||||
async def post_cis_established():
|
async def post_cis_established():
|
||||||
await cis_link.setup_data_path(direction=self.role)
|
await cis_link.setup_data_path(direction=self.role)
|
||||||
@@ -361,7 +370,9 @@ class AseStateMachine(gatt.Characteristic):
|
|||||||
self.state = self.State.STREAMING
|
self.state = self.State.STREAMING
|
||||||
await self.service.device.notify_subscribers(self, self.value)
|
await self.service.device.notify_subscribers(self, self.value)
|
||||||
|
|
||||||
cis_link.acl_connection.abort_on('flush', post_cis_established())
|
utils.cancel_on_event(
|
||||||
|
cis_link.acl_connection, 'flush', post_cis_established()
|
||||||
|
)
|
||||||
self.cis_link = cis_link
|
self.cis_link = cis_link
|
||||||
|
|
||||||
def on_cis_disconnection(self, _reason) -> None:
|
def on_cis_disconnection(self, _reason) -> None:
|
||||||
@@ -509,7 +520,7 @@ class AseStateMachine(gatt.Characteristic):
|
|||||||
self.state = self.State.IDLE
|
self.state = self.State.IDLE
|
||||||
await self.service.device.notify_subscribers(self, self.value)
|
await self.service.device.notify_subscribers(self, self.value)
|
||||||
|
|
||||||
self.service.device.abort_on('flush', remove_cis_async())
|
utils.cancel_on_event(self.service.device, 'flush', remove_cis_async())
|
||||||
return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
|
return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -520,7 +531,7 @@ class AseStateMachine(gatt.Characteristic):
|
|||||||
def state(self, new_state: State) -> None:
|
def state(self, new_state: State) -> None:
|
||||||
logger.debug(f'{self} state change -> {colors.color(new_state.name, "cyan")}')
|
logger.debug(f'{self} state change -> {colors.color(new_state.name, "cyan")}')
|
||||||
self._state = new_state
|
self._state = new_state
|
||||||
self.emit('state_change')
|
self.emit(self.EVENT_STATE_CHANGE)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def value(self):
|
def value(self):
|
||||||
@@ -579,7 +590,7 @@ class AseStateMachine(gatt.Characteristic):
|
|||||||
# Readonly. Do nothing in the setter.
|
# Readonly. Do nothing in the setter.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def on_read(self, _: Optional[device.Connection]) -> bytes:
|
def on_read(self, _: device.Connection) -> bytes:
|
||||||
return self.value
|
return self.value
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
@@ -594,7 +605,7 @@ class AudioStreamControlService(gatt.TemplateService):
|
|||||||
UUID = gatt.GATT_AUDIO_STREAM_CONTROL_SERVICE
|
UUID = gatt.GATT_AUDIO_STREAM_CONTROL_SERVICE
|
||||||
|
|
||||||
ase_state_machines: Dict[int, AseStateMachine]
|
ase_state_machines: Dict[int, AseStateMachine]
|
||||||
ase_control_point: gatt.Characteristic
|
ase_control_point: gatt.Characteristic[bytes]
|
||||||
_active_client: Optional[device.Connection] = None
|
_active_client: Optional[device.Connection] = None
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -691,7 +702,8 @@ class AudioStreamControlService(gatt.TemplateService):
|
|||||||
control_point_notification = bytes(
|
control_point_notification = bytes(
|
||||||
[operation.op_code, len(responses)]
|
[operation.op_code, len(responses)]
|
||||||
) + b''.join(map(bytes, responses))
|
) + b''.join(map(bytes, responses))
|
||||||
self.device.abort_on(
|
utils.cancel_on_event(
|
||||||
|
self.device,
|
||||||
'flush',
|
'flush',
|
||||||
self.device.notify_subscribers(
|
self.device.notify_subscribers(
|
||||||
self.ase_control_point, control_point_notification
|
self.ase_control_point, control_point_notification
|
||||||
@@ -700,7 +712,8 @@ class AudioStreamControlService(gatt.TemplateService):
|
|||||||
|
|
||||||
for ase_id, *_ in responses:
|
for ase_id, *_ in responses:
|
||||||
if ase := self.ase_state_machines.get(ase_id):
|
if ase := self.ase_state_machines.get(ase_id):
|
||||||
self.device.abort_on(
|
utils.cancel_on_event(
|
||||||
|
self.device,
|
||||||
'flush',
|
'flush',
|
||||||
self.device.notify_subscribers(ase, ase.value),
|
self.device.notify_subscribers(ase, ase.value),
|
||||||
)
|
)
|
||||||
@@ -710,9 +723,9 @@ class AudioStreamControlService(gatt.TemplateService):
|
|||||||
class AudioStreamControlServiceProxy(gatt_client.ProfileServiceProxy):
|
class AudioStreamControlServiceProxy(gatt_client.ProfileServiceProxy):
|
||||||
SERVICE_CLASS = AudioStreamControlService
|
SERVICE_CLASS = AudioStreamControlService
|
||||||
|
|
||||||
sink_ase: List[gatt_client.CharacteristicProxy]
|
sink_ase: List[gatt_client.CharacteristicProxy[bytes]]
|
||||||
source_ase: List[gatt_client.CharacteristicProxy]
|
source_ase: List[gatt_client.CharacteristicProxy[bytes]]
|
||||||
ase_control_point: gatt_client.CharacteristicProxy
|
ase_control_point: gatt_client.CharacteristicProxy[bytes]
|
||||||
|
|
||||||
def __init__(self, service_proxy: gatt_client.ServiceProxy):
|
def __init__(self, service_proxy: gatt_client.ServiceProxy):
|
||||||
self.service_proxy = service_proxy
|
self.service_proxy = service_proxy
|
||||||
|
|||||||
+16
-11
@@ -88,6 +88,11 @@ class AudioStatus(utils.OpenIntEnum):
|
|||||||
class AshaService(gatt.TemplateService):
|
class AshaService(gatt.TemplateService):
|
||||||
UUID = gatt.GATT_ASHA_SERVICE
|
UUID = gatt.GATT_ASHA_SERVICE
|
||||||
|
|
||||||
|
EVENT_STARTED = "started"
|
||||||
|
EVENT_STOPPED = "stopped"
|
||||||
|
EVENT_DISCONNECTED = "disconnected"
|
||||||
|
EVENT_VOLUME_CHANGED = "volume_changed"
|
||||||
|
|
||||||
audio_sink: Optional[Callable[[bytes], Any]]
|
audio_sink: Optional[Callable[[bytes], Any]]
|
||||||
active_codec: Optional[Codec] = None
|
active_codec: Optional[Codec] = None
|
||||||
audio_type: Optional[AudioType] = None
|
audio_type: Optional[AudioType] = None
|
||||||
@@ -195,7 +200,7 @@ class AshaService(gatt.TemplateService):
|
|||||||
|
|
||||||
# Handler for audio control commands
|
# Handler for audio control commands
|
||||||
async def _on_audio_control_point_write(
|
async def _on_audio_control_point_write(
|
||||||
self, connection: Optional[Connection], value: bytes
|
self, connection: Connection, value: bytes
|
||||||
) -> None:
|
) -> None:
|
||||||
_logger.debug(f'--- AUDIO CONTROL POINT Write:{value.hex()}')
|
_logger.debug(f'--- AUDIO CONTROL POINT Write:{value.hex()}')
|
||||||
opcode = value[0]
|
opcode = value[0]
|
||||||
@@ -211,14 +216,14 @@ class AshaService(gatt.TemplateService):
|
|||||||
f'volume={self.volume}, '
|
f'volume={self.volume}, '
|
||||||
f'other_state={self.other_state}'
|
f'other_state={self.other_state}'
|
||||||
)
|
)
|
||||||
self.emit('started')
|
self.emit(self.EVENT_STARTED)
|
||||||
elif opcode == OpCode.STOP:
|
elif opcode == OpCode.STOP:
|
||||||
_logger.debug('### STOP')
|
_logger.debug('### STOP')
|
||||||
self.active_codec = None
|
self.active_codec = None
|
||||||
self.audio_type = None
|
self.audio_type = None
|
||||||
self.volume = None
|
self.volume = None
|
||||||
self.other_state = None
|
self.other_state = None
|
||||||
self.emit('stopped')
|
self.emit(self.EVENT_STOPPED)
|
||||||
elif opcode == OpCode.STATUS:
|
elif opcode == OpCode.STATUS:
|
||||||
_logger.debug('### STATUS: %s', PeripheralStatus(value[1]).name)
|
_logger.debug('### STATUS: %s', PeripheralStatus(value[1]).name)
|
||||||
|
|
||||||
@@ -231,7 +236,7 @@ class AshaService(gatt.TemplateService):
|
|||||||
self.audio_type = None
|
self.audio_type = None
|
||||||
self.volume = None
|
self.volume = None
|
||||||
self.other_state = None
|
self.other_state = None
|
||||||
self.emit('disconnected')
|
self.emit(self.EVENT_DISCONNECTED)
|
||||||
|
|
||||||
connection.once('disconnection', on_disconnection)
|
connection.once('disconnection', on_disconnection)
|
||||||
|
|
||||||
@@ -242,10 +247,10 @@ class AshaService(gatt.TemplateService):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Handler for volume control
|
# Handler for volume control
|
||||||
def _on_volume_write(self, connection: Optional[Connection], value: bytes) -> None:
|
def _on_volume_write(self, connection: Connection, value: bytes) -> None:
|
||||||
_logger.debug(f'--- VOLUME Write:{value[0]}')
|
_logger.debug(f'--- VOLUME Write:{value[0]}')
|
||||||
self.volume = value[0]
|
self.volume = value[0]
|
||||||
self.emit('volume_changed')
|
self.emit(self.EVENT_VOLUME_CHANGED)
|
||||||
|
|
||||||
# Register an L2CAP CoC server
|
# Register an L2CAP CoC server
|
||||||
def _on_connection(self, channel: l2cap.LeCreditBasedChannel) -> None:
|
def _on_connection(self, channel: l2cap.LeCreditBasedChannel) -> None:
|
||||||
@@ -259,11 +264,11 @@ class AshaService(gatt.TemplateService):
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class AshaServiceProxy(gatt_client.ProfileServiceProxy):
|
class AshaServiceProxy(gatt_client.ProfileServiceProxy):
|
||||||
SERVICE_CLASS = AshaService
|
SERVICE_CLASS = AshaService
|
||||||
read_only_properties_characteristic: gatt_client.CharacteristicProxy
|
read_only_properties_characteristic: gatt_client.CharacteristicProxy[bytes]
|
||||||
audio_control_point_characteristic: gatt_client.CharacteristicProxy
|
audio_control_point_characteristic: gatt_client.CharacteristicProxy[bytes]
|
||||||
audio_status_point_characteristic: gatt_client.CharacteristicProxy
|
audio_status_point_characteristic: gatt_client.CharacteristicProxy[bytes]
|
||||||
volume_characteristic: gatt_client.CharacteristicProxy
|
volume_characteristic: gatt_client.CharacteristicProxy[bytes]
|
||||||
psm_characteristic: gatt_client.CharacteristicProxy
|
psm_characteristic: gatt_client.CharacteristicProxy[bytes]
|
||||||
|
|
||||||
def __init__(self, service_proxy: gatt_client.ServiceProxy) -> None:
|
def __init__(self, service_proxy: gatt_client.ServiceProxy) -> None:
|
||||||
self.service_proxy = service_proxy
|
self.service_proxy = service_proxy
|
||||||
|
|||||||
@@ -354,7 +354,7 @@ class BroadcastAudioScanService(gatt.TemplateService):
|
|||||||
class BroadcastAudioScanServiceProxy(gatt_client.ProfileServiceProxy):
|
class BroadcastAudioScanServiceProxy(gatt_client.ProfileServiceProxy):
|
||||||
SERVICE_CLASS = BroadcastAudioScanService
|
SERVICE_CLASS = BroadcastAudioScanService
|
||||||
|
|
||||||
broadcast_audio_scan_control_point: gatt_client.CharacteristicProxy
|
broadcast_audio_scan_control_point: gatt_client.CharacteristicProxy[bytes]
|
||||||
broadcast_receive_states: list[
|
broadcast_receive_states: list[
|
||||||
gatt_client.CharacteristicProxy[Optional[BroadcastReceiveState]]
|
gatt_client.CharacteristicProxy[Optional[BroadcastReceiveState]]
|
||||||
]
|
]
|
||||||
|
|||||||
+11
-13
@@ -99,10 +99,10 @@ class CoordinatedSetIdentificationService(gatt.TemplateService):
|
|||||||
UUID = gatt.GATT_COORDINATED_SET_IDENTIFICATION_SERVICE
|
UUID = gatt.GATT_COORDINATED_SET_IDENTIFICATION_SERVICE
|
||||||
|
|
||||||
set_identity_resolving_key: bytes
|
set_identity_resolving_key: bytes
|
||||||
set_identity_resolving_key_characteristic: gatt.Characteristic
|
set_identity_resolving_key_characteristic: gatt.Characteristic[bytes]
|
||||||
coordinated_set_size_characteristic: Optional[gatt.Characteristic] = None
|
coordinated_set_size_characteristic: Optional[gatt.Characteristic[bytes]] = None
|
||||||
set_member_lock_characteristic: Optional[gatt.Characteristic] = None
|
set_member_lock_characteristic: Optional[gatt.Characteristic[bytes]] = None
|
||||||
set_member_rank_characteristic: Optional[gatt.Characteristic] = None
|
set_member_rank_characteristic: Optional[gatt.Characteristic[bytes]] = None
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -164,13 +164,11 @@ class CoordinatedSetIdentificationService(gatt.TemplateService):
|
|||||||
|
|
||||||
super().__init__(characteristics)
|
super().__init__(characteristics)
|
||||||
|
|
||||||
async def on_sirk_read(self, connection: Optional[device.Connection]) -> bytes:
|
async def on_sirk_read(self, connection: device.Connection) -> bytes:
|
||||||
if self.set_identity_resolving_key_type == SirkType.PLAINTEXT:
|
if self.set_identity_resolving_key_type == SirkType.PLAINTEXT:
|
||||||
sirk_bytes = self.set_identity_resolving_key
|
sirk_bytes = self.set_identity_resolving_key
|
||||||
else:
|
else:
|
||||||
assert connection
|
if connection.transport == core.PhysicalTransport.LE:
|
||||||
|
|
||||||
if connection.transport == core.BT_LE_TRANSPORT:
|
|
||||||
key = await connection.device.get_long_term_key(
|
key = await connection.device.get_long_term_key(
|
||||||
connection_handle=connection.handle, rand=b'', ediv=0
|
connection_handle=connection.handle, rand=b'', ediv=0
|
||||||
)
|
)
|
||||||
@@ -203,10 +201,10 @@ class CoordinatedSetIdentificationService(gatt.TemplateService):
|
|||||||
class CoordinatedSetIdentificationProxy(gatt_client.ProfileServiceProxy):
|
class CoordinatedSetIdentificationProxy(gatt_client.ProfileServiceProxy):
|
||||||
SERVICE_CLASS = CoordinatedSetIdentificationService
|
SERVICE_CLASS = CoordinatedSetIdentificationService
|
||||||
|
|
||||||
set_identity_resolving_key: gatt_client.CharacteristicProxy
|
set_identity_resolving_key: gatt_client.CharacteristicProxy[bytes]
|
||||||
coordinated_set_size: Optional[gatt_client.CharacteristicProxy] = None
|
coordinated_set_size: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||||
set_member_lock: Optional[gatt_client.CharacteristicProxy] = None
|
set_member_lock: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||||
set_member_rank: Optional[gatt_client.CharacteristicProxy] = None
|
set_member_rank: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||||
|
|
||||||
def __init__(self, service_proxy: gatt_client.ServiceProxy) -> None:
|
def __init__(self, service_proxy: gatt_client.ServiceProxy) -> None:
|
||||||
self.service_proxy = service_proxy
|
self.service_proxy = service_proxy
|
||||||
@@ -242,7 +240,7 @@ class CoordinatedSetIdentificationProxy(gatt_client.ProfileServiceProxy):
|
|||||||
else:
|
else:
|
||||||
connection = self.service_proxy.client.connection
|
connection = self.service_proxy.client.connection
|
||||||
device = connection.device
|
device = connection.device
|
||||||
if connection.transport == core.BT_LE_TRANSPORT:
|
if connection.transport == core.PhysicalTransport.LE:
|
||||||
key = await device.get_long_term_key(
|
key = await device.get_long_term_key(
|
||||||
connection_handle=connection.handle, rand=b'', ediv=0
|
connection_handle=connection.handle, rand=b'', ediv=0
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -32,10 +32,10 @@ class GenericAttributeProfileService(gatt.TemplateService):
|
|||||||
|
|
||||||
UUID = gatt.GATT_GENERIC_ATTRIBUTE_SERVICE
|
UUID = gatt.GATT_GENERIC_ATTRIBUTE_SERVICE
|
||||||
|
|
||||||
client_supported_features_characteristic: gatt.Characteristic | None = None
|
client_supported_features_characteristic: gatt.Characteristic[bytes] | None = None
|
||||||
server_supported_features_characteristic: gatt.Characteristic | None = None
|
server_supported_features_characteristic: gatt.Characteristic[bytes] | None = None
|
||||||
database_hash_characteristic: gatt.Characteristic | None = None
|
database_hash_characteristic: gatt.Characteristic[bytes] | None = None
|
||||||
service_changed_characteristic: gatt.Characteristic | None = None
|
service_changed_characteristic: gatt.Characteristic[bytes] | None = None
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -127,9 +127,7 @@ class GenericAttributeProfileService(gatt.TemplateService):
|
|||||||
|
|
||||||
return b''
|
return b''
|
||||||
|
|
||||||
def get_database_hash(self, connection: device.Connection | None) -> bytes:
|
def get_database_hash(self, connection: device.Connection) -> bytes:
|
||||||
assert connection
|
|
||||||
|
|
||||||
m = b''.join(
|
m = b''.join(
|
||||||
[
|
[
|
||||||
self.get_attribute_data(attribute)
|
self.get_attribute_data(attribute)
|
||||||
@@ -143,14 +141,14 @@ class GenericAttributeProfileService(gatt.TemplateService):
|
|||||||
class GenericAttributeProfileServiceProxy(gatt_client.ProfileServiceProxy):
|
class GenericAttributeProfileServiceProxy(gatt_client.ProfileServiceProxy):
|
||||||
SERVICE_CLASS = GenericAttributeProfileService
|
SERVICE_CLASS = GenericAttributeProfileService
|
||||||
|
|
||||||
client_supported_features_characteristic: gatt_client.CharacteristicProxy | None = (
|
client_supported_features_characteristic: (
|
||||||
None
|
gatt_client.CharacteristicProxy[bytes] | None
|
||||||
)
|
) = None
|
||||||
server_supported_features_characteristic: gatt_client.CharacteristicProxy | None = (
|
server_supported_features_characteristic: (
|
||||||
None
|
gatt_client.CharacteristicProxy[bytes] | None
|
||||||
)
|
) = None
|
||||||
database_hash_characteristic: gatt_client.CharacteristicProxy | None = None
|
database_hash_characteristic: gatt_client.CharacteristicProxy[bytes] | None = None
|
||||||
service_changed_characteristic: gatt_client.CharacteristicProxy | None = None
|
service_changed_characteristic: gatt_client.CharacteristicProxy[bytes] | None = None
|
||||||
|
|
||||||
_CHARACTERISTICS = {
|
_CHARACTERISTICS = {
|
||||||
gatt.GATT_CLIENT_SUPPORTED_FEATURES_CHARACTERISTIC: 'client_supported_features_characteristic',
|
gatt.GATT_CLIENT_SUPPORTED_FEATURES_CHARACTERISTIC: 'client_supported_features_characteristic',
|
||||||
|
|||||||
+32
-49
@@ -25,14 +25,14 @@ from typing import Any, Dict, List, Optional, Set, Union
|
|||||||
from bumble import att, gatt, gatt_adapters, gatt_client
|
from bumble import att, gatt, gatt_adapters, gatt_client
|
||||||
from bumble.core import InvalidArgumentError, InvalidStateError
|
from bumble.core import InvalidArgumentError, InvalidStateError
|
||||||
from bumble.device import Device, Connection
|
from bumble.device import Device, Connection
|
||||||
from bumble.utils import AsyncRunner, OpenIntEnum
|
from bumble import utils
|
||||||
from bumble.hci import Address
|
from bumble.hci import Address
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Constants
|
# Constants
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class ErrorCode(OpenIntEnum):
|
class ErrorCode(utils.OpenIntEnum):
|
||||||
'''See Hearing Access Service 2.4. Attribute Profile error codes.'''
|
'''See Hearing Access Service 2.4. Attribute Profile error codes.'''
|
||||||
|
|
||||||
INVALID_OPCODE = 0x80
|
INVALID_OPCODE = 0x80
|
||||||
@@ -42,7 +42,7 @@ class ErrorCode(OpenIntEnum):
|
|||||||
INVALID_PARAMETERS_LENGTH = 0x84
|
INVALID_PARAMETERS_LENGTH = 0x84
|
||||||
|
|
||||||
|
|
||||||
class HearingAidType(OpenIntEnum):
|
class HearingAidType(utils.OpenIntEnum):
|
||||||
'''See Hearing Access Service 3.1. Hearing Aid Features.'''
|
'''See Hearing Access Service 3.1. Hearing Aid Features.'''
|
||||||
|
|
||||||
BINAURAL_HEARING_AID = 0b00
|
BINAURAL_HEARING_AID = 0b00
|
||||||
@@ -50,35 +50,35 @@ class HearingAidType(OpenIntEnum):
|
|||||||
BANDED_HEARING_AID = 0b10
|
BANDED_HEARING_AID = 0b10
|
||||||
|
|
||||||
|
|
||||||
class PresetSynchronizationSupport(OpenIntEnum):
|
class PresetSynchronizationSupport(utils.OpenIntEnum):
|
||||||
'''See Hearing Access Service 3.1. Hearing Aid Features.'''
|
'''See Hearing Access Service 3.1. Hearing Aid Features.'''
|
||||||
|
|
||||||
PRESET_SYNCHRONIZATION_IS_NOT_SUPPORTED = 0b0
|
PRESET_SYNCHRONIZATION_IS_NOT_SUPPORTED = 0b0
|
||||||
PRESET_SYNCHRONIZATION_IS_SUPPORTED = 0b1
|
PRESET_SYNCHRONIZATION_IS_SUPPORTED = 0b1
|
||||||
|
|
||||||
|
|
||||||
class IndependentPresets(OpenIntEnum):
|
class IndependentPresets(utils.OpenIntEnum):
|
||||||
'''See Hearing Access Service 3.1. Hearing Aid Features.'''
|
'''See Hearing Access Service 3.1. Hearing Aid Features.'''
|
||||||
|
|
||||||
IDENTICAL_PRESET_RECORD = 0b0
|
IDENTICAL_PRESET_RECORD = 0b0
|
||||||
DIFFERENT_PRESET_RECORD = 0b1
|
DIFFERENT_PRESET_RECORD = 0b1
|
||||||
|
|
||||||
|
|
||||||
class DynamicPresets(OpenIntEnum):
|
class DynamicPresets(utils.OpenIntEnum):
|
||||||
'''See Hearing Access Service 3.1. Hearing Aid Features.'''
|
'''See Hearing Access Service 3.1. Hearing Aid Features.'''
|
||||||
|
|
||||||
PRESET_RECORDS_DOES_NOT_CHANGE = 0b0
|
PRESET_RECORDS_DOES_NOT_CHANGE = 0b0
|
||||||
PRESET_RECORDS_MAY_CHANGE = 0b1
|
PRESET_RECORDS_MAY_CHANGE = 0b1
|
||||||
|
|
||||||
|
|
||||||
class WritablePresetsSupport(OpenIntEnum):
|
class WritablePresetsSupport(utils.OpenIntEnum):
|
||||||
'''See Hearing Access Service 3.1. Hearing Aid Features.'''
|
'''See Hearing Access Service 3.1. Hearing Aid Features.'''
|
||||||
|
|
||||||
WRITABLE_PRESET_RECORDS_NOT_SUPPORTED = 0b0
|
WRITABLE_PRESET_RECORDS_NOT_SUPPORTED = 0b0
|
||||||
WRITABLE_PRESET_RECORDS_SUPPORTED = 0b1
|
WRITABLE_PRESET_RECORDS_SUPPORTED = 0b1
|
||||||
|
|
||||||
|
|
||||||
class HearingAidPresetControlPointOpcode(OpenIntEnum):
|
class HearingAidPresetControlPointOpcode(utils.OpenIntEnum):
|
||||||
'''See Hearing Access Service 3.3.1 Hearing Aid Preset Control Point operation requirements.'''
|
'''See Hearing Access Service 3.3.1 Hearing Aid Preset Control Point operation requirements.'''
|
||||||
|
|
||||||
# fmt: off
|
# fmt: off
|
||||||
@@ -130,7 +130,7 @@ def HearingAidFeatures_from_bytes(data: int) -> HearingAidFeatures:
|
|||||||
class PresetChangedOperation:
|
class PresetChangedOperation:
|
||||||
'''See Hearing Access Service 3.2.2.2. Preset Changed operation.'''
|
'''See Hearing Access Service 3.2.2.2. Preset Changed operation.'''
|
||||||
|
|
||||||
class ChangeId(OpenIntEnum):
|
class ChangeId(utils.OpenIntEnum):
|
||||||
# fmt: off
|
# fmt: off
|
||||||
GENERIC_UPDATE = 0x00
|
GENERIC_UPDATE = 0x00
|
||||||
PRESET_RECORD_DELETED = 0x01
|
PRESET_RECORD_DELETED = 0x01
|
||||||
@@ -190,11 +190,11 @@ class PresetRecord:
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Property:
|
class Property:
|
||||||
class Writable(OpenIntEnum):
|
class Writable(utils.OpenIntEnum):
|
||||||
CANNOT_BE_WRITTEN = 0b0
|
CANNOT_BE_WRITTEN = 0b0
|
||||||
CAN_BE_WRITTEN = 0b1
|
CAN_BE_WRITTEN = 0b1
|
||||||
|
|
||||||
class IsAvailable(OpenIntEnum):
|
class IsAvailable(utils.OpenIntEnum):
|
||||||
IS_UNAVAILABLE = 0b0
|
IS_UNAVAILABLE = 0b0
|
||||||
IS_AVAILABLE = 0b1
|
IS_AVAILABLE = 0b1
|
||||||
|
|
||||||
@@ -224,9 +224,9 @@ class PresetRecord:
|
|||||||
class HearingAccessService(gatt.TemplateService):
|
class HearingAccessService(gatt.TemplateService):
|
||||||
UUID = gatt.GATT_HEARING_ACCESS_SERVICE
|
UUID = gatt.GATT_HEARING_ACCESS_SERVICE
|
||||||
|
|
||||||
hearing_aid_features_characteristic: gatt.Characteristic
|
hearing_aid_features_characteristic: gatt.Characteristic[bytes]
|
||||||
hearing_aid_preset_control_point: gatt.Characteristic
|
hearing_aid_preset_control_point: gatt.Characteristic[bytes]
|
||||||
active_preset_index_characteristic: gatt.Characteristic
|
active_preset_index_characteristic: gatt.Characteristic[bytes]
|
||||||
active_preset_index: int
|
active_preset_index: int
|
||||||
active_preset_index_per_device: Dict[Address, int]
|
active_preset_index_per_device: Dict[Address, int]
|
||||||
|
|
||||||
@@ -266,13 +266,13 @@ class HearingAccessService(gatt.TemplateService):
|
|||||||
# associate the lowest index as the current active preset at startup
|
# associate the lowest index as the current active preset at startup
|
||||||
self.active_preset_index = sorted(self.preset_records.keys())[0]
|
self.active_preset_index = sorted(self.preset_records.keys())[0]
|
||||||
|
|
||||||
@device.on('connection') # type: ignore
|
@device.on(device.EVENT_CONNECTION)
|
||||||
def on_connection(connection: Connection) -> None:
|
def on_connection(connection: Connection) -> None:
|
||||||
@connection.on('disconnection') # type: ignore
|
@connection.on(connection.EVENT_DISCONNECTION)
|
||||||
def on_disconnection(_reason) -> None:
|
def on_disconnection(_reason) -> None:
|
||||||
self.currently_connected_clients.remove(connection)
|
self.currently_connected_clients.remove(connection)
|
||||||
|
|
||||||
@connection.on('pairing') # type: ignore
|
@connection.on(connection.EVENT_PAIRING)
|
||||||
def on_pairing(*_: Any) -> None:
|
def on_pairing(*_: Any) -> None:
|
||||||
self.on_incoming_paired_connection(connection)
|
self.on_incoming_paired_connection(connection)
|
||||||
|
|
||||||
@@ -333,11 +333,10 @@ class HearingAccessService(gatt.TemplateService):
|
|||||||
# Update the active preset index if needed
|
# Update the active preset index if needed
|
||||||
await self.notify_active_preset_for_connection(connection)
|
await self.notify_active_preset_for_connection(connection)
|
||||||
|
|
||||||
connection.abort_on('disconnection', on_connection_async())
|
utils.cancel_on_event(connection, 'disconnection', on_connection_async())
|
||||||
|
|
||||||
def _on_read_active_preset_index(
|
def _on_read_active_preset_index(self, connection: Connection) -> bytes:
|
||||||
self, __connection__: Optional[Connection]
|
del connection # Unused
|
||||||
) -> bytes:
|
|
||||||
return bytes([self.active_preset_index])
|
return bytes([self.active_preset_index])
|
||||||
|
|
||||||
# TODO this need to be triggered when device is unbonded
|
# TODO this need to be triggered when device is unbonded
|
||||||
@@ -345,18 +344,13 @@ class HearingAccessService(gatt.TemplateService):
|
|||||||
self.preset_changed_operations_history_per_device.pop(addr)
|
self.preset_changed_operations_history_per_device.pop(addr)
|
||||||
|
|
||||||
async def _on_write_hearing_aid_preset_control_point(
|
async def _on_write_hearing_aid_preset_control_point(
|
||||||
self, connection: Optional[Connection], value: bytes
|
self, connection: Connection, value: bytes
|
||||||
):
|
):
|
||||||
assert connection
|
|
||||||
|
|
||||||
opcode = HearingAidPresetControlPointOpcode(value[0])
|
opcode = HearingAidPresetControlPointOpcode(value[0])
|
||||||
handler = getattr(self, '_on_' + opcode.name.lower())
|
handler = getattr(self, '_on_' + opcode.name.lower())
|
||||||
await handler(connection, value)
|
await handler(connection, value)
|
||||||
|
|
||||||
async def _on_read_presets_request(
|
async def _on_read_presets_request(self, connection: Connection, value: bytes):
|
||||||
self, connection: Optional[Connection], value: bytes
|
|
||||||
):
|
|
||||||
assert connection
|
|
||||||
if connection.att_mtu < 49: # 2.5. GATT sub-procedure requirements
|
if connection.att_mtu < 49: # 2.5. GATT sub-procedure requirements
|
||||||
logging.warning(f'HAS require MTU >= 49: {connection}')
|
logging.warning(f'HAS require MTU >= 49: {connection}')
|
||||||
|
|
||||||
@@ -382,7 +376,7 @@ class HearingAccessService(gatt.TemplateService):
|
|||||||
if len(presets) == 0:
|
if len(presets) == 0:
|
||||||
raise att.ATT_Error(att.ErrorCode.OUT_OF_RANGE)
|
raise att.ATT_Error(att.ErrorCode.OUT_OF_RANGE)
|
||||||
|
|
||||||
AsyncRunner.spawn(self._read_preset_response(connection, presets))
|
utils.AsyncRunner.spawn(self._read_preset_response(connection, presets))
|
||||||
|
|
||||||
async def _read_preset_response(
|
async def _read_preset_response(
|
||||||
self, connection: Connection, presets: List[PresetRecord]
|
self, connection: Connection, presets: List[PresetRecord]
|
||||||
@@ -471,10 +465,7 @@ class HearingAccessService(gatt.TemplateService):
|
|||||||
for connection in self.currently_connected_clients:
|
for connection in self.currently_connected_clients:
|
||||||
await self._preset_changed_operation(connection)
|
await self._preset_changed_operation(connection)
|
||||||
|
|
||||||
async def _on_write_preset_name(
|
async def _on_write_preset_name(self, connection: Connection, value: bytes):
|
||||||
self, connection: Optional[Connection], value: bytes
|
|
||||||
):
|
|
||||||
assert connection
|
|
||||||
|
|
||||||
if self.read_presets_request_in_progress:
|
if self.read_presets_request_in_progress:
|
||||||
raise att.ATT_Error(att.ErrorCode.PROCEDURE_ALREADY_IN_PROGRESS)
|
raise att.ATT_Error(att.ErrorCode.PROCEDURE_ALREADY_IN_PROGRESS)
|
||||||
@@ -522,10 +513,7 @@ class HearingAccessService(gatt.TemplateService):
|
|||||||
for connection in self.currently_connected_clients:
|
for connection in self.currently_connected_clients:
|
||||||
await self.notify_active_preset_for_connection(connection)
|
await self.notify_active_preset_for_connection(connection)
|
||||||
|
|
||||||
async def set_active_preset(
|
async def set_active_preset(self, connection: Connection, value: bytes) -> None:
|
||||||
self, connection: Optional[Connection], value: bytes
|
|
||||||
) -> None:
|
|
||||||
assert connection
|
|
||||||
index = value[1]
|
index = value[1]
|
||||||
preset = self.preset_records.get(index, None)
|
preset = self.preset_records.get(index, None)
|
||||||
if (
|
if (
|
||||||
@@ -542,16 +530,11 @@ class HearingAccessService(gatt.TemplateService):
|
|||||||
self.active_preset_index = index
|
self.active_preset_index = index
|
||||||
await self.notify_active_preset()
|
await self.notify_active_preset()
|
||||||
|
|
||||||
async def _on_set_active_preset(
|
async def _on_set_active_preset(self, connection: Connection, value: bytes):
|
||||||
self, connection: Optional[Connection], value: bytes
|
|
||||||
):
|
|
||||||
await self.set_active_preset(connection, value)
|
await self.set_active_preset(connection, value)
|
||||||
|
|
||||||
async def set_next_or_previous_preset(
|
async def set_next_or_previous_preset(self, connection: Connection, is_previous):
|
||||||
self, connection: Optional[Connection], is_previous
|
|
||||||
):
|
|
||||||
'''Set the next or the previous preset as active'''
|
'''Set the next or the previous preset as active'''
|
||||||
assert connection
|
|
||||||
|
|
||||||
if self.active_preset_index == 0x00:
|
if self.active_preset_index == 0x00:
|
||||||
raise att.ATT_Error(ErrorCode.PRESET_OPERATION_NOT_POSSIBLE)
|
raise att.ATT_Error(ErrorCode.PRESET_OPERATION_NOT_POSSIBLE)
|
||||||
@@ -581,17 +564,17 @@ class HearingAccessService(gatt.TemplateService):
|
|||||||
await self.notify_active_preset()
|
await self.notify_active_preset()
|
||||||
|
|
||||||
async def _on_set_next_preset(
|
async def _on_set_next_preset(
|
||||||
self, connection: Optional[Connection], __value__: bytes
|
self, connection: Connection, __value__: bytes
|
||||||
) -> None:
|
) -> None:
|
||||||
await self.set_next_or_previous_preset(connection, False)
|
await self.set_next_or_previous_preset(connection, False)
|
||||||
|
|
||||||
async def _on_set_previous_preset(
|
async def _on_set_previous_preset(
|
||||||
self, connection: Optional[Connection], __value__: bytes
|
self, connection: Connection, __value__: bytes
|
||||||
) -> None:
|
) -> None:
|
||||||
await self.set_next_or_previous_preset(connection, True)
|
await self.set_next_or_previous_preset(connection, True)
|
||||||
|
|
||||||
async def _on_set_active_preset_synchronized_locally(
|
async def _on_set_active_preset_synchronized_locally(
|
||||||
self, connection: Optional[Connection], value: bytes
|
self, connection: Connection, value: bytes
|
||||||
):
|
):
|
||||||
if (
|
if (
|
||||||
self.server_features.preset_synchronization_support
|
self.server_features.preset_synchronization_support
|
||||||
@@ -602,7 +585,7 @@ class HearingAccessService(gatt.TemplateService):
|
|||||||
# TODO (low priority) inform other server of the change
|
# TODO (low priority) inform other server of the change
|
||||||
|
|
||||||
async def _on_set_next_preset_synchronized_locally(
|
async def _on_set_next_preset_synchronized_locally(
|
||||||
self, connection: Optional[Connection], __value__: bytes
|
self, connection: Connection, __value__: bytes
|
||||||
):
|
):
|
||||||
if (
|
if (
|
||||||
self.server_features.preset_synchronization_support
|
self.server_features.preset_synchronization_support
|
||||||
@@ -613,7 +596,7 @@ class HearingAccessService(gatt.TemplateService):
|
|||||||
# TODO (low priority) inform other server of the change
|
# TODO (low priority) inform other server of the change
|
||||||
|
|
||||||
async def _on_set_previous_preset_synchronized_locally(
|
async def _on_set_previous_preset_synchronized_locally(
|
||||||
self, connection: Optional[Connection], __value__: bytes
|
self, connection: Connection, __value__: bytes
|
||||||
):
|
):
|
||||||
if (
|
if (
|
||||||
self.server_features.preset_synchronization_support
|
self.server_features.preset_synchronization_support
|
||||||
|
|||||||
+38
-33
@@ -287,11 +287,8 @@ class MediaControlService(gatt.TemplateService):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def on_media_control_point(
|
async def on_media_control_point(
|
||||||
self, connection: Optional[device.Connection], data: bytes
|
self, connection: device.Connection, data: bytes
|
||||||
) -> None:
|
) -> None:
|
||||||
if not connection:
|
|
||||||
raise core.InvalidStateError()
|
|
||||||
|
|
||||||
opcode = MediaControlPointOpcode(data[0])
|
opcode = MediaControlPointOpcode(data[0])
|
||||||
|
|
||||||
await connection.device.notify_subscriber(
|
await connection.device.notify_subscriber(
|
||||||
@@ -338,30 +335,38 @@ class MediaControlServiceProxy(
|
|||||||
'content_control_id': gatt.GATT_CONTENT_CONTROL_ID_CHARACTERISTIC,
|
'content_control_id': gatt.GATT_CONTENT_CONTROL_ID_CHARACTERISTIC,
|
||||||
}
|
}
|
||||||
|
|
||||||
media_player_name: Optional[gatt_client.CharacteristicProxy] = None
|
EVENT_MEDIA_STATE = "media_state"
|
||||||
media_player_icon_object_id: Optional[gatt_client.CharacteristicProxy] = None
|
EVENT_TRACK_CHANGED = "track_changed"
|
||||||
media_player_icon_url: Optional[gatt_client.CharacteristicProxy] = None
|
EVENT_TRACK_TITLE = "track_title"
|
||||||
track_changed: Optional[gatt_client.CharacteristicProxy] = None
|
EVENT_TRACK_DURATION = "track_duration"
|
||||||
track_title: Optional[gatt_client.CharacteristicProxy] = None
|
EVENT_TRACK_POSITION = "track_position"
|
||||||
track_duration: Optional[gatt_client.CharacteristicProxy] = None
|
|
||||||
track_position: Optional[gatt_client.CharacteristicProxy] = None
|
media_player_name: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||||
playback_speed: Optional[gatt_client.CharacteristicProxy] = None
|
media_player_icon_object_id: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||||
seeking_speed: Optional[gatt_client.CharacteristicProxy] = None
|
media_player_icon_url: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||||
current_track_segments_object_id: Optional[gatt_client.CharacteristicProxy] = None
|
track_changed: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||||
current_track_object_id: Optional[gatt_client.CharacteristicProxy] = None
|
track_title: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||||
next_track_object_id: Optional[gatt_client.CharacteristicProxy] = None
|
track_duration: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||||
parent_group_object_id: Optional[gatt_client.CharacteristicProxy] = None
|
track_position: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||||
current_group_object_id: Optional[gatt_client.CharacteristicProxy] = None
|
playback_speed: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||||
playing_order: Optional[gatt_client.CharacteristicProxy] = None
|
seeking_speed: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||||
playing_orders_supported: Optional[gatt_client.CharacteristicProxy] = None
|
current_track_segments_object_id: Optional[
|
||||||
media_state: Optional[gatt_client.CharacteristicProxy] = None
|
gatt_client.CharacteristicProxy[bytes]
|
||||||
media_control_point: Optional[gatt_client.CharacteristicProxy] = None
|
] = None
|
||||||
media_control_point_opcodes_supported: Optional[gatt_client.CharacteristicProxy] = (
|
current_track_object_id: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||||
None
|
next_track_object_id: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||||
)
|
parent_group_object_id: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||||
search_control_point: Optional[gatt_client.CharacteristicProxy] = None
|
current_group_object_id: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||||
search_results_object_id: Optional[gatt_client.CharacteristicProxy] = None
|
playing_order: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
||||||
content_control_id: Optional[gatt_client.CharacteristicProxy] = 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:
|
if TYPE_CHECKING:
|
||||||
media_control_point_notifications: asyncio.Queue[bytes]
|
media_control_point_notifications: asyncio.Queue[bytes]
|
||||||
@@ -430,20 +435,20 @@ class MediaControlServiceProxy(
|
|||||||
self.media_control_point_notifications.put_nowait(data)
|
self.media_control_point_notifications.put_nowait(data)
|
||||||
|
|
||||||
def _on_media_state(self, data: bytes) -> None:
|
def _on_media_state(self, data: bytes) -> None:
|
||||||
self.emit('media_state', MediaState(data[0]))
|
self.emit(self.EVENT_MEDIA_STATE, MediaState(data[0]))
|
||||||
|
|
||||||
def _on_track_changed(self, data: bytes) -> None:
|
def _on_track_changed(self, data: bytes) -> None:
|
||||||
del data
|
del data
|
||||||
self.emit('track_changed')
|
self.emit(self.EVENT_TRACK_CHANGED)
|
||||||
|
|
||||||
def _on_track_title(self, data: bytes) -> None:
|
def _on_track_title(self, data: bytes) -> None:
|
||||||
self.emit('track_title', data.decode("utf-8"))
|
self.emit(self.EVENT_TRACK_TITLE, data.decode("utf-8"))
|
||||||
|
|
||||||
def _on_track_duration(self, data: bytes) -> None:
|
def _on_track_duration(self, data: bytes) -> None:
|
||||||
self.emit('track_duration', struct.unpack_from('<i', data)[0])
|
self.emit(self.EVENT_TRACK_DURATION, struct.unpack_from('<i', data)[0])
|
||||||
|
|
||||||
def _on_track_position(self, data: bytes) -> None:
|
def _on_track_position(self, data: bytes) -> None:
|
||||||
self.emit('track_position', struct.unpack_from('<i', data)[0])
|
self.emit(self.EVENT_TRACK_POSITION, struct.unpack_from('<i', data)[0])
|
||||||
|
|
||||||
|
|
||||||
class GenericMediaControlServiceProxy(MediaControlServiceProxy):
|
class GenericMediaControlServiceProxy(MediaControlServiceProxy):
|
||||||
|
|||||||
@@ -104,12 +104,12 @@ class PacRecord:
|
|||||||
class PublishedAudioCapabilitiesService(gatt.TemplateService):
|
class PublishedAudioCapabilitiesService(gatt.TemplateService):
|
||||||
UUID = gatt.GATT_PUBLISHED_AUDIO_CAPABILITIES_SERVICE
|
UUID = gatt.GATT_PUBLISHED_AUDIO_CAPABILITIES_SERVICE
|
||||||
|
|
||||||
sink_pac: Optional[gatt.Characteristic]
|
sink_pac: Optional[gatt.Characteristic[bytes]]
|
||||||
sink_audio_locations: Optional[gatt.Characteristic]
|
sink_audio_locations: Optional[gatt.Characteristic[bytes]]
|
||||||
source_pac: Optional[gatt.Characteristic]
|
source_pac: Optional[gatt.Characteristic[bytes]]
|
||||||
source_audio_locations: Optional[gatt.Characteristic]
|
source_audio_locations: Optional[gatt.Characteristic[bytes]]
|
||||||
available_audio_contexts: gatt.Characteristic
|
available_audio_contexts: gatt.Characteristic[bytes]
|
||||||
supported_audio_contexts: gatt.Characteristic
|
supported_audio_contexts: gatt.Characteristic[bytes]
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class PublicBroadcastAnnouncement:
|
|||||||
def from_bytes(cls, data: bytes) -> Self:
|
def from_bytes(cls, data: bytes) -> Self:
|
||||||
features = cls.Features(data[0])
|
features = cls.Features(data[0])
|
||||||
metadata_length = data[1]
|
metadata_length = data[1]
|
||||||
metadata_ltv = data[1 : 1 + metadata_length]
|
metadata_ltv = data[2 : 2 + metadata_length]
|
||||||
return cls(
|
return cls(
|
||||||
features=features, metadata=le_audio.Metadata.from_bytes(metadata_ltv)
|
features=features, metadata=le_audio.Metadata.from_bytes(metadata_ltv)
|
||||||
)
|
)
|
||||||
|
|||||||
+11
-9
@@ -23,6 +23,7 @@ import enum
|
|||||||
from typing import Optional, Sequence
|
from typing import Optional, Sequence
|
||||||
|
|
||||||
from bumble import att
|
from bumble import att
|
||||||
|
from bumble import utils
|
||||||
from bumble import device
|
from bumble import device
|
||||||
from bumble import gatt
|
from bumble import gatt
|
||||||
from bumble import gatt_adapters
|
from bumble import gatt_adapters
|
||||||
@@ -90,9 +91,11 @@ class VolumeState:
|
|||||||
class VolumeControlService(gatt.TemplateService):
|
class VolumeControlService(gatt.TemplateService):
|
||||||
UUID = gatt.GATT_VOLUME_CONTROL_SERVICE
|
UUID = gatt.GATT_VOLUME_CONTROL_SERVICE
|
||||||
|
|
||||||
volume_state: gatt.Characteristic
|
EVENT_VOLUME_STATE_CHANGE = "volume_state_change"
|
||||||
volume_control_point: gatt.Characteristic
|
|
||||||
volume_flags: gatt.Characteristic
|
volume_state: gatt.Characteristic[bytes]
|
||||||
|
volume_control_point: gatt.Characteristic[bytes]
|
||||||
|
volume_flags: gatt.Characteristic[bytes]
|
||||||
|
|
||||||
volume_setting: int
|
volume_setting: int
|
||||||
muted: int
|
muted: int
|
||||||
@@ -143,14 +146,12 @@ class VolumeControlService(gatt.TemplateService):
|
|||||||
included_services=list(included_services),
|
included_services=list(included_services),
|
||||||
)
|
)
|
||||||
|
|
||||||
def _on_read_volume_state(self, _connection: Optional[device.Connection]) -> bytes:
|
def _on_read_volume_state(self, _connection: device.Connection) -> bytes:
|
||||||
return bytes(VolumeState(self.volume_setting, self.muted, self.change_counter))
|
return bytes(VolumeState(self.volume_setting, self.muted, self.change_counter))
|
||||||
|
|
||||||
def _on_write_volume_control_point(
|
def _on_write_volume_control_point(
|
||||||
self, connection: Optional[device.Connection], value: bytes
|
self, connection: device.Connection, value: bytes
|
||||||
) -> None:
|
) -> None:
|
||||||
assert connection
|
|
||||||
|
|
||||||
opcode = VolumeControlPointOpcode(value[0])
|
opcode = VolumeControlPointOpcode(value[0])
|
||||||
change_counter = value[1]
|
change_counter = value[1]
|
||||||
|
|
||||||
@@ -160,11 +161,12 @@ class VolumeControlService(gatt.TemplateService):
|
|||||||
handler = getattr(self, '_on_' + opcode.name.lower())
|
handler = getattr(self, '_on_' + opcode.name.lower())
|
||||||
if handler(*value[2:]):
|
if handler(*value[2:]):
|
||||||
self.change_counter = (self.change_counter + 1) % 256
|
self.change_counter = (self.change_counter + 1) % 256
|
||||||
connection.abort_on(
|
utils.cancel_on_event(
|
||||||
|
connection,
|
||||||
'disconnection',
|
'disconnection',
|
||||||
connection.device.notify_subscribers(attribute=self.volume_state),
|
connection.device.notify_subscribers(attribute=self.volume_state),
|
||||||
)
|
)
|
||||||
self.emit('volume_state_change')
|
self.emit(self.EVENT_VOLUME_STATE_CHANGE)
|
||||||
|
|
||||||
def _on_relative_volume_down(self) -> bool:
|
def _on_relative_volume_down(self) -> bool:
|
||||||
old_volume = self.volume_setting
|
old_volume = self.volume_setting
|
||||||
|
|||||||
+9
-12
@@ -38,7 +38,7 @@ from bumble.gatt_adapters import (
|
|||||||
UTF8CharacteristicProxyAdapter,
|
UTF8CharacteristicProxyAdapter,
|
||||||
)
|
)
|
||||||
from bumble.gatt_client import ProfileServiceProxy, ServiceProxy
|
from bumble.gatt_client import ProfileServiceProxy, ServiceProxy
|
||||||
from bumble.utils import OpenIntEnum
|
from bumble import utils
|
||||||
from bumble.profiles.bap import AudioLocation
|
from bumble.profiles.bap import AudioLocation
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -50,11 +50,11 @@ MAX_VOLUME_OFFSET = 255
|
|||||||
CHANGE_COUNTER_MAX_VALUE = 0xFF
|
CHANGE_COUNTER_MAX_VALUE = 0xFF
|
||||||
|
|
||||||
|
|
||||||
class SetVolumeOffsetOpCode(OpenIntEnum):
|
class SetVolumeOffsetOpCode(utils.OpenIntEnum):
|
||||||
SET_VOLUME_OFFSET = 0x01
|
SET_VOLUME_OFFSET = 0x01
|
||||||
|
|
||||||
|
|
||||||
class ErrorCode(OpenIntEnum):
|
class ErrorCode(utils.OpenIntEnum):
|
||||||
"""
|
"""
|
||||||
See Volume Offset Control Service 1.6. Application error codes.
|
See Volume Offset Control Service 1.6. Application error codes.
|
||||||
"""
|
"""
|
||||||
@@ -86,7 +86,7 @@ class VolumeOffsetState:
|
|||||||
assert self.attribute is not None
|
assert self.attribute is not None
|
||||||
await connection.device.notify_subscribers(attribute=self.attribute)
|
await connection.device.notify_subscribers(attribute=self.attribute)
|
||||||
|
|
||||||
def on_read(self, _connection: Optional[Connection]) -> bytes:
|
def on_read(self, _connection: Connection) -> bytes:
|
||||||
return bytes(self)
|
return bytes(self)
|
||||||
|
|
||||||
|
|
||||||
@@ -103,11 +103,10 @@ class VocsAudioLocation:
|
|||||||
audio_location = AudioLocation(struct.unpack('<I', data)[0])
|
audio_location = AudioLocation(struct.unpack('<I', data)[0])
|
||||||
return cls(audio_location)
|
return cls(audio_location)
|
||||||
|
|
||||||
def on_read(self, _connection: Optional[Connection]) -> bytes:
|
def on_read(self, _connection: Connection) -> bytes:
|
||||||
return bytes(self)
|
return bytes(self)
|
||||||
|
|
||||||
async def on_write(self, connection: Optional[Connection], value: bytes) -> None:
|
async def on_write(self, connection: Connection, value: bytes) -> None:
|
||||||
assert connection
|
|
||||||
assert self.attribute
|
assert self.attribute
|
||||||
|
|
||||||
self.audio_location = AudioLocation(int.from_bytes(value, 'little'))
|
self.audio_location = AudioLocation(int.from_bytes(value, 'little'))
|
||||||
@@ -118,8 +117,7 @@ class VocsAudioLocation:
|
|||||||
class VolumeOffsetControlPoint:
|
class VolumeOffsetControlPoint:
|
||||||
volume_offset_state: VolumeOffsetState
|
volume_offset_state: VolumeOffsetState
|
||||||
|
|
||||||
async def on_write(self, connection: Optional[Connection], value: bytes) -> None:
|
async def on_write(self, connection: Connection, value: bytes) -> None:
|
||||||
assert connection
|
|
||||||
|
|
||||||
opcode = value[0]
|
opcode = value[0]
|
||||||
if opcode != SetVolumeOffsetOpCode.SET_VOLUME_OFFSET:
|
if opcode != SetVolumeOffsetOpCode.SET_VOLUME_OFFSET:
|
||||||
@@ -159,11 +157,10 @@ class AudioOutputDescription:
|
|||||||
def __bytes__(self) -> bytes:
|
def __bytes__(self) -> bytes:
|
||||||
return self.audio_output_description.encode('utf-8')
|
return self.audio_output_description.encode('utf-8')
|
||||||
|
|
||||||
def on_read(self, _connection: Optional[Connection]) -> bytes:
|
def on_read(self, _connection: Connection) -> bytes:
|
||||||
return bytes(self)
|
return bytes(self)
|
||||||
|
|
||||||
async def on_write(self, connection: Optional[Connection], value: bytes) -> None:
|
async def on_write(self, connection: Connection, value: bytes) -> None:
|
||||||
assert connection
|
|
||||||
assert self.attribute
|
assert self.attribute
|
||||||
|
|
||||||
self.audio_output_description = value.decode('utf-8')
|
self.audio_output_description = value.decode('utf-8')
|
||||||
|
|||||||
+25
-16
@@ -25,16 +25,16 @@ import enum
|
|||||||
from typing import Callable, Dict, List, Optional, Tuple, Union, TYPE_CHECKING
|
from typing import Callable, Dict, List, Optional, Tuple, Union, TYPE_CHECKING
|
||||||
from typing_extensions import Self
|
from typing_extensions import Self
|
||||||
|
|
||||||
from pyee import EventEmitter
|
|
||||||
|
|
||||||
from bumble import core
|
from bumble import core
|
||||||
from bumble import l2cap
|
from bumble import l2cap
|
||||||
from bumble import sdp
|
from bumble import sdp
|
||||||
from .colors import color
|
from bumble import utils
|
||||||
from .core import (
|
from bumble.colors import color
|
||||||
|
from bumble.core import (
|
||||||
UUID,
|
UUID,
|
||||||
BT_RFCOMM_PROTOCOL_ID,
|
BT_RFCOMM_PROTOCOL_ID,
|
||||||
BT_BR_EDR_TRANSPORT,
|
PhysicalTransport,
|
||||||
BT_L2CAP_PROTOCOL_ID,
|
BT_L2CAP_PROTOCOL_ID,
|
||||||
InvalidArgumentError,
|
InvalidArgumentError,
|
||||||
InvalidStateError,
|
InvalidStateError,
|
||||||
@@ -441,7 +441,10 @@ class RFCOMM_MCC_MSC:
|
|||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class DLC(EventEmitter):
|
class DLC(utils.EventEmitter):
|
||||||
|
EVENT_OPEN = "open"
|
||||||
|
EVENT_CLOSE = "close"
|
||||||
|
|
||||||
class State(enum.IntEnum):
|
class State(enum.IntEnum):
|
||||||
INIT = 0x00
|
INIT = 0x00
|
||||||
CONNECTING = 0x01
|
CONNECTING = 0x01
|
||||||
@@ -529,7 +532,7 @@ class DLC(EventEmitter):
|
|||||||
self.send_frame(RFCOMM_Frame.uih(c_r=self.c_r, dlci=0, information=mcc))
|
self.send_frame(RFCOMM_Frame.uih(c_r=self.c_r, dlci=0, information=mcc))
|
||||||
|
|
||||||
self.change_state(DLC.State.CONNECTED)
|
self.change_state(DLC.State.CONNECTED)
|
||||||
self.emit('open')
|
self.emit(self.EVENT_OPEN)
|
||||||
|
|
||||||
def on_ua_frame(self, _frame: RFCOMM_Frame) -> None:
|
def on_ua_frame(self, _frame: RFCOMM_Frame) -> None:
|
||||||
if self.state == DLC.State.CONNECTING:
|
if self.state == DLC.State.CONNECTING:
|
||||||
@@ -550,7 +553,7 @@ class DLC(EventEmitter):
|
|||||||
self.disconnection_result.set_result(None)
|
self.disconnection_result.set_result(None)
|
||||||
self.disconnection_result = None
|
self.disconnection_result = None
|
||||||
self.multiplexer.on_dlc_disconnection(self)
|
self.multiplexer.on_dlc_disconnection(self)
|
||||||
self.emit('close')
|
self.emit(self.EVENT_CLOSE)
|
||||||
else:
|
else:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
color(
|
color(
|
||||||
@@ -733,7 +736,7 @@ class DLC(EventEmitter):
|
|||||||
self.disconnection_result.cancel()
|
self.disconnection_result.cancel()
|
||||||
self.disconnection_result = None
|
self.disconnection_result = None
|
||||||
self.change_state(DLC.State.RESET)
|
self.change_state(DLC.State.RESET)
|
||||||
self.emit('close')
|
self.emit(self.EVENT_CLOSE)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return (
|
return (
|
||||||
@@ -749,7 +752,7 @@ class DLC(EventEmitter):
|
|||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class Multiplexer(EventEmitter):
|
class Multiplexer(utils.EventEmitter):
|
||||||
class Role(enum.IntEnum):
|
class Role(enum.IntEnum):
|
||||||
INITIATOR = 0x00
|
INITIATOR = 0x00
|
||||||
RESPONDER = 0x01
|
RESPONDER = 0x01
|
||||||
@@ -763,6 +766,8 @@ class Multiplexer(EventEmitter):
|
|||||||
DISCONNECTED = 0x05
|
DISCONNECTED = 0x05
|
||||||
RESET = 0x06
|
RESET = 0x06
|
||||||
|
|
||||||
|
EVENT_DLC = "dlc"
|
||||||
|
|
||||||
connection_result: Optional[asyncio.Future]
|
connection_result: Optional[asyncio.Future]
|
||||||
disconnection_result: Optional[asyncio.Future]
|
disconnection_result: Optional[asyncio.Future]
|
||||||
open_result: Optional[asyncio.Future]
|
open_result: Optional[asyncio.Future]
|
||||||
@@ -785,7 +790,7 @@ class Multiplexer(EventEmitter):
|
|||||||
# Become a sink for the L2CAP channel
|
# Become a sink for the L2CAP channel
|
||||||
l2cap_channel.sink = self.on_pdu
|
l2cap_channel.sink = self.on_pdu
|
||||||
|
|
||||||
l2cap_channel.on('close', self.on_l2cap_channel_close)
|
l2cap_channel.on(l2cap_channel.EVENT_CLOSE, self.on_l2cap_channel_close)
|
||||||
|
|
||||||
def change_state(self, new_state: State) -> None:
|
def change_state(self, new_state: State) -> None:
|
||||||
logger.debug(f'{self} state change -> {color(new_state.name, "cyan")}')
|
logger.debug(f'{self} state change -> {color(new_state.name, "cyan")}')
|
||||||
@@ -845,7 +850,7 @@ class Multiplexer(EventEmitter):
|
|||||||
self.open_result.set_exception(
|
self.open_result.set_exception(
|
||||||
core.ConnectionError(
|
core.ConnectionError(
|
||||||
core.ConnectionError.CONNECTION_REFUSED,
|
core.ConnectionError.CONNECTION_REFUSED,
|
||||||
BT_BR_EDR_TRANSPORT,
|
PhysicalTransport.BR_EDR,
|
||||||
self.l2cap_channel.connection.peer_address,
|
self.l2cap_channel.connection.peer_address,
|
||||||
'rfcomm',
|
'rfcomm',
|
||||||
)
|
)
|
||||||
@@ -901,7 +906,7 @@ class Multiplexer(EventEmitter):
|
|||||||
self.dlcs[pn.dlci] = dlc
|
self.dlcs[pn.dlci] = dlc
|
||||||
|
|
||||||
# Re-emit the handshake completion event
|
# Re-emit the handshake completion event
|
||||||
dlc.on('open', lambda: self.emit('dlc', dlc))
|
dlc.on(dlc.EVENT_OPEN, lambda: self.emit(self.EVENT_DLC, dlc))
|
||||||
|
|
||||||
# Respond to complete the handshake
|
# Respond to complete the handshake
|
||||||
dlc.accept()
|
dlc.accept()
|
||||||
@@ -1075,7 +1080,9 @@ class Client:
|
|||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class Server(EventEmitter):
|
class Server(utils.EventEmitter):
|
||||||
|
EVENT_START = "start"
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, device: Device, l2cap_mtu: int = RFCOMM_DEFAULT_L2CAP_MTU
|
self, device: Device, l2cap_mtu: int = RFCOMM_DEFAULT_L2CAP_MTU
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -1122,7 +1129,9 @@ class Server(EventEmitter):
|
|||||||
|
|
||||||
def on_connection(self, l2cap_channel: l2cap.ClassicChannel) -> None:
|
def on_connection(self, l2cap_channel: l2cap.ClassicChannel) -> None:
|
||||||
logger.debug(f'+++ new L2CAP connection: {l2cap_channel}')
|
logger.debug(f'+++ new L2CAP connection: {l2cap_channel}')
|
||||||
l2cap_channel.on('open', lambda: self.on_l2cap_channel_open(l2cap_channel))
|
l2cap_channel.on(
|
||||||
|
l2cap_channel.EVENT_OPEN, lambda: self.on_l2cap_channel_open(l2cap_channel)
|
||||||
|
)
|
||||||
|
|
||||||
def on_l2cap_channel_open(self, l2cap_channel: l2cap.ClassicChannel) -> None:
|
def on_l2cap_channel_open(self, l2cap_channel: l2cap.ClassicChannel) -> None:
|
||||||
logger.debug(f'$$$ L2CAP channel open: {l2cap_channel}')
|
logger.debug(f'$$$ L2CAP channel open: {l2cap_channel}')
|
||||||
@@ -1130,10 +1139,10 @@ class Server(EventEmitter):
|
|||||||
# Create a new multiplexer for the channel
|
# Create a new multiplexer for the channel
|
||||||
multiplexer = Multiplexer(l2cap_channel, Multiplexer.Role.RESPONDER)
|
multiplexer = Multiplexer(l2cap_channel, Multiplexer.Role.RESPONDER)
|
||||||
multiplexer.acceptor = self.accept_dlc
|
multiplexer.acceptor = self.accept_dlc
|
||||||
multiplexer.on('dlc', self.on_dlc)
|
multiplexer.on(multiplexer.EVENT_DLC, self.on_dlc)
|
||||||
|
|
||||||
# Notify
|
# Notify
|
||||||
self.emit('start', multiplexer)
|
self.emit(self.EVENT_START, multiplexer)
|
||||||
|
|
||||||
def accept_dlc(self, channel_number: int) -> Optional[Tuple[int, int]]:
|
def accept_dlc(self, channel_number: int) -> Optional[Tuple[int, int]]:
|
||||||
return self.dlc_configs.get(channel_number)
|
return self.dlc_configs.get(channel_number)
|
||||||
|
|||||||
+1
-1
@@ -33,7 +33,7 @@ from bumble.core import (
|
|||||||
from bumble.hci import HCI_Object, name_or_number, key_with_value
|
from bumble.hci import HCI_Object, name_or_number, key_with_value
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .device import Device, Connection
|
from bumble.device import Device, Connection
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Logging
|
# Logging
|
||||||
|
|||||||
+50
-38
@@ -41,26 +41,25 @@ from typing import (
|
|||||||
cast,
|
cast,
|
||||||
)
|
)
|
||||||
|
|
||||||
from pyee import EventEmitter
|
|
||||||
|
|
||||||
from .colors import color
|
from bumble.colors import color
|
||||||
from .hci import (
|
from bumble.hci import (
|
||||||
Address,
|
Address,
|
||||||
Role,
|
Role,
|
||||||
HCI_LE_Enable_Encryption_Command,
|
HCI_LE_Enable_Encryption_Command,
|
||||||
HCI_Object,
|
HCI_Object,
|
||||||
key_with_value,
|
key_with_value,
|
||||||
)
|
)
|
||||||
from .core import (
|
from bumble.core import (
|
||||||
BT_BR_EDR_TRANSPORT,
|
PhysicalTransport,
|
||||||
BT_LE_TRANSPORT,
|
|
||||||
AdvertisingData,
|
AdvertisingData,
|
||||||
InvalidArgumentError,
|
InvalidArgumentError,
|
||||||
ProtocolError,
|
ProtocolError,
|
||||||
name_or_number,
|
name_or_number,
|
||||||
)
|
)
|
||||||
from .keys import PairingKeys
|
from bumble.keys import PairingKeys
|
||||||
from . import crypto
|
from bumble import crypto
|
||||||
|
from bumble import utils
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from bumble.device import Connection, Device
|
from bumble.device import Connection, Device
|
||||||
@@ -725,12 +724,13 @@ class Session:
|
|||||||
self.is_responder = not self.is_initiator
|
self.is_responder = not self.is_initiator
|
||||||
|
|
||||||
# Listen for connection events
|
# Listen for connection events
|
||||||
connection.on('disconnection', self.on_disconnection)
|
connection.on(connection.EVENT_DISCONNECTION, self.on_disconnection)
|
||||||
connection.on(
|
connection.on(
|
||||||
'connection_encryption_change', self.on_connection_encryption_change
|
connection.EVENT_CONNECTION_ENCRYPTION_CHANGE,
|
||||||
|
self.on_connection_encryption_change,
|
||||||
)
|
)
|
||||||
connection.on(
|
connection.on(
|
||||||
'connection_encryption_key_refresh',
|
connection.EVENT_CONNECTION_ENCRYPTION_KEY_REFRESH,
|
||||||
self.on_connection_encryption_key_refresh,
|
self.on_connection_encryption_key_refresh,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -857,7 +857,7 @@ class Session:
|
|||||||
initiator_io_capability: int,
|
initiator_io_capability: int,
|
||||||
responder_io_capability: int,
|
responder_io_capability: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
if self.connection.transport == BT_BR_EDR_TRANSPORT:
|
if self.connection.transport == PhysicalTransport.BR_EDR:
|
||||||
self.pairing_method = PairingMethod.CTKD_OVER_CLASSIC
|
self.pairing_method = PairingMethod.CTKD_OVER_CLASSIC
|
||||||
return
|
return
|
||||||
if (not self.mitm) and (auth_req & SMP_MITM_AUTHREQ == 0):
|
if (not self.mitm) and (auth_req & SMP_MITM_AUTHREQ == 0):
|
||||||
@@ -900,7 +900,7 @@ class Session:
|
|||||||
|
|
||||||
self.send_pairing_failed(SMP_CONFIRM_VALUE_FAILED_ERROR)
|
self.send_pairing_failed(SMP_CONFIRM_VALUE_FAILED_ERROR)
|
||||||
|
|
||||||
self.connection.abort_on('disconnection', prompt())
|
utils.cancel_on_event(self.connection, 'disconnection', prompt())
|
||||||
|
|
||||||
def prompt_user_for_numeric_comparison(
|
def prompt_user_for_numeric_comparison(
|
||||||
self, code: int, next_steps: Callable[[], None]
|
self, code: int, next_steps: Callable[[], None]
|
||||||
@@ -919,7 +919,7 @@ class Session:
|
|||||||
|
|
||||||
self.send_pairing_failed(SMP_CONFIRM_VALUE_FAILED_ERROR)
|
self.send_pairing_failed(SMP_CONFIRM_VALUE_FAILED_ERROR)
|
||||||
|
|
||||||
self.connection.abort_on('disconnection', prompt())
|
utils.cancel_on_event(self.connection, 'disconnection', prompt())
|
||||||
|
|
||||||
def prompt_user_for_number(self, next_steps: Callable[[int], None]) -> None:
|
def prompt_user_for_number(self, next_steps: Callable[[int], None]) -> None:
|
||||||
async def prompt() -> None:
|
async def prompt() -> None:
|
||||||
@@ -936,7 +936,7 @@ class Session:
|
|||||||
logger.warning(f'exception while prompting: {error}')
|
logger.warning(f'exception while prompting: {error}')
|
||||||
self.send_pairing_failed(SMP_PASSKEY_ENTRY_FAILED_ERROR)
|
self.send_pairing_failed(SMP_PASSKEY_ENTRY_FAILED_ERROR)
|
||||||
|
|
||||||
self.connection.abort_on('disconnection', prompt())
|
utils.cancel_on_event(self.connection, 'disconnection', prompt())
|
||||||
|
|
||||||
def display_passkey(self) -> None:
|
def display_passkey(self) -> None:
|
||||||
# Generate random Passkey/PIN code
|
# Generate random Passkey/PIN code
|
||||||
@@ -951,7 +951,8 @@ class Session:
|
|||||||
logger.debug(f'TK from passkey = {self.tk.hex()}')
|
logger.debug(f'TK from passkey = {self.tk.hex()}')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.connection.abort_on(
|
utils.cancel_on_event(
|
||||||
|
self.connection,
|
||||||
'disconnection',
|
'disconnection',
|
||||||
self.pairing_config.delegate.display_number(self.passkey, digits=6),
|
self.pairing_config.delegate.display_number(self.passkey, digits=6),
|
||||||
)
|
)
|
||||||
@@ -1050,7 +1051,7 @@ class Session:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Perform the next steps asynchronously in case we need to wait for input
|
# Perform the next steps asynchronously in case we need to wait for input
|
||||||
self.connection.abort_on('disconnection', next_steps())
|
utils.cancel_on_event(self.connection, 'disconnection', next_steps())
|
||||||
else:
|
else:
|
||||||
confirm_value = crypto.c1(
|
confirm_value = crypto.c1(
|
||||||
self.tk,
|
self.tk,
|
||||||
@@ -1170,11 +1171,11 @@ class Session:
|
|||||||
if self.is_initiator:
|
if self.is_initiator:
|
||||||
# CTKD: Derive LTK from LinkKey
|
# CTKD: Derive LTK from LinkKey
|
||||||
if (
|
if (
|
||||||
self.connection.transport == BT_BR_EDR_TRANSPORT
|
self.connection.transport == PhysicalTransport.BR_EDR
|
||||||
and self.initiator_key_distribution & SMP_ENC_KEY_DISTRIBUTION_FLAG
|
and self.initiator_key_distribution & SMP_ENC_KEY_DISTRIBUTION_FLAG
|
||||||
):
|
):
|
||||||
self.ctkd_task = self.connection.abort_on(
|
self.ctkd_task = utils.cancel_on_event(
|
||||||
'disconnection', self.get_link_key_and_derive_ltk()
|
self.connection, 'disconnection', self.get_link_key_and_derive_ltk()
|
||||||
)
|
)
|
||||||
elif not self.sc:
|
elif not self.sc:
|
||||||
# Distribute the LTK, EDIV and RAND
|
# Distribute the LTK, EDIV and RAND
|
||||||
@@ -1209,11 +1210,11 @@ class Session:
|
|||||||
else:
|
else:
|
||||||
# CTKD: Derive LTK from LinkKey
|
# CTKD: Derive LTK from LinkKey
|
||||||
if (
|
if (
|
||||||
self.connection.transport == BT_BR_EDR_TRANSPORT
|
self.connection.transport == PhysicalTransport.BR_EDR
|
||||||
and self.responder_key_distribution & SMP_ENC_KEY_DISTRIBUTION_FLAG
|
and self.responder_key_distribution & SMP_ENC_KEY_DISTRIBUTION_FLAG
|
||||||
):
|
):
|
||||||
self.ctkd_task = self.connection.abort_on(
|
self.ctkd_task = utils.cancel_on_event(
|
||||||
'disconnection', self.get_link_key_and_derive_ltk()
|
self.connection, 'disconnection', self.get_link_key_and_derive_ltk()
|
||||||
)
|
)
|
||||||
# Distribute the LTK, EDIV and RAND
|
# Distribute the LTK, EDIV and RAND
|
||||||
elif not self.sc:
|
elif not self.sc:
|
||||||
@@ -1248,7 +1249,7 @@ class Session:
|
|||||||
def compute_peer_expected_distributions(self, key_distribution_flags: int) -> None:
|
def compute_peer_expected_distributions(self, key_distribution_flags: int) -> None:
|
||||||
# Set our expectations for what to wait for in the key distribution phase
|
# Set our expectations for what to wait for in the key distribution phase
|
||||||
self.peer_expected_distributions = []
|
self.peer_expected_distributions = []
|
||||||
if not self.sc and self.connection.transport == BT_LE_TRANSPORT:
|
if not self.sc and self.connection.transport == PhysicalTransport.LE:
|
||||||
if key_distribution_flags & SMP_ENC_KEY_DISTRIBUTION_FLAG != 0:
|
if key_distribution_flags & SMP_ENC_KEY_DISTRIBUTION_FLAG != 0:
|
||||||
self.peer_expected_distributions.append(
|
self.peer_expected_distributions.append(
|
||||||
SMP_Encryption_Information_Command
|
SMP_Encryption_Information_Command
|
||||||
@@ -1305,15 +1306,20 @@ class Session:
|
|||||||
|
|
||||||
# Wait for the pairing process to finish
|
# Wait for the pairing process to finish
|
||||||
assert self.pairing_result
|
assert self.pairing_result
|
||||||
await self.connection.abort_on('disconnection', self.pairing_result)
|
await utils.cancel_on_event(
|
||||||
|
self.connection, 'disconnection', self.pairing_result
|
||||||
|
)
|
||||||
|
|
||||||
def on_disconnection(self, _: int) -> None:
|
def on_disconnection(self, _: int) -> None:
|
||||||
self.connection.remove_listener('disconnection', self.on_disconnection)
|
|
||||||
self.connection.remove_listener(
|
self.connection.remove_listener(
|
||||||
'connection_encryption_change', self.on_connection_encryption_change
|
self.connection.EVENT_DISCONNECTION, self.on_disconnection
|
||||||
)
|
)
|
||||||
self.connection.remove_listener(
|
self.connection.remove_listener(
|
||||||
'connection_encryption_key_refresh',
|
self.connection.EVENT_CONNECTION_ENCRYPTION_CHANGE,
|
||||||
|
self.on_connection_encryption_change,
|
||||||
|
)
|
||||||
|
self.connection.remove_listener(
|
||||||
|
self.connection.EVENT_CONNECTION_ENCRYPTION_KEY_REFRESH,
|
||||||
self.on_connection_encryption_key_refresh,
|
self.on_connection_encryption_key_refresh,
|
||||||
)
|
)
|
||||||
self.manager.on_session_end(self)
|
self.manager.on_session_end(self)
|
||||||
@@ -1323,7 +1329,7 @@ class Session:
|
|||||||
if self.is_initiator:
|
if self.is_initiator:
|
||||||
self.distribute_keys()
|
self.distribute_keys()
|
||||||
|
|
||||||
self.connection.abort_on('disconnection', self.on_pairing())
|
utils.cancel_on_event(self.connection, 'disconnection', self.on_pairing())
|
||||||
|
|
||||||
def on_connection_encryption_change(self) -> None:
|
def on_connection_encryption_change(self) -> None:
|
||||||
if self.connection.is_encrypted and not self.completed:
|
if self.connection.is_encrypted and not self.completed:
|
||||||
@@ -1365,7 +1371,7 @@ class Session:
|
|||||||
keys = PairingKeys()
|
keys = PairingKeys()
|
||||||
keys.address_type = peer_address.address_type
|
keys.address_type = peer_address.address_type
|
||||||
authenticated = self.pairing_method != PairingMethod.JUST_WORKS
|
authenticated = self.pairing_method != PairingMethod.JUST_WORKS
|
||||||
if self.sc or self.connection.transport == BT_BR_EDR_TRANSPORT:
|
if self.sc or self.connection.transport == PhysicalTransport.BR_EDR:
|
||||||
keys.ltk = PairingKeys.Key(value=self.ltk, authenticated=authenticated)
|
keys.ltk = PairingKeys.Key(value=self.ltk, authenticated=authenticated)
|
||||||
else:
|
else:
|
||||||
our_ltk_key = PairingKeys.Key(
|
our_ltk_key = PairingKeys.Key(
|
||||||
@@ -1374,8 +1380,10 @@ class Session:
|
|||||||
ediv=self.ltk_ediv,
|
ediv=self.ltk_ediv,
|
||||||
rand=self.ltk_rand,
|
rand=self.ltk_rand,
|
||||||
)
|
)
|
||||||
|
if not self.peer_ltk:
|
||||||
|
logger.error("peer_ltk is None")
|
||||||
peer_ltk_key = PairingKeys.Key(
|
peer_ltk_key = PairingKeys.Key(
|
||||||
value=self.peer_ltk,
|
value=self.peer_ltk or b'',
|
||||||
authenticated=authenticated,
|
authenticated=authenticated,
|
||||||
ediv=self.peer_ediv,
|
ediv=self.peer_ediv,
|
||||||
rand=self.peer_rand,
|
rand=self.peer_rand,
|
||||||
@@ -1432,8 +1440,10 @@ class Session:
|
|||||||
def on_smp_pairing_request_command(
|
def on_smp_pairing_request_command(
|
||||||
self, command: SMP_Pairing_Request_Command
|
self, command: SMP_Pairing_Request_Command
|
||||||
) -> None:
|
) -> None:
|
||||||
self.connection.abort_on(
|
utils.cancel_on_event(
|
||||||
'disconnection', self.on_smp_pairing_request_command_async(command)
|
self.connection,
|
||||||
|
'disconnection',
|
||||||
|
self.on_smp_pairing_request_command_async(command),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def on_smp_pairing_request_command_async(
|
async def on_smp_pairing_request_command_async(
|
||||||
@@ -1506,7 +1516,7 @@ class Session:
|
|||||||
# CTKD over BR/EDR should happen after the connection has been encrypted,
|
# CTKD over BR/EDR should happen after the connection has been encrypted,
|
||||||
# so when receiving pairing requests, responder should start distributing keys
|
# so when receiving pairing requests, responder should start distributing keys
|
||||||
if (
|
if (
|
||||||
self.connection.transport == BT_BR_EDR_TRANSPORT
|
self.connection.transport == PhysicalTransport.BR_EDR
|
||||||
and self.connection.is_encrypted
|
and self.connection.is_encrypted
|
||||||
and self.is_responder
|
and self.is_responder
|
||||||
and accepted
|
and accepted
|
||||||
@@ -1878,7 +1888,7 @@ class Session:
|
|||||||
self.wait_before_continuing = None
|
self.wait_before_continuing = None
|
||||||
self.send_pairing_dhkey_check_command()
|
self.send_pairing_dhkey_check_command()
|
||||||
|
|
||||||
self.connection.abort_on('disconnection', next_steps())
|
utils.cancel_on_event(self.connection, 'disconnection', next_steps())
|
||||||
else:
|
else:
|
||||||
self.send_pairing_dhkey_check_command()
|
self.send_pairing_dhkey_check_command()
|
||||||
else:
|
else:
|
||||||
@@ -1922,7 +1932,7 @@ class Session:
|
|||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class Manager(EventEmitter):
|
class Manager(utils.EventEmitter):
|
||||||
'''
|
'''
|
||||||
Implements the Initiator and Responder roles of the Security Manager Protocol
|
Implements the Initiator and Responder roles of the Security Manager Protocol
|
||||||
'''
|
'''
|
||||||
@@ -1950,13 +1960,15 @@ class Manager(EventEmitter):
|
|||||||
f'>>> Sending SMP Command on connection [0x{connection.handle:04X}] '
|
f'>>> Sending SMP Command on connection [0x{connection.handle:04X}] '
|
||||||
f'{connection.peer_address}: {command}'
|
f'{connection.peer_address}: {command}'
|
||||||
)
|
)
|
||||||
cid = SMP_BR_CID if connection.transport == BT_BR_EDR_TRANSPORT else SMP_CID
|
cid = (
|
||||||
|
SMP_BR_CID if connection.transport == PhysicalTransport.BR_EDR else SMP_CID
|
||||||
|
)
|
||||||
connection.send_l2cap_pdu(cid, bytes(command))
|
connection.send_l2cap_pdu(cid, bytes(command))
|
||||||
|
|
||||||
def on_smp_security_request_command(
|
def on_smp_security_request_command(
|
||||||
self, connection: Connection, request: SMP_Security_Request_Command
|
self, connection: Connection, request: SMP_Security_Request_Command
|
||||||
) -> None:
|
) -> None:
|
||||||
connection.emit('security_request', request.auth_req)
|
connection.emit(connection.EVENT_SECURITY_REQUEST, request.auth_req)
|
||||||
|
|
||||||
def on_smp_pdu(self, connection: Connection, pdu: bytes) -> None:
|
def on_smp_pdu(self, connection: Connection, pdu: bytes) -> None:
|
||||||
# Parse the L2CAP payload into an SMP Command object
|
# Parse the L2CAP payload into an SMP Command object
|
||||||
|
|||||||
@@ -20,8 +20,13 @@ import logging
|
|||||||
import os
|
import os
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from .common import Transport, AsyncPipeSink, SnoopingTransport, TransportSpecError
|
from bumble.transport.common import (
|
||||||
from ..snoop import create_snooper
|
Transport,
|
||||||
|
AsyncPipeSink,
|
||||||
|
SnoopingTransport,
|
||||||
|
TransportSpecError,
|
||||||
|
)
|
||||||
|
from bumble.snoop import create_snooper
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Logging
|
# Logging
|
||||||
@@ -108,80 +113,80 @@ async def _open_transport(scheme: str, spec: Optional[str]) -> Transport:
|
|||||||
# pylint: disable=too-many-return-statements
|
# pylint: disable=too-many-return-statements
|
||||||
|
|
||||||
if scheme == 'serial' and spec:
|
if scheme == 'serial' and spec:
|
||||||
from .serial import open_serial_transport
|
from bumble.transport.serial import open_serial_transport
|
||||||
|
|
||||||
return await open_serial_transport(spec)
|
return await open_serial_transport(spec)
|
||||||
|
|
||||||
if scheme == 'udp' and spec:
|
if scheme == 'udp' and spec:
|
||||||
from .udp import open_udp_transport
|
from bumble.transport.udp import open_udp_transport
|
||||||
|
|
||||||
return await open_udp_transport(spec)
|
return await open_udp_transport(spec)
|
||||||
|
|
||||||
if scheme == 'tcp-client' and spec:
|
if scheme == 'tcp-client' and spec:
|
||||||
from .tcp_client import open_tcp_client_transport
|
from bumble.transport.tcp_client import open_tcp_client_transport
|
||||||
|
|
||||||
return await open_tcp_client_transport(spec)
|
return await open_tcp_client_transport(spec)
|
||||||
|
|
||||||
if scheme == 'tcp-server' and spec:
|
if scheme == 'tcp-server' and spec:
|
||||||
from .tcp_server import open_tcp_server_transport
|
from bumble.transport.tcp_server import open_tcp_server_transport
|
||||||
|
|
||||||
return await open_tcp_server_transport(spec)
|
return await open_tcp_server_transport(spec)
|
||||||
|
|
||||||
if scheme == 'ws-client' and spec:
|
if scheme == 'ws-client' and spec:
|
||||||
from .ws_client import open_ws_client_transport
|
from bumble.transport.ws_client import open_ws_client_transport
|
||||||
|
|
||||||
return await open_ws_client_transport(spec)
|
return await open_ws_client_transport(spec)
|
||||||
|
|
||||||
if scheme == 'ws-server' and spec:
|
if scheme == 'ws-server' and spec:
|
||||||
from .ws_server import open_ws_server_transport
|
from bumble.transport.ws_server import open_ws_server_transport
|
||||||
|
|
||||||
return await open_ws_server_transport(spec)
|
return await open_ws_server_transport(spec)
|
||||||
|
|
||||||
if scheme == 'pty':
|
if scheme == 'pty':
|
||||||
from .pty import open_pty_transport
|
from bumble.transport.pty import open_pty_transport
|
||||||
|
|
||||||
return await open_pty_transport(spec)
|
return await open_pty_transport(spec)
|
||||||
|
|
||||||
if scheme == 'file':
|
if scheme == 'file':
|
||||||
from .file import open_file_transport
|
from bumble.transport.file import open_file_transport
|
||||||
|
|
||||||
assert spec is not None
|
assert spec is not None
|
||||||
return await open_file_transport(spec)
|
return await open_file_transport(spec)
|
||||||
|
|
||||||
if scheme == 'vhci':
|
if scheme == 'vhci':
|
||||||
from .vhci import open_vhci_transport
|
from bumble.transport.vhci import open_vhci_transport
|
||||||
|
|
||||||
return await open_vhci_transport(spec)
|
return await open_vhci_transport(spec)
|
||||||
|
|
||||||
if scheme == 'hci-socket':
|
if scheme == 'hci-socket':
|
||||||
from .hci_socket import open_hci_socket_transport
|
from bumble.transport.hci_socket import open_hci_socket_transport
|
||||||
|
|
||||||
return await open_hci_socket_transport(spec)
|
return await open_hci_socket_transport(spec)
|
||||||
|
|
||||||
if scheme == 'usb':
|
if scheme == 'usb':
|
||||||
from .usb import open_usb_transport
|
from bumble.transport.usb import open_usb_transport
|
||||||
|
|
||||||
assert spec
|
assert spec
|
||||||
return await open_usb_transport(spec)
|
return await open_usb_transport(spec)
|
||||||
|
|
||||||
if scheme == 'pyusb':
|
if scheme == 'pyusb':
|
||||||
from .pyusb import open_pyusb_transport
|
from bumble.transport.pyusb import open_pyusb_transport
|
||||||
|
|
||||||
assert spec
|
assert spec
|
||||||
return await open_pyusb_transport(spec)
|
return await open_pyusb_transport(spec)
|
||||||
|
|
||||||
if scheme == 'android-emulator':
|
if scheme == 'android-emulator':
|
||||||
from .android_emulator import open_android_emulator_transport
|
from bumble.transport.android_emulator import open_android_emulator_transport
|
||||||
|
|
||||||
return await open_android_emulator_transport(spec)
|
return await open_android_emulator_transport(spec)
|
||||||
|
|
||||||
if scheme == 'android-netsim':
|
if scheme == 'android-netsim':
|
||||||
from .android_netsim import open_android_netsim_transport
|
from bumble.transport.android_netsim import open_android_netsim_transport
|
||||||
|
|
||||||
return await open_android_netsim_transport(spec)
|
return await open_android_netsim_transport(spec)
|
||||||
|
|
||||||
if scheme == 'unix':
|
if scheme == 'unix':
|
||||||
from .unix import open_unix_client_transport
|
from bumble.transport.unix import open_unix_client_transport
|
||||||
|
|
||||||
assert spec
|
assert spec
|
||||||
return await open_unix_client_transport(spec)
|
return await open_unix_client_transport(spec)
|
||||||
@@ -204,8 +209,8 @@ async def open_transport_or_link(name: str) -> Transport:
|
|||||||
"""
|
"""
|
||||||
if name.startswith('link-relay:'):
|
if name.startswith('link-relay:'):
|
||||||
logger.warning('Link Relay has been deprecated.')
|
logger.warning('Link Relay has been deprecated.')
|
||||||
from ..controller import Controller
|
from bumble.controller import Controller
|
||||||
from ..link import RemoteLink # lazy import
|
from bumble.link import RemoteLink # lazy import
|
||||||
|
|
||||||
link = RemoteLink(name[11:])
|
link = RemoteLink(name[11:])
|
||||||
await link.wait_until_connected()
|
await link.wait_until_connected()
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import grpc.aio
|
|||||||
|
|
||||||
from typing import Optional, Union
|
from typing import Optional, Union
|
||||||
|
|
||||||
from .common import (
|
from bumble.transport.common import (
|
||||||
PumpedTransport,
|
PumpedTransport,
|
||||||
PumpedPacketSource,
|
PumpedPacketSource,
|
||||||
PumpedPacketSink,
|
PumpedPacketSink,
|
||||||
@@ -29,9 +29,13 @@ from .common import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
# pylint: disable=no-name-in-module
|
# pylint: disable=no-name-in-module
|
||||||
from .grpc_protobuf.emulated_bluetooth_pb2_grpc import EmulatedBluetoothServiceStub
|
from bumble.transport.grpc_protobuf.emulated_bluetooth_pb2_grpc import (
|
||||||
from .grpc_protobuf.emulated_bluetooth_packets_pb2 import HCIPacket
|
EmulatedBluetoothServiceStub,
|
||||||
from .grpc_protobuf.emulated_bluetooth_vhci_pb2_grpc import VhciForwardingServiceStub
|
)
|
||||||
|
from bumble.transport.grpc_protobuf.emulated_bluetooth_packets_pb2 import HCIPacket
|
||||||
|
from bumble.transport.grpc_protobuf.emulated_bluetooth_vhci_pb2_grpc import (
|
||||||
|
VhciForwardingServiceStub,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -38,15 +38,18 @@ from bumble.transport.common import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
# pylint: disable=no-name-in-module
|
# pylint: disable=no-name-in-module
|
||||||
from .grpc_protobuf.netsim.packet_streamer_pb2_grpc import (
|
from bumble.transport.grpc_protobuf.netsim.packet_streamer_pb2_grpc import (
|
||||||
PacketStreamerStub,
|
PacketStreamerStub,
|
||||||
PacketStreamerServicer,
|
PacketStreamerServicer,
|
||||||
add_PacketStreamerServicer_to_server,
|
add_PacketStreamerServicer_to_server,
|
||||||
)
|
)
|
||||||
from .grpc_protobuf.netsim.packet_streamer_pb2 import PacketRequest, PacketResponse
|
from bumble.transport.grpc_protobuf.netsim.packet_streamer_pb2 import (
|
||||||
from .grpc_protobuf.netsim.hci_packet_pb2 import HCIPacket
|
PacketRequest,
|
||||||
from .grpc_protobuf.netsim.startup_pb2 import Chip, ChipInfo, DeviceInfo
|
PacketResponse,
|
||||||
from .grpc_protobuf.netsim.common_pb2 import ChipKind
|
)
|
||||||
|
from bumble.transport.grpc_protobuf.netsim.hci_packet_pb2 import HCIPacket
|
||||||
|
from bumble.transport.grpc_protobuf.netsim.startup_pb2 import Chip, ChipInfo, DeviceInfo
|
||||||
|
from bumble.transport.grpc_protobuf.netsim.common_pb2 import ChipKind
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ class PacketParser:
|
|||||||
packet_type
|
packet_type
|
||||||
) or self.extended_packet_info.get(packet_type)
|
) or self.extended_packet_info.get(packet_type)
|
||||||
if self.packet_info is None:
|
if self.packet_info is None:
|
||||||
|
self.reset()
|
||||||
raise core.InvalidPacketError(
|
raise core.InvalidPacketError(
|
||||||
f'invalid packet type {packet_type}'
|
f'invalid packet type {packet_type}'
|
||||||
)
|
)
|
||||||
@@ -302,7 +303,10 @@ class ParserSource(BaseSource):
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class StreamPacketSource(asyncio.Protocol, ParserSource):
|
class StreamPacketSource(asyncio.Protocol, ParserSource):
|
||||||
def data_received(self, data: bytes) -> None:
|
def data_received(self, data: bytes) -> None:
|
||||||
self.parser.feed_data(data)
|
try:
|
||||||
|
self.parser.feed_data(data)
|
||||||
|
except core.InvalidPacketError:
|
||||||
|
logger.warning("invalid packet, ignoring data")
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import asyncio
|
|||||||
import io
|
import io
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from .common import Transport, StreamPacketSource, StreamPacketSink
|
from bumble.transport.common import Transport, StreamPacketSource, StreamPacketSink
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Logging
|
# Logging
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import collections
|
|||||||
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from .common import Transport, ParserSource
|
from bumble.transport.common import Transport, ParserSource
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import logging
|
|||||||
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from .common import Transport, StreamPacketSource, StreamPacketSink
|
from bumble.transport.common import Transport, StreamPacketSource, StreamPacketSink
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Logging
|
# Logging
|
||||||
|
|||||||
@@ -29,9 +29,9 @@ from usb.core import USBError
|
|||||||
from usb.util import CTRL_TYPE_CLASS, CTRL_RECIPIENT_OTHER
|
from usb.util import CTRL_TYPE_CLASS, CTRL_RECIPIENT_OTHER
|
||||||
from usb.legacy import REQ_SET_FEATURE, REQ_CLEAR_FEATURE, CLASS_HUB
|
from usb.legacy import REQ_SET_FEATURE, REQ_CLEAR_FEATURE, CLASS_HUB
|
||||||
|
|
||||||
from .common import Transport, ParserSource, TransportInitError
|
from bumble.transport.common import Transport, ParserSource, TransportInitError
|
||||||
from .. import hci
|
from bumble import hci
|
||||||
from ..colors import color
|
from bumble.colors import color
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
import serial_asyncio
|
import serial_asyncio
|
||||||
|
|
||||||
from .common import Transport, StreamPacketSource, StreamPacketSink
|
from bumble.transport.common import Transport, StreamPacketSource, StreamPacketSink
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Logging
|
# Logging
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from .common import Transport, StreamPacketSource, StreamPacketSink
|
from bumble.transport.common import Transport, StreamPacketSource, StreamPacketSink
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Logging
|
# Logging
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
from .common import Transport, StreamPacketSource
|
from bumble.transport.common import Transport, StreamPacketSource
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Logging
|
# Logging
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from .common import Transport, ParserSource
|
from bumble.transport.common import Transport, ParserSource
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Logging
|
# Logging
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from .common import Transport, StreamPacketSource, StreamPacketSink
|
from bumble.transport.common import Transport, StreamPacketSource, StreamPacketSink
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Logging
|
# Logging
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ import logging
|
|||||||
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from .common import Transport
|
from bumble.transport.common import Transport
|
||||||
from .file import open_file_transport
|
from bumble.transport.file import open_file_transport
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Logging
|
# Logging
|
||||||
|
|||||||
@@ -18,7 +18,12 @@
|
|||||||
import logging
|
import logging
|
||||||
import websockets.client
|
import websockets.client
|
||||||
|
|
||||||
from .common import PumpedPacketSource, PumpedPacketSink, PumpedTransport, Transport
|
from bumble.transport.common import (
|
||||||
|
PumpedPacketSource,
|
||||||
|
PumpedPacketSink,
|
||||||
|
PumpedTransport,
|
||||||
|
Transport,
|
||||||
|
)
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Logging
|
# Logging
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
import logging
|
import logging
|
||||||
import websockets
|
import websockets
|
||||||
|
|
||||||
from .common import Transport, ParserSource, PumpedPacketSink
|
from bumble.transport.common import Transport, ParserSource, PumpedPacketSink
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Logging
|
# Logging
|
||||||
|
|||||||
+92
-79
@@ -38,9 +38,10 @@ from typing import (
|
|||||||
)
|
)
|
||||||
from typing_extensions import Self
|
from typing_extensions import Self
|
||||||
|
|
||||||
from pyee import EventEmitter
|
import pyee
|
||||||
|
import pyee.asyncio
|
||||||
|
|
||||||
from .colors import color
|
from bumble.colors import color
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Logging
|
# Logging
|
||||||
@@ -56,6 +57,48 @@ def setup_event_forwarding(emitter, forwarder, event_name):
|
|||||||
emitter.on(event_name, emit)
|
emitter.on(event_name, emit)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def wrap_async(function):
|
||||||
|
"""
|
||||||
|
Wraps the provided function in an async function.
|
||||||
|
"""
|
||||||
|
return functools.partial(async_call, function)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def deprecated(msg: str):
|
||||||
|
"""
|
||||||
|
Throw deprecation warning before execution.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def wrapper(function):
|
||||||
|
@functools.wraps(function)
|
||||||
|
def inner(*args, **kwargs):
|
||||||
|
warnings.warn(msg, DeprecationWarning, stacklevel=2)
|
||||||
|
return function(*args, **kwargs)
|
||||||
|
|
||||||
|
return inner
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def experimental(msg: str):
|
||||||
|
"""
|
||||||
|
Throws a future warning before execution.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def wrapper(function):
|
||||||
|
@functools.wraps(function)
|
||||||
|
def inner(*args, **kwargs):
|
||||||
|
warnings.warn(msg, FutureWarning, stacklevel=2)
|
||||||
|
return function(*args, **kwargs)
|
||||||
|
|
||||||
|
return inner
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
def composite_listener(cls):
|
def composite_listener(cls):
|
||||||
"""
|
"""
|
||||||
@@ -113,21 +156,23 @@ class EventWatcher:
|
|||||||
```
|
```
|
||||||
'''
|
'''
|
||||||
|
|
||||||
handlers: List[Tuple[EventEmitter, str, Callable[..., Any]]]
|
handlers: List[Tuple[pyee.EventEmitter, str, Callable[..., Any]]]
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.handlers = []
|
self.handlers = []
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def on(
|
def on(
|
||||||
self, emitter: EventEmitter, event: str
|
self, emitter: pyee.EventEmitter, event: str
|
||||||
) -> Callable[[_Handler], _Handler]: ...
|
) -> Callable[[_Handler], _Handler]: ...
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def on(self, emitter: EventEmitter, event: str, handler: _Handler) -> _Handler: ...
|
def on(
|
||||||
|
self, emitter: pyee.EventEmitter, event: str, handler: _Handler
|
||||||
|
) -> _Handler: ...
|
||||||
|
|
||||||
def on(
|
def on(
|
||||||
self, emitter: EventEmitter, event: str, handler: Optional[_Handler] = None
|
self, emitter: pyee.EventEmitter, event: str, handler: Optional[_Handler] = None
|
||||||
) -> Union[_Handler, Callable[[_Handler], _Handler]]:
|
) -> Union[_Handler, Callable[[_Handler], _Handler]]:
|
||||||
'''Watch an event until the context is closed.
|
'''Watch an event until the context is closed.
|
||||||
|
|
||||||
@@ -147,16 +192,16 @@ class EventWatcher:
|
|||||||
|
|
||||||
@overload
|
@overload
|
||||||
def once(
|
def once(
|
||||||
self, emitter: EventEmitter, event: str
|
self, emitter: pyee.EventEmitter, event: str
|
||||||
) -> Callable[[_Handler], _Handler]: ...
|
) -> Callable[[_Handler], _Handler]: ...
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def once(
|
def once(
|
||||||
self, emitter: EventEmitter, event: str, handler: _Handler
|
self, emitter: pyee.EventEmitter, event: str, handler: _Handler
|
||||||
) -> _Handler: ...
|
) -> _Handler: ...
|
||||||
|
|
||||||
def once(
|
def once(
|
||||||
self, emitter: EventEmitter, event: str, handler: Optional[_Handler] = None
|
self, emitter: pyee.EventEmitter, event: str, handler: Optional[_Handler] = None
|
||||||
) -> Union[_Handler, Callable[[_Handler], _Handler]]:
|
) -> Union[_Handler, Callable[[_Handler], _Handler]]:
|
||||||
'''Watch an event for once.
|
'''Watch an event for once.
|
||||||
|
|
||||||
@@ -184,38 +229,48 @@ class EventWatcher:
|
|||||||
_T = TypeVar('_T')
|
_T = TypeVar('_T')
|
||||||
|
|
||||||
|
|
||||||
class AbortableEventEmitter(EventEmitter):
|
def cancel_on_event(
|
||||||
def abort_on(self, event: str, awaitable: Awaitable[_T]) -> Awaitable[_T]:
|
emitter: pyee.EventEmitter, event: str, awaitable: Awaitable[_T]
|
||||||
"""
|
) -> Awaitable[_T]:
|
||||||
Set a coroutine or future to abort when an event occur.
|
"""Set a coroutine or future to cancel when an event occur."""
|
||||||
"""
|
future = asyncio.ensure_future(awaitable)
|
||||||
future = asyncio.ensure_future(awaitable)
|
if future.done():
|
||||||
if future.done():
|
|
||||||
return future
|
|
||||||
|
|
||||||
def on_event(*_):
|
|
||||||
if future.done():
|
|
||||||
return
|
|
||||||
msg = f'abort: {event} event occurred.'
|
|
||||||
if isinstance(future, asyncio.Task):
|
|
||||||
# python < 3.9 does not support passing a message on `Task.cancel`
|
|
||||||
if sys.version_info < (3, 9, 0):
|
|
||||||
future.cancel()
|
|
||||||
else:
|
|
||||||
future.cancel(msg)
|
|
||||||
else:
|
|
||||||
future.set_exception(asyncio.CancelledError(msg))
|
|
||||||
|
|
||||||
def on_done(_):
|
|
||||||
self.remove_listener(event, on_event)
|
|
||||||
|
|
||||||
self.on(event, on_event)
|
|
||||||
future.add_done_callback(on_done)
|
|
||||||
return future
|
return future
|
||||||
|
|
||||||
|
def on_event(*args, **kwargs) -> None:
|
||||||
|
del args, kwargs
|
||||||
|
if future.done():
|
||||||
|
return
|
||||||
|
msg = f'abort: {event} event occurred.'
|
||||||
|
if isinstance(future, asyncio.Task):
|
||||||
|
# python < 3.9 does not support passing a message on `Task.cancel`
|
||||||
|
if sys.version_info < (3, 9, 0):
|
||||||
|
future.cancel()
|
||||||
|
else:
|
||||||
|
future.cancel(msg)
|
||||||
|
else:
|
||||||
|
future.set_exception(asyncio.CancelledError(msg))
|
||||||
|
|
||||||
|
def on_done(_):
|
||||||
|
emitter.remove_listener(event, on_event)
|
||||||
|
|
||||||
|
emitter.on(event, on_event)
|
||||||
|
future.add_done_callback(on_done)
|
||||||
|
return future
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class CompositeEventEmitter(AbortableEventEmitter):
|
class EventEmitter(pyee.asyncio.AsyncIOEventEmitter):
|
||||||
|
"""A Base EventEmitter for Bumble."""
|
||||||
|
|
||||||
|
@deprecated("Use `cancel_on_event` instead.")
|
||||||
|
def abort_on(self, event: str, awaitable: Awaitable[_T]) -> Awaitable[_T]:
|
||||||
|
"""Set a coroutine or future to abort when an event occur."""
|
||||||
|
return cancel_on_event(self, event, awaitable)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class CompositeEventEmitter(EventEmitter):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._listener = None
|
self._listener = None
|
||||||
@@ -430,48 +485,6 @@ async def async_call(function, *args, **kwargs):
|
|||||||
return function(*args, **kwargs)
|
return function(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
def wrap_async(function):
|
|
||||||
"""
|
|
||||||
Wraps the provided function in an async function.
|
|
||||||
"""
|
|
||||||
return functools.partial(async_call, function)
|
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
def deprecated(msg: str):
|
|
||||||
"""
|
|
||||||
Throw deprecation warning before execution.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def wrapper(function):
|
|
||||||
@functools.wraps(function)
|
|
||||||
def inner(*args, **kwargs):
|
|
||||||
warnings.warn(msg, DeprecationWarning, stacklevel=2)
|
|
||||||
return function(*args, **kwargs)
|
|
||||||
|
|
||||||
return inner
|
|
||||||
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
def experimental(msg: str):
|
|
||||||
"""
|
|
||||||
Throws a future warning before execution.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def wrapper(function):
|
|
||||||
@functools.wraps(function)
|
|
||||||
def inner(*args, **kwargs):
|
|
||||||
warnings.warn(msg, FutureWarning, stacklevel=2)
|
|
||||||
return function(*args, **kwargs)
|
|
||||||
|
|
||||||
return inner
|
|
||||||
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class OpenIntEnum(enum.IntEnum):
|
class OpenIntEnum(enum.IntEnum):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ from bumble.colors import color
|
|||||||
from bumble.device import Device
|
from bumble.device import Device
|
||||||
from bumble.transport import open_transport_or_link
|
from bumble.transport import open_transport_or_link
|
||||||
from bumble.core import (
|
from bumble.core import (
|
||||||
BT_BR_EDR_TRANSPORT,
|
PhysicalTransport,
|
||||||
BT_AVDTP_PROTOCOL_ID,
|
BT_AVDTP_PROTOCOL_ID,
|
||||||
BT_AUDIO_SINK_SERVICE,
|
BT_AUDIO_SINK_SERVICE,
|
||||||
BT_L2CAP_PROTOCOL_ID,
|
BT_L2CAP_PROTOCOL_ID,
|
||||||
@@ -165,7 +165,9 @@ async def main() -> None:
|
|||||||
# Connect to a peer
|
# Connect to a peer
|
||||||
target_address = sys.argv[3]
|
target_address = sys.argv[3]
|
||||||
print(f'=== Connecting to {target_address}...')
|
print(f'=== Connecting to {target_address}...')
|
||||||
connection = await device.connect(target_address, transport=BT_BR_EDR_TRANSPORT)
|
connection = await device.connect(
|
||||||
|
target_address, transport=PhysicalTransport.BR_EDR
|
||||||
|
)
|
||||||
print(f'=== Connected to {connection.peer_address}!')
|
print(f'=== Connected to {connection.peer_address}!')
|
||||||
|
|
||||||
# Request authentication
|
# Request authentication
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ from typing import Any, Dict
|
|||||||
|
|
||||||
from bumble.device import Device
|
from bumble.device import Device
|
||||||
from bumble.transport import open_transport_or_link
|
from bumble.transport import open_transport_or_link
|
||||||
from bumble.core import BT_BR_EDR_TRANSPORT
|
from bumble.core import PhysicalTransport
|
||||||
from bumble.avdtp import (
|
from bumble.avdtp import (
|
||||||
AVDTP_AUDIO_MEDIA_TYPE,
|
AVDTP_AUDIO_MEDIA_TYPE,
|
||||||
Protocol,
|
Protocol,
|
||||||
@@ -33,12 +33,6 @@ from bumble.avdtp import (
|
|||||||
from bumble.a2dp import (
|
from bumble.a2dp import (
|
||||||
make_audio_sink_service_sdp_records,
|
make_audio_sink_service_sdp_records,
|
||||||
A2DP_SBC_CODEC_TYPE,
|
A2DP_SBC_CODEC_TYPE,
|
||||||
SBC_MONO_CHANNEL_MODE,
|
|
||||||
SBC_DUAL_CHANNEL_MODE,
|
|
||||||
SBC_SNR_ALLOCATION_METHOD,
|
|
||||||
SBC_LOUDNESS_ALLOCATION_METHOD,
|
|
||||||
SBC_STEREO_CHANNEL_MODE,
|
|
||||||
SBC_JOINT_STEREO_CHANNEL_MODE,
|
|
||||||
SbcMediaCodecInformation,
|
SbcMediaCodecInformation,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -145,7 +139,7 @@ async def main() -> None:
|
|||||||
target_address = sys.argv[4]
|
target_address = sys.argv[4]
|
||||||
print(f'=== Connecting to {target_address}...')
|
print(f'=== Connecting to {target_address}...')
|
||||||
connection = await device.connect(
|
connection = await device.connect(
|
||||||
target_address, transport=BT_BR_EDR_TRANSPORT
|
target_address, transport=PhysicalTransport.BR_EDR
|
||||||
)
|
)
|
||||||
print(f'=== Connected to {connection.peer_address}!')
|
print(f'=== Connected to {connection.peer_address}!')
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import logging
|
|||||||
from bumble.colors import color
|
from bumble.colors import color
|
||||||
from bumble.device import Device
|
from bumble.device import Device
|
||||||
from bumble.transport import open_transport_or_link
|
from bumble.transport import open_transport_or_link
|
||||||
from bumble.core import BT_BR_EDR_TRANSPORT
|
from bumble.core import PhysicalTransport
|
||||||
from bumble.avdtp import (
|
from bumble.avdtp import (
|
||||||
find_avdtp_service_with_connection,
|
find_avdtp_service_with_connection,
|
||||||
AVDTP_AUDIO_MEDIA_TYPE,
|
AVDTP_AUDIO_MEDIA_TYPE,
|
||||||
@@ -146,7 +146,7 @@ async def main() -> None:
|
|||||||
target_address = sys.argv[4]
|
target_address = sys.argv[4]
|
||||||
print(f'=== Connecting to {target_address}...')
|
print(f'=== Connecting to {target_address}...')
|
||||||
connection = await device.connect(
|
connection = await device.connect(
|
||||||
target_address, transport=BT_BR_EDR_TRANSPORT
|
target_address, transport=PhysicalTransport.BR_EDR
|
||||||
)
|
)
|
||||||
print(f'=== Connected to {connection.peer_address}!')
|
print(f'=== Connected to {connection.peer_address}!')
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import websockets
|
|||||||
|
|
||||||
from bumble.device import Device
|
from bumble.device import Device
|
||||||
from bumble.transport import open_transport_or_link
|
from bumble.transport import open_transport_or_link
|
||||||
from bumble.core import BT_BR_EDR_TRANSPORT
|
from bumble.core import PhysicalTransport
|
||||||
from bumble import avc
|
from bumble import avc
|
||||||
from bumble import avrcp
|
from bumble import avrcp
|
||||||
from bumble import avdtp
|
from bumble import avdtp
|
||||||
@@ -379,7 +379,7 @@ async def main() -> None:
|
|||||||
target_address = sys.argv[4]
|
target_address = sys.argv[4]
|
||||||
print(f'=== Connecting to {target_address}...')
|
print(f'=== Connecting to {target_address}...')
|
||||||
connection = await device.connect(
|
connection = await device.connect(
|
||||||
target_address, transport=BT_BR_EDR_TRANSPORT
|
target_address, transport=PhysicalTransport.BR_EDR
|
||||||
)
|
)
|
||||||
print(f'=== Connected to {connection.peer_address}!')
|
print(f'=== Connected to {connection.peer_address}!')
|
||||||
|
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ async def main() -> None:
|
|||||||
|
|
||||||
print(f'<<< Connecting to {target_address}')
|
print(f'<<< Connecting to {target_address}')
|
||||||
connection = await device.connect(
|
connection = await device.connect(
|
||||||
target_address, transport=core.BT_LE_TRANSPORT
|
target_address, transport=core.PhysicalTransport.LE
|
||||||
)
|
)
|
||||||
print('<<< ACL Connected')
|
print('<<< ACL Connected')
|
||||||
if not (await device.get_long_term_key(connection.handle, b'', 0)):
|
if not (await device.get_long_term_key(connection.handle, b'', 0)):
|
||||||
|
|||||||
@@ -19,12 +19,8 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
from bumble.device import (
|
from bumble import utils
|
||||||
Device,
|
from bumble.device import Device, Connection
|
||||||
Connection,
|
|
||||||
AdvertisingParameters,
|
|
||||||
AdvertisingEventProperties,
|
|
||||||
)
|
|
||||||
from bumble.hci import (
|
from bumble.hci import (
|
||||||
OwnAddressType,
|
OwnAddressType,
|
||||||
)
|
)
|
||||||
@@ -79,7 +75,9 @@ async def main() -> None:
|
|||||||
def on_cis_request(
|
def on_cis_request(
|
||||||
connection: Connection, cis_handle: int, _cig_id: int, _cis_id: int
|
connection: Connection, cis_handle: int, _cig_id: int, _cis_id: int
|
||||||
):
|
):
|
||||||
connection.abort_on('disconnection', devices[0].accept_cis_request(cis_handle))
|
utils.cancel_on_event(
|
||||||
|
connection, 'disconnection', devices[0].accept_cis_request(cis_handle)
|
||||||
|
)
|
||||||
|
|
||||||
devices[0].on('cis_request', on_cis_request)
|
devices[0].on('cis_request', on_cis_request)
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ from bumble.colors import color
|
|||||||
|
|
||||||
from bumble.device import Device
|
from bumble.device import Device
|
||||||
from bumble.transport import open_transport_or_link
|
from bumble.transport import open_transport_or_link
|
||||||
from bumble.core import BT_BR_EDR_TRANSPORT, BT_L2CAP_PROTOCOL_ID, CommandTimeoutError
|
from bumble.core import PhysicalTransport, BT_L2CAP_PROTOCOL_ID, CommandTimeoutError
|
||||||
from bumble.sdp import (
|
from bumble.sdp import (
|
||||||
Client as SDP_Client,
|
Client as SDP_Client,
|
||||||
SDP_PUBLIC_BROWSE_ROOT,
|
SDP_PUBLIC_BROWSE_ROOT,
|
||||||
@@ -57,7 +57,7 @@ async def main() -> None:
|
|||||||
print(f'=== Connecting to {target_address}...')
|
print(f'=== Connecting to {target_address}...')
|
||||||
try:
|
try:
|
||||||
connection = await device.connect(
|
connection = await device.connect(
|
||||||
target_address, transport=BT_BR_EDR_TRANSPORT
|
target_address, transport=PhysicalTransport.BR_EDR
|
||||||
)
|
)
|
||||||
except CommandTimeoutError:
|
except CommandTimeoutError:
|
||||||
print('!!! Connection timed out')
|
print('!!! Connection timed out')
|
||||||
|
|||||||
@@ -16,11 +16,10 @@
|
|||||||
# Imports
|
# Imports
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
import asyncio
|
import asyncio
|
||||||
import dataclasses
|
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
from bumble.core import BT_BR_EDR_TRANSPORT
|
from bumble.core import PhysicalTransport
|
||||||
from bumble.device import Device, ScoLink
|
from bumble.device import Device, ScoLink
|
||||||
from bumble.hci import HCI_Enhanced_Setup_Synchronous_Connection_Command
|
from bumble.hci import HCI_Enhanced_Setup_Synchronous_Connection_Command
|
||||||
from bumble.hfp import DefaultCodecParameters, ESCO_PARAMETERS
|
from bumble.hfp import DefaultCodecParameters, ESCO_PARAMETERS
|
||||||
@@ -61,7 +60,9 @@ async def main() -> None:
|
|||||||
|
|
||||||
connections = await asyncio.gather(
|
connections = await asyncio.gather(
|
||||||
devices[0].accept(devices[1].public_address),
|
devices[0].accept(devices[1].public_address),
|
||||||
devices[1].connect(devices[0].public_address, transport=BT_BR_EDR_TRANSPORT),
|
devices[1].connect(
|
||||||
|
devices[0].public_address, transport=PhysicalTransport.BR_EDR
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_sco(sco_link: ScoLink):
|
def on_sco(sco_link: ScoLink):
|
||||||
|
|||||||
@@ -28,9 +28,7 @@ import websockets
|
|||||||
import bumble.core
|
import bumble.core
|
||||||
from bumble.device import Device, ScoLink
|
from bumble.device import Device, ScoLink
|
||||||
from bumble.transport import open_transport_or_link
|
from bumble.transport import open_transport_or_link
|
||||||
from bumble.core import (
|
from bumble.core import PhysicalTransport
|
||||||
BT_BR_EDR_TRANSPORT,
|
|
||||||
)
|
|
||||||
from bumble import hci, rfcomm, hfp
|
from bumble import hci, rfcomm, hfp
|
||||||
|
|
||||||
|
|
||||||
@@ -234,7 +232,7 @@ async def main() -> None:
|
|||||||
target_address = sys.argv[3]
|
target_address = sys.argv[3]
|
||||||
print(f'=== Connecting to {target_address}...')
|
print(f'=== Connecting to {target_address}...')
|
||||||
connection = await device.connect(
|
connection = await device.connect(
|
||||||
target_address, transport=BT_BR_EDR_TRANSPORT
|
target_address, transport=PhysicalTransport.BR_EDR
|
||||||
)
|
)
|
||||||
print(f'=== Connected to {connection.peer_address}!')
|
print(f'=== Connected to {connection.peer_address}!')
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import websockets
|
|||||||
import functools
|
import functools
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
from bumble import utils
|
||||||
from bumble import rfcomm
|
from bumble import rfcomm
|
||||||
from bumble import hci
|
from bumble import hci
|
||||||
from bumble.device import Device, Connection
|
from bumble.device import Device, Connection
|
||||||
@@ -60,7 +61,8 @@ def on_dlc(dlc: rfcomm.DLC, configuration: hfp.HfConfiguration):
|
|||||||
else:
|
else:
|
||||||
raise RuntimeError("unknown active codec")
|
raise RuntimeError("unknown active codec")
|
||||||
|
|
||||||
connection.abort_on(
|
utils.cancel_on_event(
|
||||||
|
connection,
|
||||||
'disconnection',
|
'disconnection',
|
||||||
connection.device.send_command(
|
connection.device.send_command(
|
||||||
hci.HCI_Enhanced_Accept_Synchronous_Connection_Request_Command(
|
hci.HCI_Enhanced_Accept_Synchronous_Connection_Request_Command(
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import struct
|
|||||||
from bumble.device import Device
|
from bumble.device import Device
|
||||||
from bumble.transport import open_transport_or_link
|
from bumble.transport import open_transport_or_link
|
||||||
from bumble.core import (
|
from bumble.core import (
|
||||||
BT_BR_EDR_TRANSPORT,
|
PhysicalTransport,
|
||||||
BT_L2CAP_PROTOCOL_ID,
|
BT_L2CAP_PROTOCOL_ID,
|
||||||
BT_HUMAN_INTERFACE_DEVICE_SERVICE,
|
BT_HUMAN_INTERFACE_DEVICE_SERVICE,
|
||||||
BT_HIDP_PROTOCOL_ID,
|
BT_HIDP_PROTOCOL_ID,
|
||||||
@@ -721,7 +721,7 @@ async def main() -> None:
|
|||||||
elif choice == '9':
|
elif choice == '9':
|
||||||
hid_host_bd_addr = str(hid_device.remote_device_bd_address)
|
hid_host_bd_addr = str(hid_device.remote_device_bd_address)
|
||||||
connection = await device.connect(
|
connection = await device.connect(
|
||||||
hid_host_bd_addr, transport=BT_BR_EDR_TRANSPORT
|
hid_host_bd_addr, transport=PhysicalTransport.BR_EDR
|
||||||
)
|
)
|
||||||
await connection.authenticate()
|
await connection.authenticate()
|
||||||
await connection.encrypt()
|
await connection.encrypt()
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ from bumble.device import Device
|
|||||||
from bumble.transport import open_transport_or_link
|
from bumble.transport import open_transport_or_link
|
||||||
from bumble.core import (
|
from bumble.core import (
|
||||||
BT_HUMAN_INTERFACE_DEVICE_SERVICE,
|
BT_HUMAN_INTERFACE_DEVICE_SERVICE,
|
||||||
BT_BR_EDR_TRANSPORT,
|
PhysicalTransport,
|
||||||
)
|
)
|
||||||
from bumble.hci import Address
|
from bumble.hci import Address
|
||||||
from bumble.hid import Host, Message
|
from bumble.hid import Host, Message
|
||||||
@@ -349,7 +349,9 @@ async def main() -> None:
|
|||||||
# Connect to a peer
|
# Connect to a peer
|
||||||
target_address = sys.argv[3]
|
target_address = sys.argv[3]
|
||||||
print(f'=== Connecting to {target_address}...')
|
print(f'=== Connecting to {target_address}...')
|
||||||
connection = await device.connect(target_address, transport=BT_BR_EDR_TRANSPORT)
|
connection = await device.connect(
|
||||||
|
target_address, transport=PhysicalTransport.BR_EDR
|
||||||
|
)
|
||||||
print(f'=== Connected to {connection.peer_address}!')
|
print(f'=== Connected to {connection.peer_address}!')
|
||||||
|
|
||||||
# Request authentication
|
# Request authentication
|
||||||
@@ -519,10 +521,10 @@ async def main() -> None:
|
|||||||
|
|
||||||
elif choice == '13':
|
elif choice == '13':
|
||||||
peer_address = Address.from_string_for_transport(
|
peer_address = Address.from_string_for_transport(
|
||||||
target_address, transport=BT_BR_EDR_TRANSPORT
|
target_address, transport=PhysicalTransport.BR_EDR
|
||||||
)
|
)
|
||||||
connection = device.find_connection_by_bd_addr(
|
connection = device.find_connection_by_bd_addr(
|
||||||
peer_address, transport=BT_BR_EDR_TRANSPORT
|
peer_address, transport=PhysicalTransport.BR_EDR
|
||||||
)
|
)
|
||||||
if connection is not None:
|
if connection is not None:
|
||||||
await connection.disconnect()
|
await connection.disconnect()
|
||||||
@@ -538,7 +540,7 @@ async def main() -> None:
|
|||||||
|
|
||||||
elif choice == '15':
|
elif choice == '15':
|
||||||
connection = await device.connect(
|
connection = await device.connect(
|
||||||
target_address, transport=BT_BR_EDR_TRANSPORT
|
target_address, transport=PhysicalTransport.BR_EDR
|
||||||
)
|
)
|
||||||
await connection.authenticate()
|
await connection.authenticate()
|
||||||
await connection.encrypt()
|
await connection.encrypt()
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import os
|
|||||||
import websockets
|
import websockets
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
from bumble import utils
|
||||||
from bumble.core import AdvertisingData
|
from bumble.core import AdvertisingData
|
||||||
from bumble.device import (
|
from bumble.device import (
|
||||||
Device,
|
Device,
|
||||||
@@ -169,7 +170,7 @@ async def main() -> None:
|
|||||||
mcp.on('track_position', on_track_position)
|
mcp.on('track_position', on_track_position)
|
||||||
await mcp.subscribe_characteristics()
|
await mcp.subscribe_characteristics()
|
||||||
|
|
||||||
connection.abort_on('disconnection', on_connection_async())
|
utils.cancel_on_event(connection, 'disconnection', on_connection_async())
|
||||||
|
|
||||||
device.on('connection', on_connection)
|
device.on('connection', on_connection)
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ from bumble.transport import open_transport_or_link
|
|||||||
from bumble.core import (
|
from bumble.core import (
|
||||||
BT_L2CAP_PROTOCOL_ID,
|
BT_L2CAP_PROTOCOL_ID,
|
||||||
BT_RFCOMM_PROTOCOL_ID,
|
BT_RFCOMM_PROTOCOL_ID,
|
||||||
BT_BR_EDR_TRANSPORT,
|
PhysicalTransport,
|
||||||
)
|
)
|
||||||
from bumble.rfcomm import Client
|
from bumble.rfcomm import Client
|
||||||
from bumble.sdp import (
|
from bumble.sdp import (
|
||||||
@@ -191,7 +191,9 @@ async def main() -> None:
|
|||||||
# Connect to a peer
|
# Connect to a peer
|
||||||
target_address = sys.argv[3]
|
target_address = sys.argv[3]
|
||||||
print(f'=== Connecting to {target_address}...')
|
print(f'=== Connecting to {target_address}...')
|
||||||
connection = await device.connect(target_address, transport=BT_BR_EDR_TRANSPORT)
|
connection = await device.connect(
|
||||||
|
target_address, transport=PhysicalTransport.BR_EDR
|
||||||
|
)
|
||||||
print(f'=== Connected to {connection.peer_address}!')
|
print(f'=== Connected to {connection.peer_address}!')
|
||||||
|
|
||||||
channel_str = sys.argv[4]
|
channel_str = sys.argv[4]
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
*.iml
|
*.iml
|
||||||
.gradle
|
.gradle
|
||||||
/local.properties
|
/local.properties
|
||||||
/.idea/caches
|
.idea/
|
||||||
/.idea/libraries
|
|
||||||
/.idea/modules.xml
|
|
||||||
/.idea/workspace.xml
|
|
||||||
/.idea/navEditor.xml
|
|
||||||
/.idea/assetWizardSettings.xml
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
/build
|
/build
|
||||||
/captures
|
/captures
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<!-- Request legacy Bluetooth permissions on older devices. -->
|
<!-- Request legacy Bluetooth permissions on older devices. -->
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
|
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
|
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation"/>
|
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation"/>
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
|
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
|
||||||
|
|||||||
+3
-1
@@ -6,13 +6,14 @@ import android.bluetooth.le.AdvertiseCallback
|
|||||||
import android.bluetooth.le.AdvertiseData
|
import android.bluetooth.le.AdvertiseData
|
||||||
import android.bluetooth.le.AdvertiseSettings
|
import android.bluetooth.le.AdvertiseSettings
|
||||||
import android.bluetooth.le.AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY
|
import android.bluetooth.le.AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY
|
||||||
import android.os.Build
|
import androidx.annotation.RequiresApi
|
||||||
import java.util.logging.Logger
|
import java.util.logging.Logger
|
||||||
|
|
||||||
private val Log = Logger.getLogger("btbench.advertiser")
|
private val Log = Logger.getLogger("btbench.advertiser")
|
||||||
|
|
||||||
class Advertiser(private val bluetoothAdapter: BluetoothAdapter) : AdvertiseCallback() {
|
class Advertiser(private val bluetoothAdapter: BluetoothAdapter) : AdvertiseCallback() {
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
|
@RequiresApi(34)
|
||||||
fun start() {
|
fun start() {
|
||||||
val advertiseSettingsBuilder = AdvertiseSettings.Builder()
|
val advertiseSettingsBuilder = AdvertiseSettings.Builder()
|
||||||
.setAdvertiseMode(ADVERTISE_MODE_LOW_LATENCY)
|
.setAdvertiseMode(ADVERTISE_MODE_LOW_LATENCY)
|
||||||
@@ -26,6 +27,7 @@ class Advertiser(private val bluetoothAdapter: BluetoothAdapter) : AdvertiseCall
|
|||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
fun stop() {
|
fun stop() {
|
||||||
|
Log.info("stopping advertiser")
|
||||||
bluetoothAdapter.bluetoothLeAdvertiser.stopAdvertising(this)
|
bluetoothAdapter.bluetoothLeAdvertiser.stopAdvertising(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
@@ -26,6 +26,8 @@ import android.bluetooth.BluetoothGattService
|
|||||||
import android.bluetooth.BluetoothManager
|
import android.bluetooth.BluetoothManager
|
||||||
import android.bluetooth.BluetoothStatusCodes
|
import android.bluetooth.BluetoothStatusCodes
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
@@ -174,6 +176,7 @@ class GattServer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
override fun run() {
|
override fun run() {
|
||||||
viewModel.running = true
|
viewModel.running = true
|
||||||
|
|
||||||
|
|||||||
+2
-6
@@ -16,14 +16,9 @@ package com.github.google.bumble.btbench
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.bluetooth.BluetoothAdapter
|
import android.bluetooth.BluetoothAdapter
|
||||||
import android.bluetooth.le.AdvertiseCallback
|
|
||||||
import android.bluetooth.le.AdvertiseData
|
|
||||||
import android.bluetooth.le.AdvertiseSettings
|
|
||||||
import android.bluetooth.le.AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import java.io.IOException
|
import androidx.annotation.RequiresApi
|
||||||
import java.util.logging.Logger
|
import java.util.logging.Logger
|
||||||
import kotlin.concurrent.thread
|
|
||||||
|
|
||||||
private val Log = Logger.getLogger("btbench.l2cap-server")
|
private val Log = Logger.getLogger("btbench.l2cap-server")
|
||||||
|
|
||||||
@@ -34,6 +29,7 @@ class L2capServer(
|
|||||||
) : Mode {
|
) : Mode {
|
||||||
private var socketServer: SocketServer? = null
|
private var socketServer: SocketServer? = null
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
override fun run() {
|
override fun run() {
|
||||||
// Advertise so that the peer can find us and connect.
|
// Advertise so that the peer can find us and connect.
|
||||||
|
|||||||
+4
-2
@@ -381,7 +381,7 @@ fun MainView(
|
|||||||
label = {
|
label = {
|
||||||
Text(text = "Packet Interval (ms)")
|
Text(text = "Packet Interval (ms)")
|
||||||
},
|
},
|
||||||
value = appViewModel.senderPacketInterval.toString(),
|
value = (if (appViewModel.senderPacketInterval != 0) appViewModel.senderPacketInterval else "").toString(),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.focusRequester(focusRequester),
|
.focusRequester(focusRequester),
|
||||||
@@ -389,7 +389,9 @@ fun MainView(
|
|||||||
keyboardType = KeyboardType.Number, imeAction = ImeAction.Done
|
keyboardType = KeyboardType.Number, imeAction = ImeAction.Done
|
||||||
),
|
),
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
if (it.isNotEmpty()) {
|
if (it.isEmpty()) {
|
||||||
|
appViewModel.updateSenderPacketInterval(0)
|
||||||
|
} else {
|
||||||
val interval = it.toIntOrNull()
|
val interval = it.toIntOrNull()
|
||||||
if (interval != null) {
|
if (interval != null) {
|
||||||
appViewModel.updateSenderPacketInterval(interval)
|
appViewModel.updateSenderPacketInterval(interval)
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ val DEFAULT_RFCOMM_UUID: UUID = UUID.fromString("E6D55659-C8B4-4B85-96BB-B1143AF
|
|||||||
const val DEFAULT_PEER_BLUETOOTH_ADDRESS = "AA:BB:CC:DD:EE:FF"
|
const val DEFAULT_PEER_BLUETOOTH_ADDRESS = "AA:BB:CC:DD:EE:FF"
|
||||||
const val DEFAULT_STARTUP_DELAY = 3000
|
const val DEFAULT_STARTUP_DELAY = 3000
|
||||||
const val DEFAULT_SENDER_PACKET_COUNT = 100
|
const val DEFAULT_SENDER_PACKET_COUNT = 100
|
||||||
const val DEFAULT_SENDER_PACKET_SIZE = 1024
|
const val DEFAULT_SENDER_PACKET_SIZE = 970 // 970 is a value that works well on Android.
|
||||||
const val DEFAULT_SENDER_PACKET_INTERVAL = 100
|
const val DEFAULT_SENDER_PACKET_INTERVAL = 0
|
||||||
const val DEFAULT_PSM = 128
|
const val DEFAULT_PSM = 128
|
||||||
|
|
||||||
const val L2CAP_CLIENT_MODE = "L2CAP Client"
|
const val L2CAP_CLIENT_MODE = "L2CAP Client"
|
||||||
@@ -192,7 +192,6 @@ class AppViewModel : ViewModel() {
|
|||||||
} else if (senderPacketSizeSlider < 0.5F) {
|
} else if (senderPacketSizeSlider < 0.5F) {
|
||||||
512
|
512
|
||||||
} else if (senderPacketSizeSlider < 0.7F) {
|
} else if (senderPacketSizeSlider < 0.7F) {
|
||||||
// 970 is a value that works well on Android.
|
|
||||||
970
|
970
|
||||||
} else if (senderPacketSizeSlider < 0.9F) {
|
} else if (senderPacketSizeSlider < 0.9F) {
|
||||||
2048
|
2048
|
||||||
|
|||||||
+3
-3
@@ -13,11 +13,11 @@ dependencies = [
|
|||||||
"aiohttp ~= 3.8; platform_system!='Emscripten'",
|
"aiohttp ~= 3.8; platform_system!='Emscripten'",
|
||||||
"appdirs >= 1.4; platform_system!='Emscripten'",
|
"appdirs >= 1.4; platform_system!='Emscripten'",
|
||||||
"click >= 8.1.3; platform_system!='Emscripten'",
|
"click >= 8.1.3; platform_system!='Emscripten'",
|
||||||
"cryptography >= 39; platform_system!='Emscripten'",
|
"cryptography >= 44.0.3; platform_system!='Emscripten'",
|
||||||
# Pyodide bundles a version of cryptography that is built for wasm, which may not match the
|
# Pyodide bundles a version of cryptography that is built for wasm, which may not match the
|
||||||
# versions available on PyPI. Relax the version requirement since it's better than being
|
# versions available on PyPI. Relax the version requirement since it's better than being
|
||||||
# completely unable to import the package in case of version mismatch.
|
# completely unable to import the package in case of version mismatch.
|
||||||
"cryptography >= 39.0; platform_system=='Emscripten'",
|
"cryptography >= 44.0.3; platform_system=='Emscripten'",
|
||||||
"grpcio >= 1.62.1; platform_system!='Emscripten'",
|
"grpcio >= 1.62.1; platform_system!='Emscripten'",
|
||||||
"humanize >= 4.6.0; platform_system!='Emscripten'",
|
"humanize >= 4.6.0; platform_system!='Emscripten'",
|
||||||
"libusb1 >= 2.0.1; platform_system!='Emscripten'",
|
"libusb1 >= 2.0.1; platform_system!='Emscripten'",
|
||||||
@@ -26,7 +26,7 @@ dependencies = [
|
|||||||
"prompt_toolkit >= 3.0.16; platform_system!='Emscripten'",
|
"prompt_toolkit >= 3.0.16; platform_system!='Emscripten'",
|
||||||
"prettytable >= 3.6.0; platform_system!='Emscripten'",
|
"prettytable >= 3.6.0; platform_system!='Emscripten'",
|
||||||
"protobuf >= 3.12.4; platform_system!='Emscripten'",
|
"protobuf >= 3.12.4; platform_system!='Emscripten'",
|
||||||
"pyee >= 8.2.2",
|
"pyee >= 13.0.0",
|
||||||
"pyserial-asyncio >= 0.5; platform_system!='Emscripten'",
|
"pyserial-asyncio >= 0.5; platform_system!='Emscripten'",
|
||||||
"pyserial >= 3.5; platform_system!='Emscripten'",
|
"pyserial >= 3.5; platform_system!='Emscripten'",
|
||||||
"pyusb >= 1.2; platform_system!='Emscripten'",
|
"pyusb >= 1.2; platform_system!='Emscripten'",
|
||||||
|
|||||||
+3
-3
@@ -21,7 +21,7 @@ import os
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from bumble.controller import Controller
|
from bumble.controller import Controller
|
||||||
from bumble.core import BT_BR_EDR_TRANSPORT
|
from bumble.core import PhysicalTransport
|
||||||
from bumble.link import LocalLink
|
from bumble.link import LocalLink
|
||||||
from bumble.device import Device
|
from bumble.device import Device
|
||||||
from bumble.host import Host
|
from bumble.host import Host
|
||||||
@@ -106,7 +106,7 @@ async def test_self_connection():
|
|||||||
# Connect the two devices
|
# Connect the two devices
|
||||||
await asyncio.gather(
|
await asyncio.gather(
|
||||||
two_devices.devices[0].connect(
|
two_devices.devices[0].connect(
|
||||||
two_devices.devices[1].public_address, transport=BT_BR_EDR_TRANSPORT
|
two_devices.devices[1].public_address, transport=PhysicalTransport.BR_EDR
|
||||||
),
|
),
|
||||||
two_devices.devices[1].accept(two_devices.devices[0].public_address),
|
two_devices.devices[1].accept(two_devices.devices[0].public_address),
|
||||||
)
|
)
|
||||||
@@ -190,7 +190,7 @@ async def test_source_sink_1():
|
|||||||
async def make_connection():
|
async def make_connection():
|
||||||
connections = await asyncio.gather(
|
connections = await asyncio.gather(
|
||||||
two_devices.devices[0].connect(
|
two_devices.devices[0].connect(
|
||||||
two_devices.devices[1].public_address, BT_BR_EDR_TRANSPORT
|
two_devices.devices[1].public_address, PhysicalTransport.BR_EDR
|
||||||
),
|
),
|
||||||
two_devices.devices[1].accept(two_devices.devices[0].public_address),
|
two_devices.devices[1].accept(two_devices.devices[0].public_address),
|
||||||
)
|
)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user