forked from auracaster/bumble_mirror
Compare commits
69 Commits
v0.0.213
...
revert-771
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca23d6b89a | ||
|
|
d86d69d816 | ||
|
|
dc93f32a9a | ||
|
|
9838908a26 | ||
|
|
613519f0b3 | ||
|
|
a943ea57ef | ||
|
|
14401910bb | ||
|
|
5d35ed471c | ||
|
|
c720ad5fdc | ||
|
|
f02183f95d | ||
|
|
d903937a51 | ||
|
|
6381ee0ab1 | ||
|
|
59d99780e1 | ||
|
|
4bf0bc03af | ||
|
|
91ba2f61f1 | ||
|
|
116dc9b319 | ||
|
|
9f3d8c9b49 | ||
|
|
31961febe5 | ||
|
|
dab0993cba | ||
|
|
6f73b736d7 | ||
|
|
6091e6365d | ||
|
|
3333ba472b | ||
|
|
8bda7d2212 | ||
|
|
7aba36302a | ||
|
|
ceefe8b2a5 | ||
|
|
cd37027795 | ||
|
|
bb2aa8229d | ||
|
|
4aed53c48d | ||
|
|
4a88e9a0cf | ||
|
|
3b8dd6f3cf | ||
|
|
f41b7746d2 | ||
|
|
1b727741bf | ||
|
|
d2bc8175fb | ||
|
|
84dfff290a | ||
|
|
17563e423a | ||
|
|
19d3616032 | ||
|
|
4a48309643 | ||
|
|
870217acb3 | ||
|
|
f8077d7996 | ||
|
|
739907fa31 | ||
|
|
a275c399a3 | ||
|
|
c98275f385 | ||
|
|
0b19347bef | ||
|
|
f61fd64c0b | ||
|
|
ec12771be6 | ||
|
|
5b33e715da | ||
|
|
b885f29318 | ||
|
|
7ca13188d5 | ||
|
|
89586d5d18 | ||
|
|
381032ceb9 | ||
|
|
12ca1c01f0 | ||
|
|
a7111d0107 | ||
|
|
c034297bc0 | ||
|
|
a1eff958e6 | ||
|
|
d6282a7247 | ||
|
|
efdc770fde | ||
|
|
357d7f9c22 | ||
|
|
3bc08b4e0d | ||
|
|
982aaeabc3 | ||
|
|
1dc0950177 | ||
|
|
df0fd74533 | ||
|
|
822f97fa84 | ||
|
|
4a6b0ef840 | ||
|
|
a6ead0147e | ||
|
|
0665e9ca5c | ||
|
|
b8b78ca1ee | ||
|
|
d611d25802 | ||
|
|
bf8a2cdcb5 | ||
|
|
4bf7448a01 |
2
.github/workflows/code-check.yml
vendored
2
.github/workflows/code-check.yml
vendored
@@ -6,6 +6,8 @@ on:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
workflow_dispatch:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@@ -17,6 +17,8 @@ on:
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ main ]
|
||||
workflow_dispatch:
|
||||
branches: [main]
|
||||
schedule:
|
||||
- cron: '39 21 * * 4'
|
||||
|
||||
|
||||
4
.github/workflows/gradle-btbench.yml
vendored
4
.github/workflows/gradle-btbench.yml
vendored
@@ -7,6 +7,10 @@ on:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- 'extras/android/BtBench/**'
|
||||
workflow_dispatch:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'extras/android/BtBench/**'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
2
.github/workflows/python-avatar.yml
vendored
2
.github/workflows/python-avatar.yml
vendored
@@ -5,6 +5,8 @@ on:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
workflow_dispatch:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
2
.github/workflows/python-build-test.yml
vendored
2
.github/workflows/python-build-test.yml
vendored
@@ -6,6 +6,8 @@ on:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
workflow_dispatch:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
@@ -102,5 +102,10 @@
|
||||
"."
|
||||
],
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.pytestEnabled": true
|
||||
"python.testing.pytestEnabled": true,
|
||||
"python-envs.defaultEnvManager": "ms-python.python:system",
|
||||
"python-envs.pythonProjects": [],
|
||||
"nrf-connect.applications": [
|
||||
"${workspaceFolder}/extras/zephyr/hci_usb"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -23,14 +23,8 @@ import contextlib
|
||||
import dataclasses
|
||||
import functools
|
||||
import logging
|
||||
import os
|
||||
import struct
|
||||
from typing import (
|
||||
Any,
|
||||
AsyncGenerator,
|
||||
Coroutine,
|
||||
Optional,
|
||||
)
|
||||
from typing import Any, AsyncGenerator, Coroutine, Optional
|
||||
|
||||
import click
|
||||
|
||||
@@ -41,19 +35,14 @@ except ImportError as e:
|
||||
"Try `python -m pip install \"git+https://github.com/google/liblc3.git\"`."
|
||||
) from e
|
||||
|
||||
from bumble.audio import io as audio_io
|
||||
from bumble.colors import color
|
||||
from bumble import company_ids
|
||||
from bumble import core
|
||||
from bumble import gatt
|
||||
from bumble import hci
|
||||
from bumble.profiles import bap
|
||||
from bumble.profiles import le_audio
|
||||
from bumble.profiles import pbp
|
||||
from bumble.profiles import bass
|
||||
import bumble.device
|
||||
import bumble.logging
|
||||
import bumble.transport
|
||||
import bumble.utils
|
||||
from bumble import company_ids, core, data_types, gatt, hci
|
||||
from bumble.audio import io as audio_io
|
||||
from bumble.colors import color
|
||||
from bumble.profiles import bap, bass, le_audio, pbp
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -870,21 +859,13 @@ async def run_transmit(
|
||||
)
|
||||
broadcast_audio_announcement = bap.BroadcastAudioAnnouncement(broadcast_id)
|
||||
|
||||
advertising_manufacturer_data = (
|
||||
b''
|
||||
if manufacturer_data is None
|
||||
else bytes(
|
||||
core.AdvertisingData(
|
||||
[
|
||||
(
|
||||
core.AdvertisingData.MANUFACTURER_SPECIFIC_DATA,
|
||||
struct.pack('<H', manufacturer_data[0])
|
||||
+ manufacturer_data[1],
|
||||
)
|
||||
]
|
||||
)
|
||||
advertising_data_types: list[core.DataType] = [
|
||||
data_types.BroadcastName(broadcast_name)
|
||||
]
|
||||
if manufacturer_data is not None:
|
||||
advertising_data_types.append(
|
||||
data_types.ManufacturerSpecificData(*manufacturer_data)
|
||||
)
|
||||
)
|
||||
|
||||
advertising_set = await device.create_advertising_set(
|
||||
advertising_parameters=bumble.device.AdvertisingParameters(
|
||||
@@ -896,12 +877,7 @@ async def run_transmit(
|
||||
),
|
||||
advertising_data=(
|
||||
broadcast_audio_announcement.get_advertising_data()
|
||||
+ bytes(
|
||||
core.AdvertisingData(
|
||||
[(core.AdvertisingData.BROADCAST_NAME, broadcast_name.encode())]
|
||||
)
|
||||
)
|
||||
+ advertising_manufacturer_data
|
||||
+ bytes(core.AdvertisingData(advertising_data_types))
|
||||
),
|
||||
periodic_advertising_parameters=bumble.device.PeriodicAdvertisingParameters(
|
||||
periodic_advertising_interval_min=80,
|
||||
@@ -1235,7 +1211,7 @@ def transmit(
|
||||
|
||||
|
||||
def main():
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||
bumble.logging.setup_basic_logging()
|
||||
auracast()
|
||||
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ import asyncio
|
||||
import dataclasses
|
||||
import enum
|
||||
import logging
|
||||
import os
|
||||
import statistics
|
||||
import struct
|
||||
import time
|
||||
@@ -27,16 +26,19 @@ from typing import Optional
|
||||
|
||||
import click
|
||||
|
||||
import bumble.core
|
||||
import bumble.logging
|
||||
import bumble.rfcomm
|
||||
from bumble import l2cap
|
||||
from bumble.colors import color
|
||||
from bumble.core import (
|
||||
PhysicalTransport,
|
||||
BT_L2CAP_PROTOCOL_ID,
|
||||
BT_RFCOMM_PROTOCOL_ID,
|
||||
UUID,
|
||||
CommandTimeoutError,
|
||||
ConnectionPHY,
|
||||
PhysicalTransport,
|
||||
)
|
||||
from bumble.colors import color
|
||||
from bumble.core import ConnectionPHY
|
||||
from bumble.device import (
|
||||
CigParameters,
|
||||
CisLink,
|
||||
@@ -50,12 +52,13 @@ from bumble.hci import (
|
||||
HCI_LE_1M_PHY,
|
||||
HCI_LE_2M_PHY,
|
||||
HCI_LE_CODED_PHY,
|
||||
Role,
|
||||
HCI_Constant,
|
||||
HCI_Error,
|
||||
HCI_StatusError,
|
||||
HCI_IsoDataPacket,
|
||||
HCI_StatusError,
|
||||
Role,
|
||||
)
|
||||
from bumble.pairing import PairingConfig
|
||||
from bumble.sdp import (
|
||||
SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
|
||||
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
@@ -66,11 +69,7 @@ from bumble.sdp import (
|
||||
ServiceAttribute,
|
||||
)
|
||||
from bumble.transport import open_transport
|
||||
import bumble.rfcomm
|
||||
import bumble.core
|
||||
from bumble.utils import AsyncRunner
|
||||
from bumble.pairing import PairingConfig
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -2321,11 +2320,7 @@ def peripheral(ctx, transport):
|
||||
|
||||
|
||||
def main():
|
||||
logging.basicConfig(
|
||||
level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper(),
|
||||
format="[%(asctime)s.%(msecs)03d] %(levelname)s:%(name)s:%(message)s",
|
||||
datefmt="%H:%M:%S",
|
||||
)
|
||||
bumble.logging.setup_basic_logging('INFO')
|
||||
bench()
|
||||
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
# limitations under the License.
|
||||
|
||||
import click
|
||||
|
||||
from bumble.colors import color
|
||||
from bumble.hci import Address
|
||||
from bumble.helpers import generate_irk, verify_rpa_with_irk
|
||||
|
||||
@@ -23,58 +23,55 @@ import asyncio
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import humanize
|
||||
from typing import Optional, Union
|
||||
from collections import OrderedDict
|
||||
from typing import Optional, Union
|
||||
|
||||
import click
|
||||
import humanize
|
||||
from prettytable import PrettyTable
|
||||
|
||||
from prompt_toolkit import Application
|
||||
from prompt_toolkit.history import FileHistory
|
||||
from prompt_toolkit.completion import Completer, Completion, NestedCompleter
|
||||
from prompt_toolkit.key_binding import KeyBindings
|
||||
from prompt_toolkit.formatted_text import ANSI
|
||||
from prompt_toolkit.styles import Style
|
||||
from prompt_toolkit.filters import Condition
|
||||
from prompt_toolkit.widgets import TextArea, Frame
|
||||
from prompt_toolkit.widgets.toolbars import FormattedTextToolbar
|
||||
from prompt_toolkit.data_structures import Point
|
||||
from prompt_toolkit.filters import Condition
|
||||
from prompt_toolkit.formatted_text import ANSI
|
||||
from prompt_toolkit.history import FileHistory
|
||||
from prompt_toolkit.key_binding import KeyBindings
|
||||
from prompt_toolkit.layout import (
|
||||
Layout,
|
||||
HSplit,
|
||||
Window,
|
||||
CompletionsMenu,
|
||||
Float,
|
||||
FormattedTextControl,
|
||||
FloatContainer,
|
||||
ConditionalContainer,
|
||||
Dimension,
|
||||
Float,
|
||||
FloatContainer,
|
||||
FormattedTextControl,
|
||||
HSplit,
|
||||
Layout,
|
||||
Window,
|
||||
)
|
||||
from prompt_toolkit.styles import Style
|
||||
from prompt_toolkit.widgets import Frame, TextArea
|
||||
from prompt_toolkit.widgets.toolbars import FormattedTextToolbar
|
||||
|
||||
from bumble import __version__
|
||||
import bumble.core
|
||||
from bumble import colors
|
||||
from bumble import __version__, colors
|
||||
from bumble.core import UUID, AdvertisingData
|
||||
from bumble.device import (
|
||||
Connection,
|
||||
ConnectionParametersPreferences,
|
||||
ConnectionPHY,
|
||||
Device,
|
||||
Connection,
|
||||
Peer,
|
||||
)
|
||||
from bumble.utils import AsyncRunner
|
||||
from bumble.transport import open_transport
|
||||
from bumble.gatt import Characteristic, Service, CharacteristicDeclaration, Descriptor
|
||||
from bumble.gatt import Characteristic, CharacteristicDeclaration, Descriptor, Service
|
||||
from bumble.gatt_client import CharacteristicProxy
|
||||
from bumble.hci import (
|
||||
Address,
|
||||
HCI_Constant,
|
||||
HCI_LE_1M_PHY,
|
||||
HCI_LE_2M_PHY,
|
||||
HCI_LE_CODED_PHY,
|
||||
Address,
|
||||
HCI_Constant,
|
||||
)
|
||||
|
||||
from bumble.transport import open_transport
|
||||
from bumble.utils import AsyncRunner
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
|
||||
@@ -16,46 +16,45 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import os
|
||||
import logging
|
||||
import time
|
||||
|
||||
import click
|
||||
|
||||
from bumble.company_ids import COMPANY_IDENTIFIERS
|
||||
import bumble.logging
|
||||
from bumble.colors import color
|
||||
from bumble.company_ids import COMPANY_IDENTIFIERS
|
||||
from bumble.core import name_or_number
|
||||
from bumble.hci import (
|
||||
map_null_terminated_utf8_string,
|
||||
CodecID,
|
||||
LeFeature,
|
||||
HCI_LE_READ_BUFFER_SIZE_COMMAND,
|
||||
HCI_LE_READ_BUFFER_SIZE_V2_COMMAND,
|
||||
HCI_LE_READ_MAXIMUM_ADVERTISING_DATA_LENGTH_COMMAND,
|
||||
HCI_LE_READ_MAXIMUM_DATA_LENGTH_COMMAND,
|
||||
HCI_LE_READ_NUMBER_OF_SUPPORTED_ADVERTISING_SETS_COMMAND,
|
||||
HCI_LE_READ_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND,
|
||||
HCI_READ_BD_ADDR_COMMAND,
|
||||
HCI_READ_BUFFER_SIZE_COMMAND,
|
||||
HCI_READ_LOCAL_NAME_COMMAND,
|
||||
HCI_SUCCESS,
|
||||
HCI_VERSION_NAMES,
|
||||
LMP_VERSION_NAMES,
|
||||
CodecID,
|
||||
HCI_Command,
|
||||
HCI_Command_Complete_Event,
|
||||
HCI_Command_Status_Event,
|
||||
HCI_READ_BUFFER_SIZE_COMMAND,
|
||||
HCI_Read_Buffer_Size_Command,
|
||||
HCI_LE_READ_BUFFER_SIZE_V2_COMMAND,
|
||||
HCI_LE_Read_Buffer_Size_V2_Command,
|
||||
HCI_READ_BD_ADDR_COMMAND,
|
||||
HCI_Read_BD_ADDR_Command,
|
||||
HCI_READ_LOCAL_NAME_COMMAND,
|
||||
HCI_Read_Local_Name_Command,
|
||||
HCI_LE_READ_BUFFER_SIZE_COMMAND,
|
||||
HCI_LE_Read_Buffer_Size_Command,
|
||||
HCI_LE_READ_MAXIMUM_DATA_LENGTH_COMMAND,
|
||||
HCI_LE_Read_Maximum_Data_Length_Command,
|
||||
HCI_LE_READ_NUMBER_OF_SUPPORTED_ADVERTISING_SETS_COMMAND,
|
||||
HCI_LE_Read_Number_Of_Supported_Advertising_Sets_Command,
|
||||
HCI_LE_READ_MAXIMUM_ADVERTISING_DATA_LENGTH_COMMAND,
|
||||
HCI_LE_Read_Buffer_Size_V2_Command,
|
||||
HCI_LE_Read_Maximum_Advertising_Data_Length_Command,
|
||||
HCI_LE_READ_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND,
|
||||
HCI_LE_Read_Maximum_Data_Length_Command,
|
||||
HCI_LE_Read_Number_Of_Supported_Advertising_Sets_Command,
|
||||
HCI_LE_Read_Suggested_Default_Data_Length_Command,
|
||||
HCI_Read_BD_ADDR_Command,
|
||||
HCI_Read_Buffer_Size_Command,
|
||||
HCI_Read_Local_Name_Command,
|
||||
HCI_Read_Local_Supported_Codecs_Command,
|
||||
HCI_Read_Local_Supported_Codecs_V2_Command,
|
||||
HCI_Read_Local_Version_Information_Command,
|
||||
LeFeature,
|
||||
map_null_terminated_utf8_string,
|
||||
)
|
||||
from bumble.host import Host
|
||||
from bumble.transport import open_transport
|
||||
@@ -342,11 +341,7 @@ async def async_main(
|
||||
)
|
||||
@click.argument('transport')
|
||||
def main(latency_probes, latency_probe_interval, latency_probe_command, transport):
|
||||
logging.basicConfig(
|
||||
level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper(),
|
||||
format="[%(asctime)s.%(msecs)03d] %(levelname)s:%(name)s:%(message)s",
|
||||
datefmt="%H:%M:%S",
|
||||
)
|
||||
bumble.logging.setup_basic_logging()
|
||||
asyncio.run(
|
||||
async_main(
|
||||
latency_probes, latency_probe_interval, latency_probe_command, transport
|
||||
|
||||
@@ -16,21 +16,22 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
import click
|
||||
|
||||
import bumble.logging
|
||||
from bumble.colors import color
|
||||
from bumble.hci import (
|
||||
HCI_READ_LOOPBACK_MODE_COMMAND,
|
||||
HCI_Read_Loopback_Mode_Command,
|
||||
HCI_WRITE_LOOPBACK_MODE_COMMAND,
|
||||
HCI_Read_Loopback_Mode_Command,
|
||||
HCI_Write_Loopback_Mode_Command,
|
||||
LoopbackMode,
|
||||
)
|
||||
from bumble.host import Host
|
||||
from bumble.transport import open_transport
|
||||
import click
|
||||
|
||||
|
||||
class Loopback:
|
||||
@@ -194,12 +195,7 @@ class Loopback:
|
||||
)
|
||||
@click.argument('transport')
|
||||
def main(packet_size, packet_count, transport):
|
||||
logging.basicConfig(
|
||||
level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper(),
|
||||
format="[%(asctime)s.%(msecs)03d] %(levelname)s:%(name)s:%(message)s",
|
||||
datefmt="%H:%M:%S",
|
||||
)
|
||||
|
||||
bumble.logging.setup_basic_logging()
|
||||
loopback = Loopback(packet_size, packet_count, transport)
|
||||
asyncio.run(loopback.run())
|
||||
|
||||
|
||||
@@ -15,11 +15,10 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import logging
|
||||
import asyncio
|
||||
import sys
|
||||
import os
|
||||
|
||||
import bumble.logging
|
||||
from bumble.controller import Controller
|
||||
from bumble.link import LocalLink
|
||||
from bumble.transport import open_transport
|
||||
@@ -62,7 +61,7 @@ async def async_main():
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def main():
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||
bumble.logging.setup_basic_logging()
|
||||
asyncio.run(async_main())
|
||||
|
||||
|
||||
|
||||
@@ -16,18 +16,17 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import os
|
||||
import logging
|
||||
from typing import Callable, Iterable, Optional
|
||||
|
||||
import click
|
||||
|
||||
from bumble.core import ProtocolError
|
||||
import bumble.logging
|
||||
from bumble.colors import color
|
||||
from bumble.core import ProtocolError
|
||||
from bumble.device import Device, Peer
|
||||
from bumble.gatt import Service
|
||||
from bumble.profiles.device_information_service import DeviceInformationServiceProxy
|
||||
from bumble.profiles.battery_service import BatteryServiceProxy
|
||||
from bumble.profiles.device_information_service import DeviceInformationServiceProxy
|
||||
from bumble.profiles.gap import GenericAccessServiceProxy
|
||||
from bumble.profiles.pacs import PublishedAudioCapabilitiesServiceProxy
|
||||
from bumble.profiles.tmap import TelephonyAndMediaAudioServiceProxy
|
||||
@@ -267,7 +266,7 @@ def main(device_config, encrypt, transport, address_or_name):
|
||||
Dump the GATT database on a remote device. If ADDRESS_OR_NAME is not specified,
|
||||
wait for an incoming connection.
|
||||
"""
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||
bumble.logging.setup_basic_logging()
|
||||
asyncio.run(async_main(device_config, encrypt, transport, address_or_name))
|
||||
|
||||
|
||||
|
||||
@@ -16,11 +16,11 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import os
|
||||
import logging
|
||||
|
||||
import click
|
||||
|
||||
import bumble.core
|
||||
import bumble.logging
|
||||
from bumble.colors import color
|
||||
from bumble.device import Device, Peer
|
||||
from bumble.gatt import show_services
|
||||
@@ -112,7 +112,7 @@ def main(device_config, encrypt, transport, address_or_name):
|
||||
Dump the GATT database on a remote device. If ADDRESS_OR_NAME is not specified,
|
||||
wait for an incoming connection.
|
||||
"""
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||
bumble.logging.setup_basic_logging()
|
||||
asyncio.run(async_main(device_config, encrypt, transport, address_or_name))
|
||||
|
||||
|
||||
|
||||
@@ -16,20 +16,19 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import os
|
||||
import struct
|
||||
import logging
|
||||
|
||||
import click
|
||||
|
||||
import bumble.logging
|
||||
from bumble import l2cap
|
||||
from bumble.colors import color
|
||||
from bumble.device import Device, Peer
|
||||
from bumble.core import AdvertisingData
|
||||
from bumble.gatt import Service, Characteristic, CharacteristicValue
|
||||
from bumble.utils import AsyncRunner
|
||||
from bumble.transport import open_transport
|
||||
from bumble.device import Device, Peer
|
||||
from bumble.gatt import Characteristic, CharacteristicValue, Service
|
||||
from bumble.hci import HCI_Constant
|
||||
|
||||
from bumble.transport import open_transport
|
||||
from bumble.utils import AsyncRunner
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
@@ -383,6 +382,7 @@ def main(
|
||||
receive_host,
|
||||
receive_port,
|
||||
):
|
||||
bumble.logging.setup_basic_logging('WARNING')
|
||||
asyncio.run(
|
||||
run(
|
||||
hci_transport,
|
||||
@@ -397,6 +397,5 @@ def main(
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
@@ -12,14 +12,15 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import asyncio
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import logging
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
import bumble.logging
|
||||
from bumble import hci, transport
|
||||
from bumble.bridge import HCI_Bridge
|
||||
|
||||
@@ -100,7 +101,7 @@ async def async_main():
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def main():
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||
bumble.logging.setup_basic_logging()
|
||||
asyncio.run(async_main())
|
||||
|
||||
|
||||
|
||||
@@ -16,16 +16,16 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
|
||||
import click
|
||||
|
||||
import bumble.logging
|
||||
from bumble import l2cap
|
||||
from bumble.colors import color
|
||||
from bumble.transport import open_transport
|
||||
from bumble.device import Device
|
||||
from bumble.utils import FlowControlAsyncPipe
|
||||
from bumble.hci import HCI_Constant
|
||||
from bumble.transport import open_transport
|
||||
from bumble.utils import FlowControlAsyncPipe
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -356,6 +356,6 @@ def client(context, bluetooth_address, tcp_host, tcp_port):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
|
||||
if __name__ == '__main__':
|
||||
bumble.logging.setup_basic_logging('WARNING')
|
||||
cli(obj={}) # pylint: disable=no-value-for-parameter
|
||||
|
||||
@@ -20,31 +20,30 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import datetime
|
||||
import functools
|
||||
from importlib import resources
|
||||
import json
|
||||
import os
|
||||
import logging
|
||||
import pathlib
|
||||
import weakref
|
||||
import wave
|
||||
import weakref
|
||||
from importlib import resources
|
||||
|
||||
try:
|
||||
import lc3 # type: ignore # pylint: disable=E0401
|
||||
except ImportError as e:
|
||||
raise ImportError("Try `python -m pip install \".[lc3]\"`.") from e
|
||||
|
||||
import click
|
||||
import aiohttp.web
|
||||
import click
|
||||
|
||||
import bumble
|
||||
from bumble import utils
|
||||
from bumble.core import AdvertisingData
|
||||
import bumble.logging
|
||||
from bumble import data_types, utils
|
||||
from bumble.colors import color
|
||||
from bumble.device import Device, DeviceConfiguration, AdvertisingParameters, CisLink
|
||||
from bumble.transport import open_transport
|
||||
from bumble.profiles import ascs, bap, pacs
|
||||
from bumble.core import AdvertisingData
|
||||
from bumble.device import AdvertisingParameters, CisLink, Device, DeviceConfiguration
|
||||
from bumble.hci import Address, CodecID, CodingFormat, HCI_IsoDataPacket
|
||||
|
||||
from bumble.profiles import ascs, bap, pacs
|
||||
from bumble.transport import open_transport
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -331,22 +330,13 @@ class Speaker:
|
||||
advertising_data = bytes(
|
||||
AdvertisingData(
|
||||
[
|
||||
(
|
||||
AdvertisingData.COMPLETE_LOCAL_NAME,
|
||||
bytes(device_config.name, 'utf-8'),
|
||||
data_types.CompleteLocalName(device_config.name),
|
||||
data_types.Flags(
|
||||
AdvertisingData.Flags.LE_GENERAL_DISCOVERABLE_MODE
|
||||
| AdvertisingData.Flags.BR_EDR_NOT_SUPPORTED
|
||||
),
|
||||
(
|
||||
AdvertisingData.FLAGS,
|
||||
bytes(
|
||||
[
|
||||
AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG
|
||||
| AdvertisingData.BR_EDR_NOT_SUPPORTED_FLAG
|
||||
]
|
||||
),
|
||||
),
|
||||
(
|
||||
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
|
||||
bytes(pacs.PublishedAudioCapabilitiesService.UUID),
|
||||
data_types.IncompleteListOf16BitServiceUUIDs(
|
||||
[pacs.PublishedAudioCapabilitiesService.UUID]
|
||||
),
|
||||
]
|
||||
)
|
||||
@@ -454,7 +444,7 @@ def speaker(ui_port: int, device_config: str, transport: str, lc3_file: str) ->
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def main():
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||
bumble.logging.setup_basic_logging()
|
||||
speaker()
|
||||
|
||||
|
||||
|
||||
73
apps/pair.py
73
apps/pair.py
@@ -16,42 +16,44 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import os
|
||||
import logging
|
||||
import os
|
||||
import struct
|
||||
|
||||
import click
|
||||
from prompt_toolkit.shortcuts import PromptSession
|
||||
|
||||
from bumble import data_types
|
||||
from bumble.a2dp import make_audio_sink_service_sdp_records
|
||||
from bumble.att import (
|
||||
ATT_INSUFFICIENT_AUTHENTICATION_ERROR,
|
||||
ATT_INSUFFICIENT_ENCRYPTION_ERROR,
|
||||
ATT_Error,
|
||||
)
|
||||
from bumble.colors import color
|
||||
from bumble.device import Device, Peer
|
||||
from bumble.transport import open_transport
|
||||
from bumble.pairing import OobData, PairingDelegate, PairingConfig
|
||||
from bumble.smp import OobContext, OobLegacyContext
|
||||
from bumble.smp import error_name as smp_error_name
|
||||
from bumble.keys import JsonKeyStore
|
||||
from bumble.core import (
|
||||
UUID,
|
||||
AdvertisingData,
|
||||
Appearance,
|
||||
ProtocolError,
|
||||
DataType,
|
||||
PhysicalTransport,
|
||||
UUID,
|
||||
ProtocolError,
|
||||
)
|
||||
from bumble.device import Device, Peer
|
||||
from bumble.gatt import (
|
||||
GATT_DEVICE_NAME_CHARACTERISTIC,
|
||||
GATT_GENERIC_ACCESS_SERVICE,
|
||||
GATT_HEART_RATE_SERVICE,
|
||||
GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC,
|
||||
Service,
|
||||
GATT_HEART_RATE_SERVICE,
|
||||
Characteristic,
|
||||
Service,
|
||||
)
|
||||
from bumble.hci import OwnAddressType
|
||||
from bumble.att import (
|
||||
ATT_Error,
|
||||
ATT_INSUFFICIENT_AUTHENTICATION_ERROR,
|
||||
ATT_INSUFFICIENT_ENCRYPTION_ERROR,
|
||||
)
|
||||
from bumble.keys import JsonKeyStore
|
||||
from bumble.pairing import OobData, PairingConfig, PairingDelegate
|
||||
from bumble.smp import OobContext, OobLegacyContext
|
||||
from bumble.smp import error_name as smp_error_name
|
||||
from bumble.transport import open_transport
|
||||
from bumble.utils import AsyncRunner
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -506,33 +508,21 @@ async def pair(
|
||||
if mode == 'dual':
|
||||
flags |= AdvertisingData.Flags.SIMULTANEOUS_LE_BR_EDR_CAPABLE
|
||||
|
||||
ad_structs = [
|
||||
(
|
||||
AdvertisingData.FLAGS,
|
||||
bytes([flags]),
|
||||
),
|
||||
(AdvertisingData.COMPLETE_LOCAL_NAME, 'Bumble'.encode()),
|
||||
advertising_data_types: list[DataType] = [
|
||||
data_types.Flags(flags),
|
||||
data_types.CompleteLocalName('Bumble'),
|
||||
]
|
||||
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),
|
||||
)
|
||||
advertising_data_types.append(
|
||||
data_types.IncompleteListOf16BitServiceUUIDs(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),
|
||||
)
|
||||
advertising_data_types.append(
|
||||
data_types.IncompleteListOf32BitServiceUUIDs(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),
|
||||
)
|
||||
advertising_data_types.append(
|
||||
data_types.IncompleteListOf128BitServiceUUIDs(service_uuids_128)
|
||||
)
|
||||
|
||||
if advertise_appearance:
|
||||
@@ -559,13 +549,10 @@ async def pair(
|
||||
advertise_appearance_int = int(
|
||||
Appearance(category_enum, subcategory_enum)
|
||||
)
|
||||
ad_structs.append(
|
||||
(
|
||||
AdvertisingData.APPEARANCE,
|
||||
struct.pack('<H', advertise_appearance_int),
|
||||
)
|
||||
advertising_data_types.append(
|
||||
data_types.Appearance(category_enum, subcategory_enum)
|
||||
)
|
||||
device.advertising_data = bytes(AdvertisingData(ad_structs))
|
||||
device.advertising_data = bytes(AdvertisingData(advertising_data_types))
|
||||
await device.start_advertising(
|
||||
auto_restart=True,
|
||||
own_address_type=(
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import asyncio
|
||||
import click
|
||||
import logging
|
||||
import json
|
||||
|
||||
from bumble.pandora import PandoraDevice, Config, serve
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import click
|
||||
|
||||
from bumble.pandora import Config, PandoraDevice, serve
|
||||
|
||||
BUMBLE_SERVER_GRPC_PORT = 7999
|
||||
ROOTCANAL_PORT_CUTTLEFISH = 7300
|
||||
|
||||
|
||||
@@ -16,55 +16,51 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import asyncio.subprocess
|
||||
import os
|
||||
import logging
|
||||
from typing import Optional, Union
|
||||
|
||||
import click
|
||||
|
||||
import bumble.logging
|
||||
from bumble.a2dp import (
|
||||
make_audio_source_service_sdp_records,
|
||||
A2DP_SBC_CODEC_TYPE,
|
||||
A2DP_MPEG_2_4_AAC_CODEC_TYPE,
|
||||
A2DP_NON_A2DP_CODEC_TYPE,
|
||||
A2DP_SBC_CODEC_TYPE,
|
||||
AacFrame,
|
||||
AacParser,
|
||||
AacPacketSource,
|
||||
AacMediaCodecInformation,
|
||||
SbcFrame,
|
||||
SbcParser,
|
||||
SbcPacketSource,
|
||||
SbcMediaCodecInformation,
|
||||
OpusPacket,
|
||||
OpusParser,
|
||||
OpusPacketSource,
|
||||
AacPacketSource,
|
||||
AacParser,
|
||||
OpusMediaCodecInformation,
|
||||
OpusPacket,
|
||||
OpusPacketSource,
|
||||
OpusParser,
|
||||
SbcFrame,
|
||||
SbcMediaCodecInformation,
|
||||
SbcPacketSource,
|
||||
SbcParser,
|
||||
make_audio_source_service_sdp_records,
|
||||
)
|
||||
from bumble.avrcp import Protocol as AvrcpProtocol
|
||||
from bumble.avdtp import (
|
||||
find_avdtp_service_with_connection,
|
||||
AVDTP_AUDIO_MEDIA_TYPE,
|
||||
AVDTP_DELAY_REPORTING_SERVICE_CATEGORY,
|
||||
MediaCodecCapabilities,
|
||||
MediaPacketPump,
|
||||
Protocol as AvdtpProtocol,
|
||||
)
|
||||
from bumble.avdtp import Protocol as AvdtpProtocol
|
||||
from bumble.avdtp import find_avdtp_service_with_connection
|
||||
from bumble.avrcp import Protocol as AvrcpProtocol
|
||||
from bumble.colors import color
|
||||
from bumble.core import (
|
||||
AdvertisingData,
|
||||
ConnectionError as BumbleConnectionError,
|
||||
DeviceClass,
|
||||
PhysicalTransport,
|
||||
)
|
||||
from bumble.core import AdvertisingData
|
||||
from bumble.core import ConnectionError as BumbleConnectionError
|
||||
from bumble.core import DeviceClass, PhysicalTransport
|
||||
from bumble.device import Connection, Device, DeviceConfiguration
|
||||
from bumble.hci import Address, HCI_CONNECTION_ALREADY_EXISTS_ERROR, HCI_Constant
|
||||
from bumble.hci import HCI_CONNECTION_ALREADY_EXISTS_ERROR, Address, HCI_Constant
|
||||
from bumble.pairing import PairingConfig
|
||||
from bumble.transport import open_transport
|
||||
from bumble.utils import AsyncRunner
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -599,7 +595,7 @@ def play(context, address, audio_format, audio_file):
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def main():
|
||||
logging.basicConfig(level=os.environ.get("BUMBLE_LOGLEVEL", "WARNING").upper())
|
||||
bumble.logging.setup_basic_logging("WARNING")
|
||||
player_cli()
|
||||
|
||||
|
||||
|
||||
@@ -16,21 +16,15 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
import click
|
||||
|
||||
import bumble.logging
|
||||
from bumble import core, hci, rfcomm, transport, utils
|
||||
from bumble.colors import color
|
||||
from bumble.device import Device, DeviceConfiguration, Connection
|
||||
from bumble import core
|
||||
from bumble import hci
|
||||
from bumble import rfcomm
|
||||
from bumble import transport
|
||||
from bumble import utils
|
||||
|
||||
from bumble.device import Connection, Device, DeviceConfiguration
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
@@ -515,6 +509,6 @@ def client(context, bluetooth_address, tcp_host, tcp_port, authenticate, encrypt
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
logging.basicConfig(level=os.environ.get("BUMBLE_LOGLEVEL", "WARNING").upper())
|
||||
if __name__ == "__main__":
|
||||
bumble.logging.setup_basic_logging("WARNING")
|
||||
cli(obj={}) # pylint: disable=no-value-for-parameter
|
||||
|
||||
25
apps/scan.py
25
apps/scan.py
@@ -16,17 +16,17 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import os
|
||||
import logging
|
||||
|
||||
import click
|
||||
|
||||
import bumble.logging
|
||||
from bumble import data_types
|
||||
from bumble.colors import color
|
||||
from bumble.device import Device
|
||||
from bumble.transport import open_transport
|
||||
from bumble.device import Advertisement, Device
|
||||
from bumble.hci import HCI_LE_1M_PHY, HCI_LE_CODED_PHY, Address, HCI_Constant
|
||||
from bumble.keys import JsonKeyStore
|
||||
from bumble.smp import AddressResolver
|
||||
from bumble.device import Advertisement
|
||||
from bumble.hci import Address, HCI_Constant, HCI_LE_1M_PHY, HCI_LE_CODED_PHY
|
||||
from bumble.transport import open_transport
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -95,13 +95,22 @@ class AdvertisementPrinter:
|
||||
else:
|
||||
phy_info = ''
|
||||
|
||||
details = separator.join(
|
||||
[
|
||||
data_type.to_string(use_label=True)
|
||||
for data_type in data_types.data_types_from_advertising_data(
|
||||
advertisement.data
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
print(
|
||||
f'>>> {color(address, address_color)} '
|
||||
f'[{color(address_type_string, type_color)}]{address_qualifier}'
|
||||
f'{resolution_qualifier}:{separator}'
|
||||
f'{phy_info}'
|
||||
f'RSSI:{advertisement.rssi:4} {rssi_bar}{separator}'
|
||||
f'{advertisement.data.to_string(separator)}\n'
|
||||
f'{details}\n'
|
||||
)
|
||||
|
||||
def on_advertisement(self, advertisement):
|
||||
@@ -237,7 +246,7 @@ def main(
|
||||
device_config,
|
||||
transport,
|
||||
):
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
|
||||
bumble.logging.setup_basic_logging('WARNING')
|
||||
asyncio.run(
|
||||
scan(
|
||||
min_rssi,
|
||||
|
||||
@@ -18,16 +18,15 @@
|
||||
import datetime
|
||||
import importlib
|
||||
import logging
|
||||
import os
|
||||
import struct
|
||||
|
||||
import click
|
||||
|
||||
from bumble.colors import color
|
||||
import bumble.logging
|
||||
from bumble import hci
|
||||
from bumble.transport.common import PacketReader
|
||||
from bumble.colors import color
|
||||
from bumble.helpers import PacketTracer
|
||||
|
||||
from bumble.transport.common import PacketReader
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -188,5 +187,5 @@ def main(format, vendor, filename):
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
if __name__ == '__main__':
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
|
||||
bumble.logging.setup_basic_logging('WARNING')
|
||||
main() # pylint: disable=no-value-for-parameter
|
||||
|
||||
@@ -16,49 +16,49 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import asyncio.subprocess
|
||||
from importlib import resources
|
||||
import enum
|
||||
import json
|
||||
import os
|
||||
import logging
|
||||
import pathlib
|
||||
import subprocess
|
||||
from typing import Optional
|
||||
import weakref
|
||||
from importlib import resources
|
||||
from typing import Optional
|
||||
|
||||
import click
|
||||
import aiohttp
|
||||
import click
|
||||
from aiohttp import web
|
||||
|
||||
import bumble
|
||||
from bumble.colors import color
|
||||
from bumble.core import PhysicalTransport, CommandTimeoutError
|
||||
from bumble.device import Connection, Device, DeviceConfiguration
|
||||
from bumble.hci import HCI_StatusError
|
||||
from bumble.pairing import PairingConfig
|
||||
from bumble.sdp import ServiceAttribute
|
||||
from bumble.transport import open_transport
|
||||
import bumble.logging
|
||||
from bumble.a2dp import (
|
||||
A2DP_MPEG_2_4_AAC_CODEC_TYPE,
|
||||
A2DP_NON_A2DP_CODEC_TYPE,
|
||||
A2DP_SBC_CODEC_TYPE,
|
||||
AacMediaCodecInformation,
|
||||
OpusMediaCodecInformation,
|
||||
SbcMediaCodecInformation,
|
||||
make_audio_sink_service_sdp_records,
|
||||
)
|
||||
from bumble.avdtp import (
|
||||
AVDTP_AUDIO_MEDIA_TYPE,
|
||||
Listener,
|
||||
MediaCodecCapabilities,
|
||||
Protocol,
|
||||
)
|
||||
from bumble.a2dp import (
|
||||
make_audio_sink_service_sdp_records,
|
||||
A2DP_SBC_CODEC_TYPE,
|
||||
A2DP_MPEG_2_4_AAC_CODEC_TYPE,
|
||||
A2DP_NON_A2DP_CODEC_TYPE,
|
||||
SbcMediaCodecInformation,
|
||||
AacMediaCodecInformation,
|
||||
OpusMediaCodecInformation,
|
||||
)
|
||||
from bumble.utils import AsyncRunner
|
||||
from bumble.codecs import AacAudioRtpPacket
|
||||
from bumble.colors import color
|
||||
from bumble.core import CommandTimeoutError, PhysicalTransport
|
||||
from bumble.device import Connection, Device, DeviceConfiguration
|
||||
from bumble.hci import HCI_StatusError
|
||||
from bumble.pairing import PairingConfig
|
||||
from bumble.rtp import MediaPacket
|
||||
|
||||
from bumble.sdp import ServiceAttribute
|
||||
from bumble.transport import open_transport
|
||||
from bumble.utils import AsyncRunner
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -833,11 +833,7 @@ def speaker(
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def main():
|
||||
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",
|
||||
)
|
||||
bumble.logging.setup_basic_logging('WARNING')
|
||||
speaker()
|
||||
|
||||
|
||||
|
||||
@@ -16,10 +16,10 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import os
|
||||
import logging
|
||||
|
||||
import click
|
||||
|
||||
import bumble.logging
|
||||
from bumble.device import Device
|
||||
from bumble.keys import JsonKeyStore
|
||||
from bumble.transport import open_transport
|
||||
@@ -68,7 +68,7 @@ def main(keystore_file, hci_transport, device_config, address):
|
||||
instantiated.
|
||||
If no address is passed, the existing pairing keys for all addresses are printed.
|
||||
"""
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||
bumble.logging.setup_basic_logging()
|
||||
|
||||
if not keystore_file and not hci_transport:
|
||||
print('either --keystore-file or --hci-transport must be specified.')
|
||||
|
||||
@@ -26,15 +26,13 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import os
|
||||
import logging
|
||||
import click
|
||||
import usb1
|
||||
|
||||
import bumble.logging
|
||||
from bumble.colors import color
|
||||
from bumble.transport.usb import load_libusb
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -169,7 +167,7 @@ def is_bluetooth_hci(device):
|
||||
@click.command()
|
||||
@click.option('--verbose', is_flag=True, default=False, help='Print more details')
|
||||
def main(verbose):
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
|
||||
bumble.logging.setup_basic_logging('WARNING')
|
||||
|
||||
load_libusb()
|
||||
with usb1.USBContext() as context:
|
||||
|
||||
@@ -17,37 +17,36 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import AsyncGenerator
|
||||
import dataclasses
|
||||
import enum
|
||||
import logging
|
||||
import struct
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Awaitable, Callable
|
||||
from typing_extensions import ClassVar, Self
|
||||
|
||||
from typing_extensions import ClassVar, Self
|
||||
|
||||
from bumble.codecs import AacAudioRtpPacket
|
||||
from bumble.company_ids import COMPANY_IDENTIFIERS
|
||||
from bumble.sdp import (
|
||||
DataElement,
|
||||
ServiceAttribute,
|
||||
SDP_PUBLIC_BROWSE_ROOT,
|
||||
SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
|
||||
SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
|
||||
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
|
||||
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
)
|
||||
from bumble.core import (
|
||||
BT_L2CAP_PROTOCOL_ID,
|
||||
BT_AUDIO_SOURCE_SERVICE,
|
||||
BT_AUDIO_SINK_SERVICE,
|
||||
BT_AVDTP_PROTOCOL_ID,
|
||||
BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE,
|
||||
BT_AUDIO_SINK_SERVICE,
|
||||
BT_AUDIO_SOURCE_SERVICE,
|
||||
BT_AVDTP_PROTOCOL_ID,
|
||||
BT_L2CAP_PROTOCOL_ID,
|
||||
name_or_number,
|
||||
)
|
||||
from bumble.rtp import MediaPacket
|
||||
|
||||
from bumble.sdp import (
|
||||
SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
|
||||
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
SDP_PUBLIC_BROWSE_ROOT,
|
||||
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
|
||||
SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
|
||||
DataElement,
|
||||
ServiceAttribute,
|
||||
)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
|
||||
493
bumble/att.py
493
bumble/att.py
@@ -24,24 +24,26 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import enum
|
||||
import functools
|
||||
import inspect
|
||||
import struct
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Awaitable,
|
||||
Callable,
|
||||
ClassVar,
|
||||
Generic,
|
||||
Optional,
|
||||
TypeVar,
|
||||
Union,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
|
||||
from bumble import utils
|
||||
from bumble.core import UUID, name_or_number, InvalidOperationError, ProtocolError
|
||||
from bumble.hci import HCI_Object, key_with_value
|
||||
from bumble import hci, utils
|
||||
from bumble.colors import color
|
||||
from bumble.core import UUID, InvalidOperationError, ProtocolError
|
||||
from bumble.hci import HCI_Object
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Typing
|
||||
@@ -60,96 +62,66 @@ _T = TypeVar('_T')
|
||||
ATT_CID = 0x04
|
||||
ATT_PSM = 0x001F
|
||||
|
||||
ATT_ERROR_RESPONSE = 0x01
|
||||
ATT_EXCHANGE_MTU_REQUEST = 0x02
|
||||
ATT_EXCHANGE_MTU_RESPONSE = 0x03
|
||||
ATT_FIND_INFORMATION_REQUEST = 0x04
|
||||
ATT_FIND_INFORMATION_RESPONSE = 0x05
|
||||
ATT_FIND_BY_TYPE_VALUE_REQUEST = 0x06
|
||||
ATT_FIND_BY_TYPE_VALUE_RESPONSE = 0x07
|
||||
ATT_READ_BY_TYPE_REQUEST = 0x08
|
||||
ATT_READ_BY_TYPE_RESPONSE = 0x09
|
||||
ATT_READ_REQUEST = 0x0A
|
||||
ATT_READ_RESPONSE = 0x0B
|
||||
ATT_READ_BLOB_REQUEST = 0x0C
|
||||
ATT_READ_BLOB_RESPONSE = 0x0D
|
||||
ATT_READ_MULTIPLE_REQUEST = 0x0E
|
||||
ATT_READ_MULTIPLE_RESPONSE = 0x0F
|
||||
ATT_READ_BY_GROUP_TYPE_REQUEST = 0x10
|
||||
ATT_READ_BY_GROUP_TYPE_RESPONSE = 0x11
|
||||
ATT_WRITE_REQUEST = 0x12
|
||||
ATT_WRITE_RESPONSE = 0x13
|
||||
ATT_WRITE_COMMAND = 0x52
|
||||
ATT_SIGNED_WRITE_COMMAND = 0xD2
|
||||
ATT_PREPARE_WRITE_REQUEST = 0x16
|
||||
ATT_PREPARE_WRITE_RESPONSE = 0x17
|
||||
ATT_EXECUTE_WRITE_REQUEST = 0x18
|
||||
ATT_EXECUTE_WRITE_RESPONSE = 0x19
|
||||
ATT_HANDLE_VALUE_NOTIFICATION = 0x1B
|
||||
ATT_HANDLE_VALUE_INDICATION = 0x1D
|
||||
ATT_HANDLE_VALUE_CONFIRMATION = 0x1E
|
||||
|
||||
ATT_PDU_NAMES = {
|
||||
ATT_ERROR_RESPONSE: 'ATT_ERROR_RESPONSE',
|
||||
ATT_EXCHANGE_MTU_REQUEST: 'ATT_EXCHANGE_MTU_REQUEST',
|
||||
ATT_EXCHANGE_MTU_RESPONSE: 'ATT_EXCHANGE_MTU_RESPONSE',
|
||||
ATT_FIND_INFORMATION_REQUEST: 'ATT_FIND_INFORMATION_REQUEST',
|
||||
ATT_FIND_INFORMATION_RESPONSE: 'ATT_FIND_INFORMATION_RESPONSE',
|
||||
ATT_FIND_BY_TYPE_VALUE_REQUEST: 'ATT_FIND_BY_TYPE_VALUE_REQUEST',
|
||||
ATT_FIND_BY_TYPE_VALUE_RESPONSE: 'ATT_FIND_BY_TYPE_VALUE_RESPONSE',
|
||||
ATT_READ_BY_TYPE_REQUEST: 'ATT_READ_BY_TYPE_REQUEST',
|
||||
ATT_READ_BY_TYPE_RESPONSE: 'ATT_READ_BY_TYPE_RESPONSE',
|
||||
ATT_READ_REQUEST: 'ATT_READ_REQUEST',
|
||||
ATT_READ_RESPONSE: 'ATT_READ_RESPONSE',
|
||||
ATT_READ_BLOB_REQUEST: 'ATT_READ_BLOB_REQUEST',
|
||||
ATT_READ_BLOB_RESPONSE: 'ATT_READ_BLOB_RESPONSE',
|
||||
ATT_READ_MULTIPLE_REQUEST: 'ATT_READ_MULTIPLE_REQUEST',
|
||||
ATT_READ_MULTIPLE_RESPONSE: 'ATT_READ_MULTIPLE_RESPONSE',
|
||||
ATT_READ_BY_GROUP_TYPE_REQUEST: 'ATT_READ_BY_GROUP_TYPE_REQUEST',
|
||||
ATT_READ_BY_GROUP_TYPE_RESPONSE: 'ATT_READ_BY_GROUP_TYPE_RESPONSE',
|
||||
ATT_WRITE_REQUEST: 'ATT_WRITE_REQUEST',
|
||||
ATT_WRITE_RESPONSE: 'ATT_WRITE_RESPONSE',
|
||||
ATT_WRITE_COMMAND: 'ATT_WRITE_COMMAND',
|
||||
ATT_SIGNED_WRITE_COMMAND: 'ATT_SIGNED_WRITE_COMMAND',
|
||||
ATT_PREPARE_WRITE_REQUEST: 'ATT_PREPARE_WRITE_REQUEST',
|
||||
ATT_PREPARE_WRITE_RESPONSE: 'ATT_PREPARE_WRITE_RESPONSE',
|
||||
ATT_EXECUTE_WRITE_REQUEST: 'ATT_EXECUTE_WRITE_REQUEST',
|
||||
ATT_EXECUTE_WRITE_RESPONSE: 'ATT_EXECUTE_WRITE_RESPONSE',
|
||||
ATT_HANDLE_VALUE_NOTIFICATION: 'ATT_HANDLE_VALUE_NOTIFICATION',
|
||||
ATT_HANDLE_VALUE_INDICATION: 'ATT_HANDLE_VALUE_INDICATION',
|
||||
ATT_HANDLE_VALUE_CONFIRMATION: 'ATT_HANDLE_VALUE_CONFIRMATION'
|
||||
}
|
||||
class Opcode(hci.SpecableEnum):
|
||||
ATT_ERROR_RESPONSE = 0x01
|
||||
ATT_EXCHANGE_MTU_REQUEST = 0x02
|
||||
ATT_EXCHANGE_MTU_RESPONSE = 0x03
|
||||
ATT_FIND_INFORMATION_REQUEST = 0x04
|
||||
ATT_FIND_INFORMATION_RESPONSE = 0x05
|
||||
ATT_FIND_BY_TYPE_VALUE_REQUEST = 0x06
|
||||
ATT_FIND_BY_TYPE_VALUE_RESPONSE = 0x07
|
||||
ATT_READ_BY_TYPE_REQUEST = 0x08
|
||||
ATT_READ_BY_TYPE_RESPONSE = 0x09
|
||||
ATT_READ_REQUEST = 0x0A
|
||||
ATT_READ_RESPONSE = 0x0B
|
||||
ATT_READ_BLOB_REQUEST = 0x0C
|
||||
ATT_READ_BLOB_RESPONSE = 0x0D
|
||||
ATT_READ_MULTIPLE_REQUEST = 0x0E
|
||||
ATT_READ_MULTIPLE_RESPONSE = 0x0F
|
||||
ATT_READ_BY_GROUP_TYPE_REQUEST = 0x10
|
||||
ATT_READ_BY_GROUP_TYPE_RESPONSE = 0x11
|
||||
ATT_WRITE_REQUEST = 0x12
|
||||
ATT_WRITE_RESPONSE = 0x13
|
||||
ATT_WRITE_COMMAND = 0x52
|
||||
ATT_SIGNED_WRITE_COMMAND = 0xD2
|
||||
ATT_PREPARE_WRITE_REQUEST = 0x16
|
||||
ATT_PREPARE_WRITE_RESPONSE = 0x17
|
||||
ATT_EXECUTE_WRITE_REQUEST = 0x18
|
||||
ATT_EXECUTE_WRITE_RESPONSE = 0x19
|
||||
ATT_HANDLE_VALUE_NOTIFICATION = 0x1B
|
||||
ATT_HANDLE_VALUE_INDICATION = 0x1D
|
||||
ATT_HANDLE_VALUE_CONFIRMATION = 0x1E
|
||||
|
||||
ATT_REQUESTS = [
|
||||
ATT_EXCHANGE_MTU_REQUEST,
|
||||
ATT_FIND_INFORMATION_REQUEST,
|
||||
ATT_FIND_BY_TYPE_VALUE_REQUEST,
|
||||
ATT_READ_BY_TYPE_REQUEST,
|
||||
ATT_READ_REQUEST,
|
||||
ATT_READ_BLOB_REQUEST,
|
||||
ATT_READ_MULTIPLE_REQUEST,
|
||||
ATT_READ_BY_GROUP_TYPE_REQUEST,
|
||||
ATT_WRITE_REQUEST,
|
||||
ATT_PREPARE_WRITE_REQUEST,
|
||||
ATT_EXECUTE_WRITE_REQUEST
|
||||
Opcode.ATT_EXCHANGE_MTU_REQUEST,
|
||||
Opcode.ATT_FIND_INFORMATION_REQUEST,
|
||||
Opcode.ATT_FIND_BY_TYPE_VALUE_REQUEST,
|
||||
Opcode.ATT_READ_BY_TYPE_REQUEST,
|
||||
Opcode.ATT_READ_REQUEST,
|
||||
Opcode.ATT_READ_BLOB_REQUEST,
|
||||
Opcode.ATT_READ_MULTIPLE_REQUEST,
|
||||
Opcode.ATT_READ_BY_GROUP_TYPE_REQUEST,
|
||||
Opcode.ATT_WRITE_REQUEST,
|
||||
Opcode.ATT_PREPARE_WRITE_REQUEST,
|
||||
Opcode.ATT_EXECUTE_WRITE_REQUEST
|
||||
]
|
||||
|
||||
ATT_RESPONSES = [
|
||||
ATT_ERROR_RESPONSE,
|
||||
ATT_EXCHANGE_MTU_RESPONSE,
|
||||
ATT_FIND_INFORMATION_RESPONSE,
|
||||
ATT_FIND_BY_TYPE_VALUE_RESPONSE,
|
||||
ATT_READ_BY_TYPE_RESPONSE,
|
||||
ATT_READ_RESPONSE,
|
||||
ATT_READ_BLOB_RESPONSE,
|
||||
ATT_READ_MULTIPLE_RESPONSE,
|
||||
ATT_READ_BY_GROUP_TYPE_RESPONSE,
|
||||
ATT_WRITE_RESPONSE,
|
||||
ATT_PREPARE_WRITE_RESPONSE,
|
||||
ATT_EXECUTE_WRITE_RESPONSE
|
||||
Opcode.ATT_ERROR_RESPONSE,
|
||||
Opcode.ATT_EXCHANGE_MTU_RESPONSE,
|
||||
Opcode.ATT_FIND_INFORMATION_RESPONSE,
|
||||
Opcode.ATT_FIND_BY_TYPE_VALUE_RESPONSE,
|
||||
Opcode.ATT_READ_BY_TYPE_RESPONSE,
|
||||
Opcode.ATT_READ_RESPONSE,
|
||||
Opcode.ATT_READ_BLOB_RESPONSE,
|
||||
Opcode.ATT_READ_MULTIPLE_RESPONSE,
|
||||
Opcode.ATT_READ_BY_GROUP_TYPE_RESPONSE,
|
||||
Opcode.ATT_WRITE_RESPONSE,
|
||||
Opcode.ATT_PREPARE_WRITE_RESPONSE,
|
||||
Opcode.ATT_EXECUTE_WRITE_RESPONSE
|
||||
]
|
||||
|
||||
class ErrorCode(utils.OpenIntEnum):
|
||||
class ErrorCode(hci.SpecableEnum):
|
||||
'''
|
||||
See
|
||||
|
||||
@@ -204,10 +176,6 @@ ATT_INSUFFICIENT_RESOURCES_ERROR = ErrorCode.INSUFFICIENT_RESOURCES
|
||||
ATT_DEFAULT_MTU = 23
|
||||
|
||||
HANDLE_FIELD_SPEC = {'size': 2, 'mapper': lambda x: f'0x{x:04X}'}
|
||||
# pylint: disable-next=unnecessary-lambda-assignment,unnecessary-lambda
|
||||
UUID_2_16_FIELD_SPEC = lambda x, y: UUID.parse_uuid(x, y)
|
||||
# pylint: disable-next=unnecessary-lambda-assignment,unnecessary-lambda
|
||||
UUID_2_FIELD_SPEC = lambda x, y: UUID.parse_uuid_2(x, y) # noqa: E731
|
||||
|
||||
# fmt: on
|
||||
# pylint: enable=line-too-long
|
||||
@@ -227,7 +195,7 @@ class ATT_Error(ProtocolError):
|
||||
super().__init__(
|
||||
error_code,
|
||||
error_namespace='att',
|
||||
error_name=ATT_PDU.error_name(error_code),
|
||||
error_name=ErrorCode(error_code).name,
|
||||
)
|
||||
self.att_handle = att_handle
|
||||
self.message = message
|
||||
@@ -242,61 +210,45 @@ class ATT_Error(ProtocolError):
|
||||
# -----------------------------------------------------------------------------
|
||||
# Attribute Protocol
|
||||
# -----------------------------------------------------------------------------
|
||||
@dataclasses.dataclass
|
||||
class ATT_PDU:
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.3 ATTRIBUTE PDU
|
||||
'''
|
||||
|
||||
pdu_classes: dict[int, type[ATT_PDU]] = {}
|
||||
op_code = 0
|
||||
name: str
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(pdu):
|
||||
op_code = pdu[0]
|
||||
|
||||
cls = ATT_PDU.pdu_classes.get(op_code)
|
||||
if cls is None:
|
||||
instance = ATT_PDU(pdu)
|
||||
instance.name = ATT_PDU.pdu_name(op_code)
|
||||
instance.op_code = op_code
|
||||
return instance
|
||||
self = cls.__new__(cls)
|
||||
ATT_PDU.__init__(self, pdu)
|
||||
if hasattr(self, 'fields'):
|
||||
self.init_from_bytes(pdu, 1)
|
||||
return self
|
||||
|
||||
@staticmethod
|
||||
def pdu_name(op_code):
|
||||
return name_or_number(ATT_PDU_NAMES, op_code, 2)
|
||||
pdu_classes: ClassVar[dict[int, type[ATT_PDU]]] = {}
|
||||
fields: ClassVar[hci.Fields] = ()
|
||||
op_code: int = dataclasses.field(init=False)
|
||||
name: str = dataclasses.field(init=False)
|
||||
_payload: Optional[bytes] = dataclasses.field(default=None, init=False)
|
||||
|
||||
@classmethod
|
||||
def error_name(cls, error_code: int) -> str:
|
||||
return ErrorCode(error_code).name
|
||||
def from_bytes(cls, pdu: bytes) -> ATT_PDU:
|
||||
op_code = pdu[0]
|
||||
|
||||
@staticmethod
|
||||
def subclass(fields):
|
||||
def inner(cls):
|
||||
cls.name = cls.__name__.upper()
|
||||
cls.op_code = key_with_value(ATT_PDU_NAMES, cls.name)
|
||||
if cls.op_code is None:
|
||||
raise KeyError(f'PDU name {cls.name} not found in ATT_PDU_NAMES')
|
||||
cls.fields = fields
|
||||
subclass = ATT_PDU.pdu_classes.get(op_code)
|
||||
if subclass is None:
|
||||
instance = ATT_PDU()
|
||||
instance.op_code = op_code
|
||||
instance.payload = pdu[1:]
|
||||
instance.name = Opcode(op_code).name
|
||||
return instance
|
||||
instance = subclass(**HCI_Object.dict_from_bytes(pdu, 1, subclass.fields))
|
||||
instance.payload = pdu[1:]
|
||||
return instance
|
||||
|
||||
# Register a factory for this class
|
||||
ATT_PDU.pdu_classes[cls.op_code] = cls
|
||||
_PDU = TypeVar("_PDU", bound="ATT_PDU")
|
||||
|
||||
return cls
|
||||
@classmethod
|
||||
def subclass(cls, subclass: type[_PDU]) -> type[_PDU]:
|
||||
subclass.name = subclass.__name__.upper()
|
||||
subclass.op_code = Opcode[subclass.name]
|
||||
subclass.fields = HCI_Object.fields_from_dataclass(subclass)
|
||||
|
||||
return inner
|
||||
# Register a factory for this class
|
||||
ATT_PDU.pdu_classes[subclass.op_code] = subclass
|
||||
|
||||
def __init__(self, pdu=None, **kwargs):
|
||||
if hasattr(self, 'fields') and kwargs:
|
||||
HCI_Object.init_from_fields(self, self.fields, kwargs)
|
||||
if pdu is None:
|
||||
pdu = bytes([self.op_code]) + HCI_Object.dict_to_bytes(kwargs, self.fields)
|
||||
self.pdu = pdu
|
||||
return subclass
|
||||
|
||||
def init_from_bytes(self, pdu, offset):
|
||||
return HCI_Object.init_from_bytes(self, pdu, offset, self.fields)
|
||||
@@ -309,67 +261,91 @@ class ATT_PDU:
|
||||
def has_authentication_signature(self):
|
||||
return ((self.op_code >> 7) & 1) == 1
|
||||
|
||||
def __bytes__(self):
|
||||
return self.pdu
|
||||
@property
|
||||
def payload(self) -> bytes:
|
||||
if self._payload is None:
|
||||
self._payload = HCI_Object.dict_to_bytes(self.__dict__, self.fields)
|
||||
return self._payload
|
||||
|
||||
@payload.setter
|
||||
def payload(self, value: bytes):
|
||||
self._payload = value
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
return bytes([self.op_code]) + self.payload
|
||||
|
||||
def __str__(self):
|
||||
result = color(self.name, 'yellow')
|
||||
if fields := getattr(self, 'fields', None):
|
||||
result += ':\n' + HCI_Object.format_fields(self.__dict__, fields, ' ')
|
||||
else:
|
||||
if len(self.pdu) > 1:
|
||||
result += f': {self.pdu.hex()}'
|
||||
if self.payload:
|
||||
result += f': {self.payload.hex()}'
|
||||
return result
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass(
|
||||
[
|
||||
('request_opcode_in_error', {'size': 1, 'mapper': ATT_PDU.pdu_name}),
|
||||
('attribute_handle_in_error', HANDLE_FIELD_SPEC),
|
||||
('error_code', {'size': 1, 'mapper': ATT_PDU.error_name}),
|
||||
]
|
||||
)
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Error_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.1.1 Error Response
|
||||
'''
|
||||
|
||||
request_opcode_in_error: int = dataclasses.field(metadata=Opcode.type_metadata(1))
|
||||
attribute_handle_in_error: int = dataclasses.field(
|
||||
metadata=hci.metadata(HANDLE_FIELD_SPEC)
|
||||
)
|
||||
error_code: int = dataclasses.field(metadata=ErrorCode.type_metadata(1))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([('client_rx_mtu', 2)])
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Exchange_MTU_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.2.1 Exchange MTU Request
|
||||
'''
|
||||
|
||||
client_rx_mtu: int = dataclasses.field(metadata=hci.metadata(2))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([('server_rx_mtu', 2)])
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Exchange_MTU_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.2.2 Exchange MTU Response
|
||||
'''
|
||||
|
||||
server_rx_mtu: int = dataclasses.field(metadata=hci.metadata(2))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass(
|
||||
[('starting_handle', HANDLE_FIELD_SPEC), ('ending_handle', HANDLE_FIELD_SPEC)]
|
||||
)
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Find_Information_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.3.1 Find Information Request
|
||||
'''
|
||||
|
||||
starting_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
ending_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([('format', 1), ('information_data', '*')])
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Find_Information_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.3.2 Find Information Response
|
||||
'''
|
||||
|
||||
def parse_information_data(self):
|
||||
format: int = dataclasses.field(metadata=hci.metadata(1))
|
||||
information_data: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
information: list[tuple[int, bytes]] = dataclasses.field(init=False)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self.information = []
|
||||
offset = 0
|
||||
uuid_size = 2 if self.format == 1 else 16
|
||||
@@ -379,14 +355,6 @@ class ATT_Find_Information_Response(ATT_PDU):
|
||||
self.information.append((handle, uuid))
|
||||
offset += 2 + uuid_size
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.parse_information_data()
|
||||
|
||||
def init_from_bytes(self, pdu, offset):
|
||||
super().init_from_bytes(pdu, offset)
|
||||
self.parse_information_data()
|
||||
|
||||
def __str__(self):
|
||||
result = color(self.name, 'yellow')
|
||||
result += ':\n' + HCI_Object.format_fields(
|
||||
@@ -408,28 +376,31 @@ class ATT_Find_Information_Response(ATT_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass(
|
||||
[
|
||||
('starting_handle', HANDLE_FIELD_SPEC),
|
||||
('ending_handle', HANDLE_FIELD_SPEC),
|
||||
('attribute_type', UUID_2_FIELD_SPEC),
|
||||
('attribute_value', '*'),
|
||||
]
|
||||
)
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Find_By_Type_Value_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.3.3 Find By Type Value Request
|
||||
'''
|
||||
|
||||
starting_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
ending_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
attribute_type: UUID = dataclasses.field(metadata=hci.metadata(UUID.parse_uuid_2))
|
||||
attribute_value: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([('handles_information_list', '*')])
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Find_By_Type_Value_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.3.4 Find By Type Value Response
|
||||
'''
|
||||
|
||||
def parse_handles_information_list(self):
|
||||
handles_information_list: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
handles_information: list[tuple[int, int]] = dataclasses.field(init=False)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self.handles_information = []
|
||||
offset = 0
|
||||
while offset + 4 <= len(self.handles_information_list):
|
||||
@@ -439,14 +410,6 @@ class ATT_Find_By_Type_Value_Response(ATT_PDU):
|
||||
self.handles_information.append((found_attribute_handle, group_end_handle))
|
||||
offset += 4
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.parse_handles_information_list()
|
||||
|
||||
def init_from_bytes(self, pdu, offset):
|
||||
super().init_from_bytes(pdu, offset)
|
||||
self.parse_handles_information_list()
|
||||
|
||||
def __str__(self):
|
||||
result = color(self.name, 'yellow')
|
||||
result += ':\n' + HCI_Object.format_fields(
|
||||
@@ -470,27 +433,31 @@ class ATT_Find_By_Type_Value_Response(ATT_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass(
|
||||
[
|
||||
('starting_handle', HANDLE_FIELD_SPEC),
|
||||
('ending_handle', HANDLE_FIELD_SPEC),
|
||||
('attribute_type', UUID_2_16_FIELD_SPEC),
|
||||
]
|
||||
)
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Read_By_Type_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.1 Read By Type Request
|
||||
'''
|
||||
|
||||
starting_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
ending_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
attribute_type: UUID = dataclasses.field(metadata=hci.metadata(UUID.parse_uuid))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([('length', 1), ('attribute_data_list', '*')])
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Read_By_Type_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.2 Read By Type Response
|
||||
'''
|
||||
|
||||
def parse_attribute_data_list(self):
|
||||
length: int = dataclasses.field(metadata=hci.metadata(1))
|
||||
attribute_data_list: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
attributes: list[tuple[int, bytes]] = dataclasses.field(init=False)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self.attributes = []
|
||||
offset = 0
|
||||
while self.length != 0 and offset + self.length <= len(
|
||||
@@ -505,14 +472,6 @@ class ATT_Read_By_Type_Response(ATT_PDU):
|
||||
self.attributes.append((attribute_handle, attribute_value))
|
||||
offset += self.length
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.parse_attribute_data_list()
|
||||
|
||||
def init_from_bytes(self, pdu, offset):
|
||||
super().init_from_bytes(pdu, offset)
|
||||
self.parse_attribute_data_list()
|
||||
|
||||
def __str__(self):
|
||||
result = color(self.name, 'yellow')
|
||||
result += ':\n' + HCI_Object.format_fields(
|
||||
@@ -534,75 +493,100 @@ class ATT_Read_By_Type_Response(ATT_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([('attribute_handle', HANDLE_FIELD_SPEC)])
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Read_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.3 Read Request
|
||||
'''
|
||||
|
||||
attribute_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([('attribute_value', '*')])
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Read_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.4 Read Response
|
||||
'''
|
||||
|
||||
attribute_value: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([('attribute_handle', HANDLE_FIELD_SPEC), ('value_offset', 2)])
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Read_Blob_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.5 Read Blob Request
|
||||
'''
|
||||
|
||||
attribute_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
value_offset: int = dataclasses.field(metadata=hci.metadata(2))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([('part_attribute_value', '*')])
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Read_Blob_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.6 Read Blob Response
|
||||
'''
|
||||
|
||||
part_attribute_value: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([('set_of_handles', '*')])
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Read_Multiple_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.7 Read Multiple Request
|
||||
'''
|
||||
|
||||
set_of_handles: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([('set_of_values', '*')])
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Read_Multiple_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.8 Read Multiple Response
|
||||
'''
|
||||
|
||||
set_of_values: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass(
|
||||
[
|
||||
('starting_handle', HANDLE_FIELD_SPEC),
|
||||
('ending_handle', HANDLE_FIELD_SPEC),
|
||||
('attribute_group_type', UUID_2_16_FIELD_SPEC),
|
||||
]
|
||||
)
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Read_By_Group_Type_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.9 Read by Group Type Request
|
||||
'''
|
||||
|
||||
starting_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
ending_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
attribute_group_type: UUID = dataclasses.field(
|
||||
metadata=hci.metadata(UUID.parse_uuid)
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([('length', 1), ('attribute_data_list', '*')])
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Read_By_Group_Type_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.10 Read by Group Type Response
|
||||
'''
|
||||
|
||||
def parse_attribute_data_list(self):
|
||||
length: int = dataclasses.field(metadata=hci.metadata(1))
|
||||
attribute_data_list: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
attributes: list[tuple[int, int, bytes]] = dataclasses.field(init=False)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self.attributes = []
|
||||
offset = 0
|
||||
while self.length != 0 and offset + self.length <= len(
|
||||
@@ -619,14 +603,6 @@ class ATT_Read_By_Group_Type_Response(ATT_PDU):
|
||||
)
|
||||
offset += self.length
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.parse_attribute_data_list()
|
||||
|
||||
def init_from_bytes(self, pdu, offset):
|
||||
super().init_from_bytes(pdu, offset)
|
||||
self.parse_attribute_data_list()
|
||||
|
||||
def __str__(self):
|
||||
result = color(self.name, 'yellow')
|
||||
result += ':\n' + HCI_Object.format_fields(
|
||||
@@ -651,15 +627,20 @@ class ATT_Read_By_Group_Type_Response(ATT_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([('attribute_handle', HANDLE_FIELD_SPEC), ('attribute_value', '*')])
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Write_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.5.1 Write Request
|
||||
'''
|
||||
|
||||
attribute_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
attribute_value: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([])
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Write_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.5.2 Write Response
|
||||
@@ -667,65 +648,70 @@ class ATT_Write_Response(ATT_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([('attribute_handle', HANDLE_FIELD_SPEC), ('attribute_value', '*')])
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Write_Command(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.5.3 Write Command
|
||||
'''
|
||||
|
||||
attribute_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
attribute_value: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass(
|
||||
[
|
||||
('attribute_handle', HANDLE_FIELD_SPEC),
|
||||
('attribute_value', '*'),
|
||||
# ('authentication_signature', 'TODO')
|
||||
]
|
||||
)
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Signed_Write_Command(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.5.4 Signed Write Command
|
||||
'''
|
||||
|
||||
attribute_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
attribute_value: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
# TODO: authentication_signature
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass(
|
||||
[
|
||||
('attribute_handle', HANDLE_FIELD_SPEC),
|
||||
('value_offset', 2),
|
||||
('part_attribute_value', '*'),
|
||||
]
|
||||
)
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Prepare_Write_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.6.1 Prepare Write Request
|
||||
'''
|
||||
|
||||
attribute_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
value_offset: int = dataclasses.field(metadata=hci.metadata(2))
|
||||
part_attribute_value: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass(
|
||||
[
|
||||
('attribute_handle', HANDLE_FIELD_SPEC),
|
||||
('value_offset', 2),
|
||||
('part_attribute_value', '*'),
|
||||
]
|
||||
)
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Prepare_Write_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.6.2 Prepare Write Response
|
||||
'''
|
||||
|
||||
attribute_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
value_offset: int = dataclasses.field(metadata=hci.metadata(2))
|
||||
part_attribute_value: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([("flags", 1)])
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Execute_Write_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.6.3 Execute Write Request
|
||||
'''
|
||||
|
||||
flags: int = dataclasses.field(metadata=hci.metadata(1))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([])
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Execute_Write_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.6.4 Execute Write Response
|
||||
@@ -733,23 +719,32 @@ class ATT_Execute_Write_Response(ATT_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([('attribute_handle', HANDLE_FIELD_SPEC), ('attribute_value', '*')])
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Handle_Value_Notification(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.7.1 Handle Value Notification
|
||||
'''
|
||||
|
||||
attribute_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
attribute_value: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([('attribute_handle', HANDLE_FIELD_SPEC), ('attribute_value', '*')])
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Handle_Value_Indication(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.7.2 Handle Value Indication
|
||||
'''
|
||||
|
||||
attribute_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
attribute_value: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([])
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Handle_Value_Confirmation(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.7.3 Handle Value Confirmation
|
||||
|
||||
@@ -17,20 +17,16 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import abc
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import enum
|
||||
import logging
|
||||
import pathlib
|
||||
from typing import (
|
||||
AsyncGenerator,
|
||||
BinaryIO,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
import sys
|
||||
import wave
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from typing import TYPE_CHECKING, AsyncGenerator, BinaryIO
|
||||
|
||||
from bumble.colors import color
|
||||
|
||||
@@ -230,8 +226,8 @@ class SoundDeviceAudioOutput(ThreadedAudioOutput):
|
||||
|
||||
try:
|
||||
self._stream.write(pcm_samples)
|
||||
except Exception as error:
|
||||
print(f'Sound device error: {error}')
|
||||
except Exception:
|
||||
logger.exception('Sound device error')
|
||||
raise
|
||||
|
||||
def _close(self):
|
||||
|
||||
@@ -16,12 +16,12 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
import struct
|
||||
from typing import Union
|
||||
|
||||
from bumble import core
|
||||
from bumble import utils
|
||||
from bumble import core, utils
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -16,15 +16,14 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
from enum import IntEnum
|
||||
|
||||
import logging
|
||||
import struct
|
||||
from typing import Callable, cast, Optional
|
||||
from enum import IntEnum
|
||||
from typing import Callable, Optional, cast
|
||||
|
||||
from bumble import avc, core, l2cap
|
||||
from bumble.colors import color
|
||||
from bumble import avc
|
||||
from bumble import core
|
||||
from bumble import l2cap
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -137,8 +136,8 @@ class MessageAssembler:
|
||||
self.pid,
|
||||
self.payload,
|
||||
)
|
||||
except Exception as error:
|
||||
logger.exception(color(f"!!! exception in callback: {error}", "red"))
|
||||
except Exception:
|
||||
logger.exception(color("!!! exception in callback", "red"))
|
||||
|
||||
self.reset()
|
||||
|
||||
|
||||
@@ -16,31 +16,25 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
import logging
|
||||
import enum
|
||||
import logging
|
||||
import time
|
||||
import warnings
|
||||
from typing import (
|
||||
Any,
|
||||
Awaitable,
|
||||
Optional,
|
||||
Callable,
|
||||
AsyncGenerator,
|
||||
Awaitable,
|
||||
Callable,
|
||||
Iterable,
|
||||
Union,
|
||||
Optional,
|
||||
SupportsBytes,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
|
||||
|
||||
from bumble.core import (
|
||||
BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE,
|
||||
InvalidStateError,
|
||||
ProtocolError,
|
||||
InvalidArgumentError,
|
||||
name_or_number,
|
||||
)
|
||||
from bumble import device, l2cap, sdp, utils
|
||||
from bumble.a2dp import (
|
||||
A2DP_CODEC_TYPE_NAMES,
|
||||
A2DP_MPEG_2_4_AAC_CODEC_TYPE,
|
||||
@@ -51,10 +45,15 @@ from bumble.a2dp import (
|
||||
SbcMediaCodecInformation,
|
||||
VendorSpecificMediaCodecInformation,
|
||||
)
|
||||
from bumble.rtp import MediaPacket
|
||||
from bumble import sdp, device, l2cap, utils
|
||||
from bumble.colors import color
|
||||
|
||||
from bumble.core import (
|
||||
BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE,
|
||||
InvalidArgumentError,
|
||||
InvalidStateError,
|
||||
ProtocolError,
|
||||
name_or_number,
|
||||
)
|
||||
from bumble.rtp import MediaPacket
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -434,8 +433,8 @@ class MessageAssembler:
|
||||
)
|
||||
try:
|
||||
self.callback(self.transaction_label, message)
|
||||
except Exception as error:
|
||||
logger.exception(color(f'!!! exception in callback: {error}', 'red'))
|
||||
except Exception:
|
||||
logger.exception(color('!!! exception in callback', 'red'))
|
||||
|
||||
self.reset()
|
||||
|
||||
@@ -1400,10 +1399,8 @@ class Protocol(utils.EventEmitter):
|
||||
try:
|
||||
response = handler(message)
|
||||
self.send_message(transaction_label, response)
|
||||
except Exception as error:
|
||||
logger.warning(
|
||||
f'{color("!!! Exception in handler:", "red")} {error}'
|
||||
)
|
||||
except Exception:
|
||||
logger.exception(color("!!! Exception in handler:", "red"))
|
||||
else:
|
||||
logger.warning('unhandled command')
|
||||
else:
|
||||
|
||||
1766
bumble/avrcp.py
1766
bumble/avrcp.py
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,9 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from bumble import core
|
||||
|
||||
@@ -17,17 +17,17 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import itertools
|
||||
import logging
|
||||
import random
|
||||
import struct
|
||||
from bumble.colors import color
|
||||
from bumble.core import (
|
||||
PhysicalTransport,
|
||||
)
|
||||
from typing import TYPE_CHECKING, Any, Optional, Union
|
||||
|
||||
from bumble import hci
|
||||
from bumble.colors import color
|
||||
from bumble.core import PhysicalTransport
|
||||
from bumble.hci import (
|
||||
HCI_ACL_DATA_PACKET,
|
||||
HCI_COMMAND_DISALLOWED_ERROR,
|
||||
@@ -38,13 +38,12 @@ from bumble.hci import (
|
||||
HCI_EVENT_PACKET,
|
||||
HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR,
|
||||
HCI_LE_1M_PHY,
|
||||
HCI_SUCCESS,
|
||||
HCI_UNKNOWN_HCI_COMMAND_ERROR,
|
||||
HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR,
|
||||
HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR,
|
||||
HCI_SUCCESS,
|
||||
HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR,
|
||||
HCI_UNKNOWN_HCI_COMMAND_ERROR,
|
||||
HCI_VERSION_BLUETOOTH_CORE_5_0,
|
||||
Address,
|
||||
Role,
|
||||
HCI_AclDataPacket,
|
||||
HCI_AclDataPacketAssembler,
|
||||
HCI_Command_Complete_Event,
|
||||
@@ -53,7 +52,6 @@ from bumble.hci import (
|
||||
HCI_Connection_Request_Event,
|
||||
HCI_Disconnection_Complete_Event,
|
||||
HCI_Encryption_Change_Event,
|
||||
HCI_Synchronous_Connection_Complete_Event,
|
||||
HCI_LE_Advertising_Report_Event,
|
||||
HCI_LE_CIS_Established_Event,
|
||||
HCI_LE_CIS_Request_Event,
|
||||
@@ -62,8 +60,9 @@ from bumble.hci import (
|
||||
HCI_Number_Of_Completed_Packets_Event,
|
||||
HCI_Packet,
|
||||
HCI_Role_Change_Event,
|
||||
HCI_Synchronous_Connection_Complete_Event,
|
||||
Role,
|
||||
)
|
||||
from typing import Optional, Union, Any, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bumble.link import LocalLink
|
||||
@@ -370,6 +369,12 @@ class Controller:
|
||||
return connection
|
||||
return None
|
||||
|
||||
def find_peripheral_connection_by_handle(self, handle):
|
||||
for connection in self.peripheral_connections.values():
|
||||
if connection.handle == handle:
|
||||
return connection
|
||||
return None
|
||||
|
||||
def find_classic_connection_by_handle(self, handle):
|
||||
for connection in self.classic_connections.values():
|
||||
if connection.handle == handle:
|
||||
@@ -414,7 +419,7 @@ class Controller:
|
||||
)
|
||||
)
|
||||
|
||||
def on_link_central_disconnected(self, peer_address, reason):
|
||||
def on_link_disconnected(self, peer_address, reason):
|
||||
'''
|
||||
Called when an active disconnection occurs from a peer
|
||||
'''
|
||||
@@ -431,6 +436,17 @@ class Controller:
|
||||
|
||||
# Remove the connection
|
||||
del self.peripheral_connections[peer_address]
|
||||
elif connection := self.central_connections.get(peer_address):
|
||||
self.send_hci_packet(
|
||||
HCI_Disconnection_Complete_Event(
|
||||
status=HCI_SUCCESS,
|
||||
connection_handle=connection.handle,
|
||||
reason=reason,
|
||||
)
|
||||
)
|
||||
|
||||
# Remove the connection
|
||||
del self.central_connections[peer_address]
|
||||
else:
|
||||
logger.warning(f'!!! No peripheral connection found for {peer_address}')
|
||||
|
||||
@@ -479,7 +495,7 @@ class Controller:
|
||||
)
|
||||
)
|
||||
|
||||
def on_link_peripheral_disconnection_complete(self, disconnection_command, status):
|
||||
def on_link_disconnection_complete(self, disconnection_command, status):
|
||||
'''
|
||||
Called when a disconnection has been completed
|
||||
'''
|
||||
@@ -499,26 +515,11 @@ class Controller:
|
||||
):
|
||||
logger.debug(f'CENTRAL Connection removed: {connection}')
|
||||
del self.central_connections[connection.peer_address]
|
||||
|
||||
def on_link_peripheral_disconnected(self, peer_address):
|
||||
'''
|
||||
Called when a connection to a peripheral is broken
|
||||
'''
|
||||
|
||||
# Send a disconnection complete event
|
||||
if connection := self.central_connections.get(peer_address):
|
||||
self.send_hci_packet(
|
||||
HCI_Disconnection_Complete_Event(
|
||||
status=HCI_SUCCESS,
|
||||
connection_handle=connection.handle,
|
||||
reason=HCI_CONNECTION_TIMEOUT_ERROR,
|
||||
)
|
||||
)
|
||||
|
||||
# Remove the connection
|
||||
del self.central_connections[peer_address]
|
||||
else:
|
||||
logger.warning(f'!!! No central connection found for {peer_address}')
|
||||
elif connection := self.find_peripheral_connection_by_handle(
|
||||
disconnection_command.connection_handle
|
||||
):
|
||||
logger.debug(f'PERIPHERAL Connection removed: {connection}')
|
||||
del self.peripheral_connections[connection.peer_address]
|
||||
|
||||
def on_link_encrypted(self, peer_address, _rand, _ediv, _ltk):
|
||||
# For now, just setup the encryption without asking the host
|
||||
@@ -877,6 +878,14 @@ class Controller:
|
||||
else:
|
||||
# Remove the connection
|
||||
del self.central_connections[connection.peer_address]
|
||||
elif connection := self.find_peripheral_connection_by_handle(handle):
|
||||
if self.link:
|
||||
self.link.disconnect(
|
||||
self.random_address, connection.peer_address, command
|
||||
)
|
||||
else:
|
||||
# Remove the connection
|
||||
del self.peripheral_connections[connection.peer_address]
|
||||
elif connection := self.find_classic_connection_by_handle(handle):
|
||||
if self.link:
|
||||
self.link.classic_disconnect(
|
||||
@@ -1259,6 +1268,56 @@ class Controller:
|
||||
)
|
||||
return bytes([HCI_SUCCESS]) + bd_addr
|
||||
|
||||
def on_hci_le_set_default_subrate_command(
|
||||
self, command: hci.HCI_LE_Set_Default_Subrate_Command
|
||||
):
|
||||
'''
|
||||
See Bluetooth spec Vol 6, Part E - 7.8.123 LE Set Event Mask Command
|
||||
'''
|
||||
|
||||
if (
|
||||
command.subrate_max * (command.max_latency) > 500
|
||||
or command.subrate_max < command.subrate_min
|
||||
or command.continuation_number >= command.subrate_max
|
||||
):
|
||||
return bytes([HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR])
|
||||
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_le_subrate_request_command(
|
||||
self, command: hci.HCI_LE_Subrate_Request_Command
|
||||
):
|
||||
'''
|
||||
See Bluetooth spec Vol 6, Part E - 7.8.124 LE Subrate Request command
|
||||
'''
|
||||
if (
|
||||
command.subrate_max * (command.max_latency) > 500
|
||||
or command.continuation_number < command.continuation_number
|
||||
or command.subrate_max < command.subrate_min
|
||||
or command.continuation_number >= command.subrate_max
|
||||
):
|
||||
return bytes([HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR])
|
||||
|
||||
self.send_hci_packet(
|
||||
hci.HCI_Command_Status_Event(
|
||||
status=hci.HCI_SUCCESS,
|
||||
num_hci_command_packets=1,
|
||||
command_opcode=command.op_code,
|
||||
)
|
||||
)
|
||||
|
||||
self.send_hci_packet(
|
||||
hci.HCI_LE_Subrate_Change_Event(
|
||||
status=hci.HCI_SUCCESS,
|
||||
connection_handle=command.connection_handle,
|
||||
subrate_factor=2,
|
||||
peripheral_latency=2,
|
||||
continuation_number=command.continuation_number,
|
||||
supervision_timeout=command.supervision_timeout,
|
||||
)
|
||||
)
|
||||
return None
|
||||
|
||||
def on_hci_le_set_event_mask_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 4, Part E - 7.8.1 LE Set Event Mask Command
|
||||
@@ -1805,3 +1864,11 @@ class Controller:
|
||||
See Bluetooth spec Vol 4, Part E - 7.8.110 LE Remove ISO Data Path Command
|
||||
'''
|
||||
return struct.pack('<BH', HCI_SUCCESS, command.connection_handle)
|
||||
|
||||
def on_hci_le_set_host_feature_command(
|
||||
self, _command: hci.HCI_LE_Set_Host_Feature_Command
|
||||
):
|
||||
'''
|
||||
See Bluetooth spec Vol 4, Part E - 7.8.115 LE Set Host Feature command
|
||||
'''
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
814
bumble/core.py
814
bumble/core.py
File diff suppressed because it is too large
Load Diff
@@ -22,12 +22,12 @@ import operator
|
||||
import secrets
|
||||
|
||||
try:
|
||||
from bumble.crypto.cryptography import EccKey, e, aes_cmac
|
||||
from bumble.crypto.cryptography import EccKey, aes_cmac, e
|
||||
except ImportError:
|
||||
logging.getLogger(__name__).debug(
|
||||
"Unable to import cryptography, use built-in primitives."
|
||||
)
|
||||
from bumble.crypto.builtin import EccKey, e, aes_cmac # type: ignore[assignment]
|
||||
from bumble.crypto.builtin import EccKey, aes_cmac, e # type: ignore[assignment]
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -24,9 +24,9 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import dataclasses
|
||||
import functools
|
||||
import copy
|
||||
import secrets
|
||||
import struct
|
||||
from typing import Optional
|
||||
|
||||
@@ -16,11 +16,9 @@ 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 import ciphers, cmac
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
from cryptography.hazmat.primitives import cmac
|
||||
from cryptography.hazmat.primitives.ciphers import algorithms, modes
|
||||
|
||||
|
||||
def e(key: bytes, data: bytes) -> bytes:
|
||||
|
||||
1025
bumble/data_types.py
Normal file
1025
bumble/data_types.py
Normal file
File diff suppressed because it is too large
Load Diff
373
bumble/device.py
373
bumble/device.py
@@ -16,24 +16,22 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import collections
|
||||
from collections.abc import Iterable, Sequence
|
||||
from contextlib import (
|
||||
asynccontextmanager,
|
||||
AsyncExitStack,
|
||||
closing,
|
||||
)
|
||||
import copy
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum, IntEnum
|
||||
import functools
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
import secrets
|
||||
import sys
|
||||
from collections.abc import Iterable, Sequence
|
||||
from contextlib import AsyncExitStack, asynccontextmanager, closing
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum, IntEnum
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Awaitable,
|
||||
Callable,
|
||||
@@ -43,47 +41,47 @@ from typing import (
|
||||
Union,
|
||||
cast,
|
||||
overload,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
|
||||
from bumble.colors import color
|
||||
from bumble import (
|
||||
core,
|
||||
data_types,
|
||||
gatt,
|
||||
gatt_client,
|
||||
gatt_server,
|
||||
hci,
|
||||
l2cap,
|
||||
pairing,
|
||||
sdp,
|
||||
smp,
|
||||
utils,
|
||||
)
|
||||
from bumble.att import ATT_CID, ATT_DEFAULT_MTU, ATT_PDU
|
||||
from bumble.gatt import Attribute, Characteristic, Descriptor, Service
|
||||
from bumble.host import DataPacketQueue, Host
|
||||
from bumble.profiles.gap import GenericAccessService
|
||||
from bumble.colors import color
|
||||
from bumble.core import (
|
||||
PhysicalTransport,
|
||||
AdvertisingData,
|
||||
BaseBumbleError,
|
||||
ConnectionParameterUpdateError,
|
||||
CommandTimeoutError,
|
||||
ConnectionParameterUpdateError,
|
||||
ConnectionPHY,
|
||||
InvalidArgumentError,
|
||||
InvalidOperationError,
|
||||
InvalidStateError,
|
||||
NotSupportedError,
|
||||
OutOfResourcesError,
|
||||
PhysicalTransport,
|
||||
UnreachableError,
|
||||
)
|
||||
from bumble import utils
|
||||
from bumble.keys import (
|
||||
KeyStore,
|
||||
PairingKeys,
|
||||
)
|
||||
from bumble import hci
|
||||
from bumble import pairing
|
||||
from bumble import gatt_client
|
||||
from bumble import gatt_server
|
||||
from bumble import smp
|
||||
from bumble import sdp
|
||||
from bumble import l2cap
|
||||
from bumble import core
|
||||
from bumble.gatt import Attribute, Characteristic, Descriptor, Service
|
||||
from bumble.host import DataPacketQueue, Host
|
||||
from bumble.keys import KeyStore, PairingKeys
|
||||
from bumble.profiles import gatt_service
|
||||
from bumble.profiles.gap import GenericAccessService
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bumble.transport.common import TransportSource, TransportSink
|
||||
from bumble.transport.common import TransportSink, TransportSource
|
||||
|
||||
_T = TypeVar('_T')
|
||||
|
||||
@@ -267,7 +265,7 @@ class ExtendedAdvertisement(Advertisement):
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class AdvertisementDataAccumulator:
|
||||
def __init__(self, passive=False):
|
||||
def __init__(self, passive: bool = False):
|
||||
self.passive = passive
|
||||
self.last_advertisement = None
|
||||
self.last_data = b''
|
||||
@@ -1166,14 +1164,11 @@ class BigSync(utils.EventEmitter):
|
||||
logger.error('BIG Sync %d is not active.', self.big_handle)
|
||||
return
|
||||
|
||||
with closing(utils.EventWatcher()) as watcher:
|
||||
terminated = asyncio.Event()
|
||||
watcher.once(self, BigSync.Event.TERMINATION, lambda _: terminated.set())
|
||||
await self.device.send_command(
|
||||
hci.HCI_LE_BIG_Terminate_Sync_Command(big_handle=self.big_handle),
|
||||
check_result=True,
|
||||
)
|
||||
await terminated.wait()
|
||||
await self.device.send_command(
|
||||
hci.HCI_LE_BIG_Terminate_Sync_Command(big_handle=self.big_handle),
|
||||
check_result=True,
|
||||
)
|
||||
self.state = BigSync.State.TERMINATED
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -1249,7 +1244,7 @@ class LePhyOptions:
|
||||
PREFER_S_2_CODED_PHY = 1
|
||||
PREFER_S_8_CODED_PHY = 2
|
||||
|
||||
def __init__(self, coded_phy_preference=0):
|
||||
def __init__(self, coded_phy_preference: int = 0):
|
||||
self.coded_phy_preference = coded_phy_preference
|
||||
|
||||
def __int__(self):
|
||||
@@ -1697,6 +1692,7 @@ class Connection(utils.CompositeEventEmitter):
|
||||
self_address: hci.Address
|
||||
self_resolvable_address: Optional[hci.Address]
|
||||
peer_address: hci.Address
|
||||
peer_name: Optional[str]
|
||||
peer_resolvable_address: Optional[hci.Address]
|
||||
peer_le_features: Optional[hci.LeFeatureMask]
|
||||
role: hci.Role
|
||||
@@ -1752,6 +1748,8 @@ class Connection(utils.CompositeEventEmitter):
|
||||
EVENT_CIS_REQUEST = "cis_request"
|
||||
EVENT_CIS_ESTABLISHMENT = "cis_establishment"
|
||||
EVENT_CIS_ESTABLISHMENT_FAILURE = "cis_establishment_failure"
|
||||
EVENT_LE_SUBRATE_CHANGE = "le_subrate_change"
|
||||
EVENT_LE_SUBRATE_CHANGE_FAILURE = "le_subrate_change_failure"
|
||||
|
||||
@utils.composite_listener
|
||||
class Listener:
|
||||
@@ -1787,6 +1785,12 @@ class Connection(utils.CompositeEventEmitter):
|
||||
connection_interval: float # Connection interval, in milliseconds. [LE only]
|
||||
peripheral_latency: int # Peripheral latency, in number of intervals. [LE only]
|
||||
supervision_timeout: float # Supervision timeout, in milliseconds.
|
||||
subrate_factor: int = (
|
||||
1 # See Bluetooth spec Vol 6, Part B - 4.5.1 Connection events
|
||||
)
|
||||
continuation_number: int = (
|
||||
0 # See Bluetooth spec Vol 6, Part B - 4.5.1 Connection events
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -1878,16 +1882,6 @@ class Connection(utils.CompositeEventEmitter):
|
||||
def send_l2cap_pdu(self, cid: int, pdu: bytes) -> None:
|
||||
self.device.send_l2cap_pdu(self.handle, cid, pdu)
|
||||
|
||||
@utils.deprecated("Please use create_l2cap_channel()")
|
||||
async def open_l2cap_channel(
|
||||
self,
|
||||
psm,
|
||||
max_credits=DEVICE_DEFAULT_L2CAP_COC_MAX_CREDITS,
|
||||
mtu=DEVICE_DEFAULT_L2CAP_COC_MTU,
|
||||
mps=DEVICE_DEFAULT_L2CAP_COC_MPS,
|
||||
):
|
||||
return await self.device.open_l2cap_channel(self, psm, max_credits, mtu, mps)
|
||||
|
||||
@overload
|
||||
async def create_l2cap_channel(
|
||||
self, spec: l2cap.ClassicChannelSpec
|
||||
@@ -1939,7 +1933,7 @@ class Connection(utils.CompositeEventEmitter):
|
||||
self.remove_listener(self.EVENT_DISCONNECTION, abort.set_result)
|
||||
self.remove_listener(self.EVENT_DISCONNECTION_FAILURE, abort.set_exception)
|
||||
|
||||
async def set_data_length(self, tx_octets, tx_time) -> None:
|
||||
async def set_data_length(self, tx_octets: int, tx_time: int) -> None:
|
||||
return await self.device.set_data_length(self, tx_octets, tx_time)
|
||||
|
||||
async def update_parameters(
|
||||
@@ -1969,7 +1963,12 @@ class Connection(utils.CompositeEventEmitter):
|
||||
use_l2cap=use_l2cap,
|
||||
)
|
||||
|
||||
async def set_phy(self, tx_phys=None, rx_phys=None, phy_options=None):
|
||||
async def set_phy(
|
||||
self,
|
||||
tx_phys: Optional[Iterable[hci.Phy]] = None,
|
||||
rx_phys: Optional[Iterable[hci.Phy]] = None,
|
||||
phy_options: int = 0,
|
||||
):
|
||||
return await self.device.set_connection_phy(self, tx_phys, rx_phys, phy_options)
|
||||
|
||||
async def get_phy(self) -> ConnectionPHY:
|
||||
@@ -2058,6 +2057,7 @@ class DeviceConfiguration:
|
||||
le_simultaneous_enabled: bool = False
|
||||
le_privacy_enabled: bool = False
|
||||
le_rpa_timeout: int = DEVICE_DEFAULT_LE_RPA_TIMEOUT
|
||||
le_subrate_enabled: bool = False
|
||||
classic_enabled: bool = False
|
||||
classic_sc_enabled: bool = True
|
||||
classic_ssp_enabled: bool = True
|
||||
@@ -2067,9 +2067,7 @@ class DeviceConfiguration:
|
||||
connectable: bool = True
|
||||
discoverable: bool = True
|
||||
advertising_data: bytes = bytes(
|
||||
AdvertisingData(
|
||||
[(AdvertisingData.COMPLETE_LOCAL_NAME, bytes(DEVICE_DEFAULT_NAME, 'utf-8'))]
|
||||
)
|
||||
AdvertisingData([data_types.CompleteLocalName(DEVICE_DEFAULT_NAME)])
|
||||
)
|
||||
irk: bytes = bytes(16) # This really must be changed for any level of security
|
||||
keystore: Optional[str] = None
|
||||
@@ -2113,9 +2111,7 @@ class DeviceConfiguration:
|
||||
self.advertising_data = bytes.fromhex(advertising_data)
|
||||
elif name is not None:
|
||||
self.advertising_data = bytes(
|
||||
AdvertisingData(
|
||||
[(AdvertisingData.COMPLETE_LOCAL_NAME, bytes(self.name, 'utf-8'))]
|
||||
)
|
||||
AdvertisingData([data_types.CompleteLocalName(self.name)])
|
||||
)
|
||||
|
||||
# Load scan response data
|
||||
@@ -2167,7 +2163,7 @@ class DeviceConfiguration:
|
||||
# Decorator that converts the first argument from a connection handle to a connection
|
||||
def with_connection_from_handle(function):
|
||||
@functools.wraps(function)
|
||||
def wrapper(self, connection_handle, *args, **kwargs):
|
||||
def wrapper(self, connection_handle: int, *args, **kwargs):
|
||||
if (connection := self.lookup_connection(connection_handle)) is None:
|
||||
raise ObjectLookupError(
|
||||
f'no connection for handle: 0x{connection_handle:04x}'
|
||||
@@ -2180,7 +2176,7 @@ def with_connection_from_handle(function):
|
||||
# Decorator that converts the first argument from a bluetooth address to a connection
|
||||
def with_connection_from_address(function):
|
||||
@functools.wraps(function)
|
||||
def wrapper(self, address, *args, **kwargs):
|
||||
def wrapper(self, address: hci.Address, *args, **kwargs):
|
||||
if connection := self.pending_connections.get(address, False):
|
||||
return function(self, connection, *args, **kwargs)
|
||||
for connection in self.connections.values():
|
||||
@@ -2410,6 +2406,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
self.le_privacy_enabled = config.le_privacy_enabled
|
||||
self.le_rpa_timeout = config.le_rpa_timeout
|
||||
self.le_rpa_periodic_update_task: Optional[asyncio.Task] = None
|
||||
self.le_subrate_enabled = config.le_subrate_enabled
|
||||
self.classic_enabled = config.classic_enabled
|
||||
self.cis_enabled = config.cis_enabled
|
||||
self.classic_sc_enabled = config.classic_sc_enabled
|
||||
@@ -2598,36 +2595,6 @@ class Device(utils.CompositeEventEmitter):
|
||||
None,
|
||||
)
|
||||
|
||||
@utils.deprecated("Please use create_l2cap_server()")
|
||||
def register_l2cap_server(self, psm, server) -> int:
|
||||
return self.l2cap_channel_manager.register_server(psm, server)
|
||||
|
||||
@utils.deprecated("Please use create_l2cap_server()")
|
||||
def register_l2cap_channel_server(
|
||||
self,
|
||||
psm,
|
||||
server,
|
||||
max_credits=DEVICE_DEFAULT_L2CAP_COC_MAX_CREDITS,
|
||||
mtu=DEVICE_DEFAULT_L2CAP_COC_MTU,
|
||||
mps=DEVICE_DEFAULT_L2CAP_COC_MPS,
|
||||
):
|
||||
return self.l2cap_channel_manager.register_le_coc_server(
|
||||
psm, server, max_credits, mtu, mps
|
||||
)
|
||||
|
||||
@utils.deprecated("Please use create_l2cap_channel()")
|
||||
async def open_l2cap_channel(
|
||||
self,
|
||||
connection,
|
||||
psm,
|
||||
max_credits=DEVICE_DEFAULT_L2CAP_COC_MAX_CREDITS,
|
||||
mtu=DEVICE_DEFAULT_L2CAP_COC_MTU,
|
||||
mps=DEVICE_DEFAULT_L2CAP_COC_MPS,
|
||||
):
|
||||
return await self.l2cap_channel_manager.open_le_coc(
|
||||
connection, psm, max_credits, mtu, mps
|
||||
)
|
||||
|
||||
@overload
|
||||
async def create_l2cap_channel(
|
||||
self,
|
||||
@@ -2695,7 +2662,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
def send_l2cap_pdu(self, connection_handle: int, cid: int, pdu: bytes) -> None:
|
||||
self.host.send_l2cap_pdu(connection_handle, cid, pdu)
|
||||
|
||||
async def send_command(self, command, check_result=False):
|
||||
async def send_command(self, command: hci.HCI_Command, check_result: bool = False):
|
||||
try:
|
||||
return await asyncio.wait_for(
|
||||
self.host.send_command(command, check_result), self.command_timeout
|
||||
@@ -2789,6 +2756,15 @@ class Device(utils.CompositeEventEmitter):
|
||||
check_result=True,
|
||||
)
|
||||
|
||||
if self.le_subrate_enabled:
|
||||
await self.send_command(
|
||||
hci.HCI_LE_Set_Host_Feature_Command(
|
||||
bit_number=hci.LeFeature.CONNECTION_SUBRATING_HOST_SUPPORT,
|
||||
bit_value=1,
|
||||
),
|
||||
check_result=True,
|
||||
)
|
||||
|
||||
if self.config.channel_sounding_enabled:
|
||||
await self.send_command(
|
||||
hci.HCI_LE_Set_Host_Feature_Command(
|
||||
@@ -2943,13 +2919,13 @@ class Device(utils.CompositeEventEmitter):
|
||||
def supports_le_features(self, feature: hci.LeFeatureMask) -> bool:
|
||||
return self.host.supports_le_features(feature)
|
||||
|
||||
def supports_le_phy(self, phy: int) -> bool:
|
||||
if phy == hci.HCI_LE_1M_PHY:
|
||||
def supports_le_phy(self, phy: hci.Phy) -> bool:
|
||||
if phy == hci.Phy.LE_1M:
|
||||
return True
|
||||
|
||||
feature_map: dict[int, hci.LeFeatureMask] = {
|
||||
hci.HCI_LE_2M_PHY: hci.LeFeatureMask.LE_2M_PHY,
|
||||
hci.HCI_LE_CODED_PHY: hci.LeFeatureMask.LE_CODED_PHY,
|
||||
feature_map: dict[hci.Phy, hci.LeFeatureMask] = {
|
||||
hci.Phy.LE_2M: hci.LeFeatureMask.LE_2M_PHY,
|
||||
hci.Phy.LE_CODED: hci.LeFeatureMask.LE_CODED_PHY,
|
||||
}
|
||||
if phy not in feature_map:
|
||||
raise InvalidArgumentError('invalid PHY')
|
||||
@@ -3226,8 +3202,8 @@ class Device(utils.CompositeEventEmitter):
|
||||
else 0
|
||||
)
|
||||
await advertising_set.start(duration=duration)
|
||||
except Exception as error:
|
||||
logger.exception(f'failed to start advertising set: {error}')
|
||||
except Exception:
|
||||
logger.exception('failed to start advertising set')
|
||||
await advertising_set.remove()
|
||||
raise
|
||||
|
||||
@@ -3553,7 +3529,9 @@ class Device(utils.CompositeEventEmitter):
|
||||
self.discovering = False
|
||||
|
||||
@host_event_handler
|
||||
def on_inquiry_result(self, address, class_of_device, data, rssi):
|
||||
def on_inquiry_result(
|
||||
self, address: hci.Address, class_of_device: int, data: bytes, rssi: int
|
||||
):
|
||||
self.emit(
|
||||
self.EVENT_INQUIRY_RESULT,
|
||||
address,
|
||||
@@ -3562,7 +3540,9 @@ class Device(utils.CompositeEventEmitter):
|
||||
rssi,
|
||||
)
|
||||
|
||||
async def set_scan_enable(self, inquiry_scan_enabled, page_scan_enabled):
|
||||
async def set_scan_enable(
|
||||
self, inquiry_scan_enabled: bool, page_scan_enabled: bool
|
||||
):
|
||||
if inquiry_scan_enabled and page_scan_enabled:
|
||||
scan_enable = 0x03
|
||||
elif page_scan_enabled:
|
||||
@@ -3582,14 +3562,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
# Synthesize an inquiry response if none is set already
|
||||
if self.inquiry_response is None:
|
||||
self.inquiry_response = bytes(
|
||||
AdvertisingData(
|
||||
[
|
||||
(
|
||||
AdvertisingData.COMPLETE_LOCAL_NAME,
|
||||
bytes(self.name, 'utf-8'),
|
||||
)
|
||||
]
|
||||
)
|
||||
AdvertisingData([data_types.CompleteLocalName(self.name)])
|
||||
)
|
||||
|
||||
# Update the controller
|
||||
@@ -3695,6 +3668,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
# If the address is not parsable, assume it is a name instead
|
||||
always_resolve = False
|
||||
logger.debug('looking for peer by name')
|
||||
assert isinstance(peer_address, str)
|
||||
peer_address = await self.find_peer_by_name(
|
||||
peer_address, transport
|
||||
) # TODO: timeout
|
||||
@@ -3722,7 +3696,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
):
|
||||
pending_connection.set_result(connection)
|
||||
|
||||
def on_connection_failure(error):
|
||||
def on_connection_failure(error: core.ConnectionError):
|
||||
if transport == PhysicalTransport.LE or (
|
||||
# match BR/EDR connection failure event against peer address
|
||||
error.transport == transport
|
||||
@@ -3942,6 +3916,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
except InvalidArgumentError:
|
||||
# If the address is not parsable, assume it is a name instead
|
||||
logger.debug('looking for peer by name')
|
||||
assert isinstance(peer_address, str)
|
||||
peer_address = await self.find_peer_by_name(
|
||||
peer_address, PhysicalTransport.BR_EDR
|
||||
) # TODO: timeout
|
||||
@@ -4000,7 +3975,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
):
|
||||
pending_connection.set_result(connection)
|
||||
|
||||
def on_connection_failure(error):
|
||||
def on_connection_failure(error: core.ConnectionError):
|
||||
if (
|
||||
error.transport == PhysicalTransport.BR_EDR
|
||||
and error.peer_address == peer_address
|
||||
@@ -4037,7 +4012,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
self.pending_connections.pop(peer_address, None)
|
||||
|
||||
@asynccontextmanager
|
||||
async def connect_as_gatt(self, peer_address):
|
||||
async def connect_as_gatt(self, peer_address: Union[hci.Address, str]):
|
||||
async with AsyncExitStack() as stack:
|
||||
connection = await stack.enter_async_context(
|
||||
await self.connect(peer_address)
|
||||
@@ -4073,6 +4048,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
except InvalidArgumentError:
|
||||
# If the address is not parsable, assume it is a name instead
|
||||
logger.debug('looking for peer by name')
|
||||
assert isinstance(peer_address, str)
|
||||
peer_address = await self.find_peer_by_name(
|
||||
peer_address, PhysicalTransport.BR_EDR
|
||||
) # TODO: timeout
|
||||
@@ -4118,7 +4094,9 @@ class Device(utils.CompositeEventEmitter):
|
||||
)
|
||||
self.disconnecting = False
|
||||
|
||||
async def set_data_length(self, connection, tx_octets, tx_time) -> None:
|
||||
async def set_data_length(
|
||||
self, connection: Connection, tx_octets: int, tx_time: int
|
||||
) -> None:
|
||||
if tx_octets < 0x001B or tx_octets > 0x00FB:
|
||||
raise InvalidArgumentError('tx_octets must be between 0x001B and 0x00FB')
|
||||
|
||||
@@ -4221,7 +4199,11 @@ class Device(utils.CompositeEventEmitter):
|
||||
)
|
||||
|
||||
async def set_connection_phy(
|
||||
self, connection, tx_phys=None, rx_phys=None, phy_options=None
|
||||
self,
|
||||
connection: Connection,
|
||||
tx_phys: Optional[Iterable[hci.Phy]] = None,
|
||||
rx_phys: Optional[Iterable[hci.Phy]] = None,
|
||||
phy_options: int = 0,
|
||||
):
|
||||
if not self.host.supports_command(hci.HCI_LE_SET_PHY_COMMAND):
|
||||
logger.warning('ignoring request, command not supported')
|
||||
@@ -4237,7 +4219,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
all_phys=all_phys_bits,
|
||||
tx_phys=hci.phy_list_to_bits(tx_phys),
|
||||
rx_phys=hci.phy_list_to_bits(rx_phys),
|
||||
phy_options=0 if phy_options is None else int(phy_options),
|
||||
phy_options=phy_options,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -4248,7 +4230,11 @@ class Device(utils.CompositeEventEmitter):
|
||||
)
|
||||
raise hci.HCI_StatusError(result)
|
||||
|
||||
async def set_default_phy(self, tx_phys=None, rx_phys=None):
|
||||
async def set_default_phy(
|
||||
self,
|
||||
tx_phys: Optional[Iterable[hci.Phy]] = None,
|
||||
rx_phys: Optional[Iterable[hci.Phy]] = None,
|
||||
):
|
||||
all_phys_bits = (1 if tx_phys is None else 0) | (
|
||||
(1 if rx_phys is None else 0) << 1
|
||||
)
|
||||
@@ -4286,7 +4272,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
check_result=True,
|
||||
)
|
||||
|
||||
async def find_peer_by_name(self, name, transport=PhysicalTransport.LE):
|
||||
async def find_peer_by_name(self, name: str, transport=PhysicalTransport.LE):
|
||||
"""
|
||||
Scan for a peer with a given name and return its address.
|
||||
"""
|
||||
@@ -4301,7 +4287,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
if local_name == name:
|
||||
peer_address.set_result(address)
|
||||
|
||||
listener = None
|
||||
listener: Optional[Callable[..., None]] = None
|
||||
was_scanning = self.scanning
|
||||
was_discovering = self.discovering
|
||||
try:
|
||||
@@ -4407,10 +4393,10 @@ class Device(utils.CompositeEventEmitter):
|
||||
def smp_session_proxy(self, session_proxy: type[smp.Session]) -> None:
|
||||
self.smp_manager.session_proxy = session_proxy
|
||||
|
||||
async def pair(self, connection):
|
||||
async def pair(self, connection: Connection):
|
||||
return await self.smp_manager.pair(connection)
|
||||
|
||||
def request_pairing(self, connection):
|
||||
def request_pairing(self, connection: Connection):
|
||||
return self.smp_manager.request_pairing(connection)
|
||||
|
||||
async def get_long_term_key(
|
||||
@@ -4498,7 +4484,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
on_authentication_failure,
|
||||
)
|
||||
|
||||
async def encrypt(self, connection, enable=True):
|
||||
async def encrypt(self, connection: Connection, enable: bool = True):
|
||||
if not enable and connection.transport == PhysicalTransport.LE:
|
||||
raise InvalidArgumentError('`enable` parameter is classic only.')
|
||||
|
||||
@@ -4508,7 +4494,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
def on_encryption_change():
|
||||
pending_encryption.set_result(None)
|
||||
|
||||
def on_encryption_failure(error_code):
|
||||
def on_encryption_failure(error_code: int):
|
||||
pending_encryption.set_exception(hci.HCI_Error(error_code))
|
||||
|
||||
connection.on(
|
||||
@@ -4591,8 +4577,8 @@ class Device(utils.CompositeEventEmitter):
|
||||
try:
|
||||
await self.keystore.update(address, keys)
|
||||
await self.refresh_resolving_list()
|
||||
except Exception as error:
|
||||
logger.warning(f'!!! error while storing keys: {error}')
|
||||
except Exception:
|
||||
logger.exception('!!! error while storing keys')
|
||||
else:
|
||||
self.emit(self.EVENT_KEY_STORE_UPDATE)
|
||||
|
||||
@@ -4600,10 +4586,10 @@ class Device(utils.CompositeEventEmitter):
|
||||
async def switch_role(self, connection: Connection, role: hci.Role):
|
||||
pending_role_change = asyncio.get_running_loop().create_future()
|
||||
|
||||
def on_role_change(new_role):
|
||||
def on_role_change(new_role: hci.Role):
|
||||
pending_role_change.set_result(new_role)
|
||||
|
||||
def on_role_change_failure(error_code):
|
||||
def on_role_change_failure(error_code: int):
|
||||
pending_role_change.set_exception(hci.HCI_Error(error_code))
|
||||
|
||||
connection.on(connection.EVENT_ROLE_CHANGE, on_role_change)
|
||||
@@ -5193,10 +5179,10 @@ class Device(utils.CompositeEventEmitter):
|
||||
):
|
||||
connection.emit(connection.EVENT_LINK_KEY)
|
||||
|
||||
def add_service(self, service):
|
||||
def add_service(self, service: gatt.Service):
|
||||
self.gatt_server.add_service(service)
|
||||
|
||||
def add_services(self, services):
|
||||
def add_services(self, services: Iterable[gatt.Service]):
|
||||
self.gatt_server.add_services(services)
|
||||
|
||||
def add_default_services(
|
||||
@@ -5292,10 +5278,10 @@ class Device(utils.CompositeEventEmitter):
|
||||
@host_event_handler
|
||||
def on_advertising_set_termination(
|
||||
self,
|
||||
status,
|
||||
advertising_handle,
|
||||
connection_handle,
|
||||
number_of_completed_extended_advertising_events,
|
||||
status: int,
|
||||
advertising_handle: int,
|
||||
connection_handle: int,
|
||||
number_of_completed_extended_advertising_events: int,
|
||||
):
|
||||
# Legacy advertising set is also one of extended advertising sets.
|
||||
if not (
|
||||
@@ -5594,7 +5580,12 @@ class Device(utils.CompositeEventEmitter):
|
||||
)
|
||||
|
||||
@host_event_handler
|
||||
def on_connection_failure(self, transport, peer_address, error_code):
|
||||
def on_connection_failure(
|
||||
self,
|
||||
transport: hci.PhysicalTransport,
|
||||
peer_address: hci.Address,
|
||||
error_code: int,
|
||||
):
|
||||
logger.debug(
|
||||
f'*** Connection failed: {hci.HCI_Constant.error_name(error_code)}'
|
||||
)
|
||||
@@ -5713,7 +5704,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
|
||||
@host_event_handler
|
||||
@with_connection_from_handle
|
||||
def on_connection_authentication(self, connection):
|
||||
def on_connection_authentication(self, connection: Connection):
|
||||
logger.debug(
|
||||
f'*** Connection Authentication: [0x{connection.handle:04X}] '
|
||||
f'{connection.peer_address} as {connection.role_name}'
|
||||
@@ -5723,7 +5714,9 @@ class Device(utils.CompositeEventEmitter):
|
||||
|
||||
@host_event_handler
|
||||
@with_connection_from_handle
|
||||
def on_connection_authentication_failure(self, connection, error):
|
||||
def on_connection_authentication_failure(
|
||||
self, connection: Connection, error: core.ConnectionError
|
||||
):
|
||||
logger.debug(
|
||||
f'*** Connection Authentication Failure: [0x{connection.handle:04X}] '
|
||||
f'{connection.peer_address} as {connection.role_name}, error={error}'
|
||||
@@ -5765,10 +5758,13 @@ class Device(utils.CompositeEventEmitter):
|
||||
@host_event_handler
|
||||
@with_connection_from_address
|
||||
def on_authentication_io_capability_response(
|
||||
self, connection, io_capability, authentication_requirements
|
||||
self,
|
||||
connection: Connection,
|
||||
io_capability: int,
|
||||
authentication_requirements: int,
|
||||
):
|
||||
connection.peer_pairing_io_capability = io_capability
|
||||
connection.peer_pairing_authentication_requirements = (
|
||||
connection.pairing_peer_io_capability = io_capability
|
||||
connection.pairing_peer_authentication_requirements = (
|
||||
authentication_requirements
|
||||
)
|
||||
|
||||
@@ -5779,7 +5775,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
# Ask what the pairing config should be for this connection
|
||||
pairing_config = self.pairing_config_factory(connection)
|
||||
io_capability = pairing_config.delegate.classic_io_capability
|
||||
peer_io_capability = connection.peer_pairing_io_capability
|
||||
peer_io_capability = connection.pairing_peer_io_capability
|
||||
|
||||
async def confirm() -> bool:
|
||||
# Ask the user to confirm the pairing, without display
|
||||
@@ -5840,8 +5836,8 @@ class Device(utils.CompositeEventEmitter):
|
||||
)
|
||||
)
|
||||
return
|
||||
except Exception as error:
|
||||
logger.warning(f'exception while confirming: {error}')
|
||||
except Exception:
|
||||
logger.exception('exception while confirming')
|
||||
|
||||
await self.host.send_command(
|
||||
hci.HCI_User_Confirmation_Request_Negative_Reply_Command(
|
||||
@@ -5854,7 +5850,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
# [Classic only]
|
||||
@host_event_handler
|
||||
@with_connection_from_address
|
||||
def on_authentication_user_passkey_request(self, connection) -> None:
|
||||
def on_authentication_user_passkey_request(self, connection: Connection) -> None:
|
||||
# Ask what the pairing config should be for this connection
|
||||
pairing_config = self.pairing_config_factory(connection)
|
||||
|
||||
@@ -5870,8 +5866,8 @@ class Device(utils.CompositeEventEmitter):
|
||||
)
|
||||
)
|
||||
return
|
||||
except Exception as error:
|
||||
logger.warning(f'exception while asking for pass-key: {error}')
|
||||
except Exception:
|
||||
logger.exception('exception while asking for pass-key')
|
||||
|
||||
await self.host.send_command(
|
||||
hci.HCI_User_Passkey_Request_Negative_Reply_Command(
|
||||
@@ -5897,7 +5893,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
# [Classic only]
|
||||
@host_event_handler
|
||||
@with_connection_from_address
|
||||
def on_pin_code_request(self, connection):
|
||||
def on_pin_code_request(self, connection: Connection):
|
||||
# Classic legacy pairing
|
||||
# Ask what the pairing config should be for this connection
|
||||
pairing_config = self.pairing_config_factory(connection)
|
||||
@@ -5941,7 +5937,9 @@ class Device(utils.CompositeEventEmitter):
|
||||
# [Classic only]
|
||||
@host_event_handler
|
||||
@with_connection_from_address
|
||||
def on_authentication_user_passkey_notification(self, connection, passkey):
|
||||
def on_authentication_user_passkey_notification(
|
||||
self, connection: Connection, passkey: int
|
||||
):
|
||||
# Ask what the pairing config should be for this connection
|
||||
pairing_config = self.pairing_config_factory(connection)
|
||||
|
||||
@@ -5953,14 +5951,15 @@ class Device(utils.CompositeEventEmitter):
|
||||
# [Classic only]
|
||||
@host_event_handler
|
||||
@try_with_connection_from_address
|
||||
def on_remote_name(self, connection: Connection, address, remote_name):
|
||||
def on_remote_name(
|
||||
self, connection: Connection, address: hci.Address, remote_name: bytes
|
||||
):
|
||||
# Try to decode the name
|
||||
try:
|
||||
remote_name = remote_name.decode('utf-8')
|
||||
if connection:
|
||||
connection.peer_name = remote_name
|
||||
connection.peer_name = remote_name.decode('utf-8')
|
||||
connection.emit(connection.EVENT_REMOTE_NAME)
|
||||
self.emit(self.EVENT_REMOTE_NAME, address, remote_name)
|
||||
self.emit(self.EVENT_REMOTE_NAME, address, remote_name.decode('utf-8'))
|
||||
except UnicodeDecodeError as error:
|
||||
logger.warning('peer name is not valid UTF-8')
|
||||
if connection:
|
||||
@@ -5971,7 +5970,9 @@ class Device(utils.CompositeEventEmitter):
|
||||
# [Classic only]
|
||||
@host_event_handler
|
||||
@try_with_connection_from_address
|
||||
def on_remote_name_failure(self, connection: Connection, address, error):
|
||||
def on_remote_name_failure(
|
||||
self, connection: Connection, address: hci.Address, error: int
|
||||
):
|
||||
if connection:
|
||||
connection.emit(connection.EVENT_REMOTE_NAME_FAILURE, error)
|
||||
self.emit(self.EVENT_REMOTE_NAME_FAILURE, address, error)
|
||||
@@ -6172,7 +6173,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
|
||||
@host_event_handler
|
||||
@with_connection_from_handle
|
||||
def on_connection_encryption_key_refresh(self, connection):
|
||||
def on_connection_encryption_key_refresh(self, connection: Connection):
|
||||
logger.debug(
|
||||
f'*** Connection Key Refresh: [0x{connection.handle:04X}] '
|
||||
f'{connection.peer_address} as {connection.role_name}'
|
||||
@@ -6189,16 +6190,30 @@ class Device(utils.CompositeEventEmitter):
|
||||
f'{connection.peer_address} as {connection.role_name}, '
|
||||
f'{connection_parameters}'
|
||||
)
|
||||
connection.parameters = Connection.Parameters(
|
||||
connection_parameters.connection_interval * 1.25,
|
||||
connection_parameters.peripheral_latency,
|
||||
connection_parameters.supervision_timeout * 10.0,
|
||||
)
|
||||
if (
|
||||
connection.parameters.connection_interval
|
||||
!= connection_parameters.connection_interval * 1.25
|
||||
):
|
||||
connection.parameters = Connection.Parameters(
|
||||
connection_parameters.connection_interval * 1.25,
|
||||
connection_parameters.peripheral_latency,
|
||||
connection_parameters.supervision_timeout * 10.0,
|
||||
)
|
||||
else:
|
||||
connection.parameters = Connection.Parameters(
|
||||
connection_parameters.connection_interval * 1.25,
|
||||
connection_parameters.peripheral_latency,
|
||||
connection_parameters.supervision_timeout * 10.0,
|
||||
connection.parameters.subrate_factor,
|
||||
connection.parameters.continuation_number,
|
||||
)
|
||||
connection.emit(connection.EVENT_CONNECTION_PARAMETERS_UPDATE)
|
||||
|
||||
@host_event_handler
|
||||
@with_connection_from_handle
|
||||
def on_connection_parameters_update_failure(self, connection, error):
|
||||
def on_connection_parameters_update_failure(
|
||||
self, connection: Connection, error: int
|
||||
):
|
||||
logger.debug(
|
||||
f'*** Connection Parameters Update Failed: [0x{connection.handle:04X}] '
|
||||
f'{connection.peer_address} as {connection.role_name}, '
|
||||
@@ -6208,7 +6223,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
|
||||
@host_event_handler
|
||||
@with_connection_from_handle
|
||||
def on_connection_phy_update(self, connection, phy):
|
||||
def on_connection_phy_update(self, connection: Connection, phy: core.ConnectionPHY):
|
||||
logger.debug(
|
||||
f'*** Connection PHY Update: [0x{connection.handle:04X}] '
|
||||
f'{connection.peer_address} as {connection.role_name}, '
|
||||
@@ -6218,7 +6233,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
|
||||
@host_event_handler
|
||||
@with_connection_from_handle
|
||||
def on_connection_phy_update_failure(self, connection, error):
|
||||
def on_connection_phy_update_failure(self, connection: Connection, error: int):
|
||||
logger.debug(
|
||||
f'*** Connection PHY Update Failed: [0x{connection.handle:04X}] '
|
||||
f'{connection.peer_address} as {connection.role_name}, '
|
||||
@@ -6228,7 +6243,26 @@ class Device(utils.CompositeEventEmitter):
|
||||
|
||||
@host_event_handler
|
||||
@with_connection_from_handle
|
||||
def on_connection_att_mtu_update(self, connection, att_mtu):
|
||||
def on_le_subrate_change(
|
||||
self,
|
||||
connection: Connection,
|
||||
subrate_factor: int,
|
||||
peripheral_latency: int,
|
||||
continuation_number: int,
|
||||
supervision_timeout: int,
|
||||
):
|
||||
connection.parameters = Connection.Parameters(
|
||||
connection.parameters.connection_interval,
|
||||
peripheral_latency,
|
||||
supervision_timeout * 10.0,
|
||||
subrate_factor,
|
||||
continuation_number,
|
||||
)
|
||||
connection.emit(connection.EVENT_LE_SUBRATE_CHANGE)
|
||||
|
||||
@host_event_handler
|
||||
@with_connection_from_handle
|
||||
def on_connection_att_mtu_update(self, connection: Connection, att_mtu: int):
|
||||
logger.debug(
|
||||
f'*** Connection ATT MTU Update: [0x{connection.handle:04X}] '
|
||||
f'{connection.peer_address} as {connection.role_name}, '
|
||||
@@ -6240,7 +6274,12 @@ class Device(utils.CompositeEventEmitter):
|
||||
@host_event_handler
|
||||
@with_connection_from_handle
|
||||
def on_connection_data_length_change(
|
||||
self, connection, max_tx_octets, max_tx_time, max_rx_octets, max_rx_time
|
||||
self,
|
||||
connection: Connection,
|
||||
max_tx_octets: int,
|
||||
max_tx_time: int,
|
||||
max_rx_octets: int,
|
||||
max_rx_time: int,
|
||||
):
|
||||
logger.debug(
|
||||
f'*** Connection Data Length Change: [0x{connection.handle:04X}] '
|
||||
@@ -6365,14 +6404,16 @@ class Device(utils.CompositeEventEmitter):
|
||||
# [Classic only]
|
||||
@host_event_handler
|
||||
@with_connection_from_address
|
||||
def on_role_change(self, connection, new_role):
|
||||
def on_role_change(self, connection: Connection, new_role: hci.Role):
|
||||
connection.role = new_role
|
||||
connection.emit(connection.EVENT_ROLE_CHANGE, new_role)
|
||||
|
||||
# [Classic only]
|
||||
@host_event_handler
|
||||
@try_with_connection_from_address
|
||||
def on_role_change_failure(self, connection, address, error):
|
||||
def on_role_change_failure(
|
||||
self, connection: Connection, address: hci.Address, error: int
|
||||
):
|
||||
if connection:
|
||||
connection.emit(connection.EVENT_ROLE_CHANGE_FAILURE, error)
|
||||
self.emit(self.EVENT_ROLE_CHANGE_FAILURE, address, error)
|
||||
@@ -6386,7 +6427,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
# [Classic only]
|
||||
@host_event_handler
|
||||
@with_connection_from_address
|
||||
def on_classic_pairing_failure(self, connection: Connection, status) -> None:
|
||||
def on_classic_pairing_failure(self, connection: Connection, status: int) -> None:
|
||||
connection.emit(connection.EVENT_CLASSIC_PAIRING_FAILURE, status)
|
||||
|
||||
def on_pairing_start(self, connection: Connection) -> None:
|
||||
@@ -6410,7 +6451,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
connection.emit(connection.EVENT_PAIRING_FAILURE, reason)
|
||||
|
||||
@with_connection_from_handle
|
||||
def on_gatt_pdu(self, connection, pdu):
|
||||
def on_gatt_pdu(self, connection: Connection, pdu: bytes):
|
||||
# Parse the L2CAP payload into an ATT PDU object
|
||||
att_pdu = ATT_PDU.from_bytes(pdu)
|
||||
|
||||
@@ -6432,7 +6473,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
connection.gatt_server.on_gatt_pdu(connection, att_pdu)
|
||||
|
||||
@with_connection_from_handle
|
||||
def on_smp_pdu(self, connection, pdu):
|
||||
def on_smp_pdu(self, connection: Connection, pdu: bytes):
|
||||
self.smp_manager.on_smp_pdu(connection, pdu)
|
||||
|
||||
@host_event_handler
|
||||
|
||||
@@ -20,12 +20,13 @@ like loading firmware after a cold start.
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import pathlib
|
||||
import platform
|
||||
from typing import Iterable, Optional, TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Iterable, Optional
|
||||
|
||||
from bumble.drivers import rtk, intel
|
||||
from bumble.drivers import intel, rtk
|
||||
from bumble.drivers.common import Driver
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@@ -20,6 +20,7 @@ Loosely based on the Fuchsia OS implementation.
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import collections
|
||||
import dataclasses
|
||||
@@ -28,12 +29,10 @@ import os
|
||||
import pathlib
|
||||
import platform
|
||||
import struct
|
||||
from typing import Any, Optional, TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
|
||||
from bumble import core
|
||||
from bumble import core, hci, utils
|
||||
from bumble.drivers import common
|
||||
from bumble import hci
|
||||
from bumble import utils
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bumble.host import Host
|
||||
|
||||
@@ -17,10 +17,6 @@ Based on various online bits of information, including the Linux kernel.
|
||||
(see `drivers/bluetooth/btrtl.c`)
|
||||
"""
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from dataclasses import dataclass, field
|
||||
import asyncio
|
||||
import enum
|
||||
import logging
|
||||
@@ -31,9 +27,12 @@ import platform
|
||||
import struct
|
||||
import weakref
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from bumble import core
|
||||
from bumble import hci
|
||||
from bumble import core, hci
|
||||
from bumble.drivers import common
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -489,6 +488,21 @@ class Driver(common.Driver):
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
async def get_loaded_firmware_version(host):
|
||||
response = await host.send_command(HCI_RTK_Read_ROM_Version_Command())
|
||||
|
||||
if response.return_parameters.status != hci.HCI_SUCCESS:
|
||||
return None
|
||||
|
||||
response = await host.send_command(
|
||||
hci.HCI_Read_Local_Version_Information_Command(), check_result=True
|
||||
)
|
||||
return (
|
||||
response.return_parameters.hci_subversion << 16
|
||||
| response.return_parameters.lmp_subversion
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def driver_info_for_host(cls, host):
|
||||
try:
|
||||
@@ -592,7 +606,7 @@ class Driver(common.Driver):
|
||||
)
|
||||
if response.return_parameters.status != hci.HCI_SUCCESS:
|
||||
logger.warning("can't get ROM version")
|
||||
return
|
||||
return None
|
||||
rom_version = response.return_parameters.version
|
||||
logger.debug(f"ROM version before download: {rom_version:04X}")
|
||||
else:
|
||||
@@ -600,13 +614,14 @@ class Driver(common.Driver):
|
||||
|
||||
firmware = Firmware(self.firmware)
|
||||
logger.debug(f"firmware: project_id=0x{firmware.project_id:04X}")
|
||||
logger.debug(f"firmware: version=0x{firmware.version:04X}")
|
||||
for patch in firmware.patches:
|
||||
if patch[0] == rom_version + 1:
|
||||
logger.debug(f"using patch {patch[0]}")
|
||||
break
|
||||
else:
|
||||
logger.warning("no valid patch found for rom version {rom_version}")
|
||||
return
|
||||
return None
|
||||
|
||||
# Append the config if there is one.
|
||||
if self.config:
|
||||
@@ -642,7 +657,9 @@ class Driver(common.Driver):
|
||||
logger.warning("can't get ROM version")
|
||||
else:
|
||||
rom_version = response.return_parameters.version
|
||||
logger.debug(f"ROM version after download: {rom_version:04X}")
|
||||
logger.debug(f"ROM version after download: {rom_version:02X}")
|
||||
|
||||
return firmware.version
|
||||
|
||||
async def download_firmware(self):
|
||||
if self.driver_info.rom == RTK_ROM_LMP_8723A:
|
||||
|
||||
@@ -19,11 +19,11 @@ import logging
|
||||
import struct
|
||||
|
||||
from bumble.gatt import (
|
||||
Service,
|
||||
Characteristic,
|
||||
GATT_GENERIC_ACCESS_SERVICE,
|
||||
GATT_DEVICE_NAME_CHARACTERISTIC,
|
||||
GATT_APPEARANCE_CHARACTERISTIC,
|
||||
GATT_DEVICE_NAME_CHARACTERISTIC,
|
||||
GATT_GENERIC_ACCESS_SERVICE,
|
||||
Characteristic,
|
||||
Service,
|
||||
)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -23,15 +23,16 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
import functools
|
||||
import logging
|
||||
import struct
|
||||
from typing import Iterable, Optional, Sequence, TypeVar, Union
|
||||
|
||||
from bumble.colors import color
|
||||
from bumble.core import BaseBumbleError, UUID
|
||||
from bumble.att import Attribute, AttributeValue
|
||||
from bumble.colors import color
|
||||
from bumble.core import UUID, BaseBumbleError
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Typing
|
||||
|
||||
@@ -20,22 +20,14 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
import struct
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Generic,
|
||||
Iterable,
|
||||
Literal,
|
||||
Optional,
|
||||
TypeVar,
|
||||
)
|
||||
|
||||
import struct
|
||||
from typing import Any, Callable, Generic, Iterable, Literal, Optional, TypeVar
|
||||
|
||||
from bumble import utils
|
||||
from bumble.core import InvalidOperationError
|
||||
from bumble.gatt import Characteristic
|
||||
from bumble.gatt_client import CharacteristicProxy
|
||||
from bumble import utils
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Typing
|
||||
|
||||
@@ -24,60 +24,38 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import struct
|
||||
from datetime import datetime
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
Generic,
|
||||
Iterable,
|
||||
Optional,
|
||||
Union,
|
||||
TypeVar,
|
||||
TYPE_CHECKING,
|
||||
Union,
|
||||
)
|
||||
|
||||
|
||||
from bumble import att, core, utils
|
||||
from bumble.colors import color
|
||||
from bumble.hci import HCI_Constant
|
||||
from bumble.att import (
|
||||
ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
||||
ATT_ATTRIBUTE_NOT_LONG_ERROR,
|
||||
ATT_CID,
|
||||
ATT_DEFAULT_MTU,
|
||||
ATT_ERROR_RESPONSE,
|
||||
ATT_INVALID_OFFSET_ERROR,
|
||||
ATT_PDU,
|
||||
ATT_RESPONSES,
|
||||
ATT_Exchange_MTU_Request,
|
||||
ATT_Find_By_Type_Value_Request,
|
||||
ATT_Find_Information_Request,
|
||||
ATT_Handle_Value_Confirmation,
|
||||
ATT_Read_Blob_Request,
|
||||
ATT_Read_By_Group_Type_Request,
|
||||
ATT_Read_By_Type_Request,
|
||||
ATT_Read_Request,
|
||||
ATT_Write_Command,
|
||||
ATT_Write_Request,
|
||||
ATT_Error,
|
||||
)
|
||||
from bumble import utils
|
||||
from bumble import core
|
||||
from bumble.core import UUID, InvalidStateError
|
||||
from bumble.gatt import (
|
||||
GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
|
||||
GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
|
||||
GATT_INCLUDE_ATTRIBUTE_TYPE,
|
||||
GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
|
||||
GATT_REQUEST_TIMEOUT,
|
||||
GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE,
|
||||
GATT_INCLUDE_ATTRIBUTE_TYPE,
|
||||
Characteristic,
|
||||
ClientCharacteristicConfigurationBits,
|
||||
InvalidServiceError,
|
||||
TemplateService,
|
||||
)
|
||||
from bumble.hci import HCI_Constant
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Typing
|
||||
@@ -291,8 +269,8 @@ class Client:
|
||||
indication_subscribers: dict[
|
||||
int, set[Union[CharacteristicProxy, Callable[[bytes], Any]]]
|
||||
]
|
||||
pending_response: Optional[asyncio.futures.Future[ATT_PDU]]
|
||||
pending_request: Optional[ATT_PDU]
|
||||
pending_response: Optional[asyncio.futures.Future[att.ATT_PDU]]
|
||||
pending_request: Optional[att.ATT_PDU]
|
||||
|
||||
def __init__(self, connection: Connection) -> None:
|
||||
self.connection = connection
|
||||
@@ -308,15 +286,15 @@ class Client:
|
||||
connection.on(connection.EVENT_DISCONNECTION, self.on_disconnection)
|
||||
|
||||
def send_gatt_pdu(self, pdu: bytes) -> None:
|
||||
self.connection.send_l2cap_pdu(ATT_CID, pdu)
|
||||
self.connection.send_l2cap_pdu(att.ATT_CID, pdu)
|
||||
|
||||
async def send_command(self, command: ATT_PDU) -> None:
|
||||
async def send_command(self, command: att.ATT_PDU) -> None:
|
||||
logger.debug(
|
||||
f'GATT Command from client: [0x{self.connection.handle:04X}] {command}'
|
||||
)
|
||||
self.send_gatt_pdu(bytes(command))
|
||||
|
||||
async def send_request(self, request: ATT_PDU):
|
||||
async def send_request(self, request: att.ATT_PDU):
|
||||
logger.debug(
|
||||
f'GATT Request from client: [0x{self.connection.handle:04X}] {request}'
|
||||
)
|
||||
@@ -345,7 +323,9 @@ class Client:
|
||||
|
||||
return response
|
||||
|
||||
def send_confirmation(self, confirmation: ATT_Handle_Value_Confirmation) -> None:
|
||||
def send_confirmation(
|
||||
self, confirmation: att.ATT_Handle_Value_Confirmation
|
||||
) -> None:
|
||||
logger.debug(
|
||||
f'GATT Confirmation from client: [0x{self.connection.handle:04X}] '
|
||||
f'{confirmation}'
|
||||
@@ -354,8 +334,8 @@ class Client:
|
||||
|
||||
async def request_mtu(self, mtu: int) -> int:
|
||||
# Check the range
|
||||
if mtu < ATT_DEFAULT_MTU:
|
||||
raise core.InvalidArgumentError(f'MTU must be >= {ATT_DEFAULT_MTU}')
|
||||
if mtu < att.ATT_DEFAULT_MTU:
|
||||
raise core.InvalidArgumentError(f'MTU must be >= {att.ATT_DEFAULT_MTU}')
|
||||
if mtu > 0xFFFF:
|
||||
raise core.InvalidArgumentError('MTU must be <= 0xFFFF')
|
||||
|
||||
@@ -365,9 +345,11 @@ class Client:
|
||||
|
||||
# Send the request
|
||||
self.mtu_exchange_done = True
|
||||
response = await self.send_request(ATT_Exchange_MTU_Request(client_rx_mtu=mtu))
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
raise ATT_Error(error_code=response.error_code, message=response)
|
||||
response = await self.send_request(
|
||||
att.ATT_Exchange_MTU_Request(client_rx_mtu=mtu)
|
||||
)
|
||||
if response.op_code == att.Opcode.ATT_ERROR_RESPONSE:
|
||||
raise att.ATT_Error(error_code=response.error_code, message=response)
|
||||
|
||||
# Compute the final MTU
|
||||
self.connection.att_mtu = min(mtu, response.server_rx_mtu)
|
||||
@@ -432,7 +414,7 @@ class Client:
|
||||
services = []
|
||||
while starting_handle < 0xFFFF:
|
||||
response = await self.send_request(
|
||||
ATT_Read_By_Group_Type_Request(
|
||||
att.ATT_Read_By_Group_Type_Request(
|
||||
starting_handle=starting_handle,
|
||||
ending_handle=0xFFFF,
|
||||
attribute_group_type=GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
|
||||
@@ -443,14 +425,14 @@ class Client:
|
||||
return []
|
||||
|
||||
# Check if we reached the end of the iteration
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
if response.op_code == att.Opcode.ATT_ERROR_RESPONSE:
|
||||
if response.error_code != att.ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
# Unexpected end
|
||||
logger.warning(
|
||||
'!!! unexpected error while discovering services: '
|
||||
f'{HCI_Constant.error_name(response.error_code)}'
|
||||
)
|
||||
raise ATT_Error(
|
||||
raise att.ATT_Error(
|
||||
error_code=response.error_code,
|
||||
message='Unexpected error while discovering services',
|
||||
)
|
||||
@@ -509,7 +491,7 @@ class Client:
|
||||
services = []
|
||||
while starting_handle < 0xFFFF:
|
||||
response = await self.send_request(
|
||||
ATT_Find_By_Type_Value_Request(
|
||||
att.ATT_Find_By_Type_Value_Request(
|
||||
starting_handle=starting_handle,
|
||||
ending_handle=0xFFFF,
|
||||
attribute_type=GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
|
||||
@@ -521,8 +503,8 @@ class Client:
|
||||
return []
|
||||
|
||||
# Check if we reached the end of the iteration
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
if response.op_code == att.Opcode.ATT_ERROR_RESPONSE:
|
||||
if response.error_code != att.ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
# Unexpected end
|
||||
logger.warning(
|
||||
'!!! unexpected error while discovering services: '
|
||||
@@ -578,7 +560,7 @@ class Client:
|
||||
included_services: list[ServiceProxy] = []
|
||||
while starting_handle <= ending_handle:
|
||||
response = await self.send_request(
|
||||
ATT_Read_By_Type_Request(
|
||||
att.ATT_Read_By_Type_Request(
|
||||
starting_handle=starting_handle,
|
||||
ending_handle=ending_handle,
|
||||
attribute_type=GATT_INCLUDE_ATTRIBUTE_TYPE,
|
||||
@@ -589,14 +571,14 @@ class Client:
|
||||
return []
|
||||
|
||||
# Check if we reached the end of the iteration
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
if response.op_code == att.Opcode.ATT_ERROR_RESPONSE:
|
||||
if response.error_code != att.ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
# Unexpected end
|
||||
logger.warning(
|
||||
'!!! unexpected error while discovering included services: '
|
||||
f'{HCI_Constant.error_name(response.error_code)}'
|
||||
)
|
||||
raise ATT_Error(
|
||||
raise att.ATT_Error(
|
||||
error_code=response.error_code,
|
||||
message='Unexpected error while discovering included services',
|
||||
)
|
||||
@@ -652,7 +634,7 @@ class Client:
|
||||
characteristics: list[CharacteristicProxy[bytes]] = []
|
||||
while starting_handle <= ending_handle:
|
||||
response = await self.send_request(
|
||||
ATT_Read_By_Type_Request(
|
||||
att.ATT_Read_By_Type_Request(
|
||||
starting_handle=starting_handle,
|
||||
ending_handle=ending_handle,
|
||||
attribute_type=GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
|
||||
@@ -663,14 +645,14 @@ class Client:
|
||||
return []
|
||||
|
||||
# Check if we reached the end of the iteration
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
if response.op_code == att.Opcode.ATT_ERROR_RESPONSE:
|
||||
if response.error_code != att.ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
# Unexpected end
|
||||
logger.warning(
|
||||
'!!! unexpected error while discovering characteristics: '
|
||||
f'{HCI_Constant.error_name(response.error_code)}'
|
||||
)
|
||||
raise ATT_Error(
|
||||
raise att.ATT_Error(
|
||||
error_code=response.error_code,
|
||||
message='Unexpected error while discovering characteristics',
|
||||
)
|
||||
@@ -736,7 +718,7 @@ class Client:
|
||||
descriptors: list[DescriptorProxy] = []
|
||||
while starting_handle <= ending_handle:
|
||||
response = await self.send_request(
|
||||
ATT_Find_Information_Request(
|
||||
att.ATT_Find_Information_Request(
|
||||
starting_handle=starting_handle, ending_handle=ending_handle
|
||||
)
|
||||
)
|
||||
@@ -745,8 +727,8 @@ class Client:
|
||||
return []
|
||||
|
||||
# Check if we reached the end of the iteration
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
if response.op_code == att.Opcode.ATT_ERROR_RESPONSE:
|
||||
if response.error_code != att.ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
# Unexpected end
|
||||
logger.warning(
|
||||
'!!! unexpected error while discovering descriptors: '
|
||||
@@ -791,7 +773,7 @@ class Client:
|
||||
attributes = []
|
||||
while True:
|
||||
response = await self.send_request(
|
||||
ATT_Find_Information_Request(
|
||||
att.ATT_Find_Information_Request(
|
||||
starting_handle=starting_handle, ending_handle=ending_handle
|
||||
)
|
||||
)
|
||||
@@ -799,8 +781,8 @@ class Client:
|
||||
return []
|
||||
|
||||
# Check if we reached the end of the iteration
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
if response.op_code == att.Opcode.ATT_ERROR_RESPONSE:
|
||||
if response.error_code != att.ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
# Unexpected end
|
||||
logger.warning(
|
||||
'!!! unexpected error while discovering attributes: '
|
||||
@@ -954,12 +936,12 @@ class Client:
|
||||
# Send a request to read
|
||||
attribute_handle = attribute if isinstance(attribute, int) else attribute.handle
|
||||
response = await self.send_request(
|
||||
ATT_Read_Request(attribute_handle=attribute_handle)
|
||||
att.ATT_Read_Request(attribute_handle=attribute_handle)
|
||||
)
|
||||
if response is None:
|
||||
raise TimeoutError('read timeout')
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
raise ATT_Error(error_code=response.error_code, message=response)
|
||||
if response.op_code == att.Opcode.ATT_ERROR_RESPONSE:
|
||||
raise att.ATT_Error(error_code=response.error_code, message=response)
|
||||
|
||||
# If the value is the max size for the MTU, try to read more unless the caller
|
||||
# specifically asked not to do that
|
||||
@@ -969,19 +951,21 @@ class Client:
|
||||
offset = len(attribute_value)
|
||||
while True:
|
||||
response = await self.send_request(
|
||||
ATT_Read_Blob_Request(
|
||||
att.ATT_Read_Blob_Request(
|
||||
attribute_handle=attribute_handle, value_offset=offset
|
||||
)
|
||||
)
|
||||
if response is None:
|
||||
raise TimeoutError('read timeout')
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
if response.op_code == att.Opcode.ATT_ERROR_RESPONSE:
|
||||
if response.error_code in (
|
||||
ATT_ATTRIBUTE_NOT_LONG_ERROR,
|
||||
ATT_INVALID_OFFSET_ERROR,
|
||||
att.ATT_ATTRIBUTE_NOT_LONG_ERROR,
|
||||
att.ATT_INVALID_OFFSET_ERROR,
|
||||
):
|
||||
break
|
||||
raise ATT_Error(error_code=response.error_code, message=response)
|
||||
raise att.ATT_Error(
|
||||
error_code=response.error_code, message=response
|
||||
)
|
||||
|
||||
part = response.part_attribute_value
|
||||
attribute_value += part
|
||||
@@ -1012,7 +996,7 @@ class Client:
|
||||
characteristics_values = []
|
||||
while starting_handle <= ending_handle:
|
||||
response = await self.send_request(
|
||||
ATT_Read_By_Type_Request(
|
||||
att.ATT_Read_By_Type_Request(
|
||||
starting_handle=starting_handle,
|
||||
ending_handle=ending_handle,
|
||||
attribute_type=uuid,
|
||||
@@ -1023,8 +1007,8 @@ class Client:
|
||||
return []
|
||||
|
||||
# Check if we reached the end of the iteration
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
if response.op_code == att.Opcode.ATT_ERROR_RESPONSE:
|
||||
if response.error_code != att.ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
# Unexpected end
|
||||
logger.warning(
|
||||
'!!! unexpected error while reading characteristics: '
|
||||
@@ -1069,15 +1053,15 @@ class Client:
|
||||
attribute_handle = attribute if isinstance(attribute, int) else attribute.handle
|
||||
if with_response:
|
||||
response = await self.send_request(
|
||||
ATT_Write_Request(
|
||||
att.ATT_Write_Request(
|
||||
attribute_handle=attribute_handle, attribute_value=value
|
||||
)
|
||||
)
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
raise ATT_Error(error_code=response.error_code, message=response)
|
||||
if response.op_code == att.Opcode.ATT_ERROR_RESPONSE:
|
||||
raise att.ATT_Error(error_code=response.error_code, message=response)
|
||||
else:
|
||||
await self.send_command(
|
||||
ATT_Write_Command(
|
||||
att.ATT_Write_Command(
|
||||
attribute_handle=attribute_handle, attribute_value=value
|
||||
)
|
||||
)
|
||||
@@ -1086,11 +1070,11 @@ class Client:
|
||||
if self.pending_response and not self.pending_response.done():
|
||||
self.pending_response.cancel()
|
||||
|
||||
def on_gatt_pdu(self, att_pdu: ATT_PDU) -> None:
|
||||
def on_gatt_pdu(self, att_pdu: att.ATT_PDU) -> None:
|
||||
logger.debug(
|
||||
f'GATT Response to client: [0x{self.connection.handle:04X}] {att_pdu}'
|
||||
)
|
||||
if att_pdu.op_code in ATT_RESPONSES:
|
||||
if att_pdu.op_code in att.ATT_RESPONSES:
|
||||
if self.pending_request is None:
|
||||
# Not expected!
|
||||
logger.warning('!!! unexpected response, there is no pending request')
|
||||
@@ -1098,7 +1082,7 @@ class Client:
|
||||
|
||||
# The response should match the pending request unless it is
|
||||
# an error response
|
||||
if att_pdu.op_code != ATT_ERROR_RESPONSE:
|
||||
if att_pdu.op_code != att.Opcode.ATT_ERROR_RESPONSE:
|
||||
expected_response_name = self.pending_request.name.replace(
|
||||
'_REQUEST', '_RESPONSE'
|
||||
)
|
||||
@@ -1126,7 +1110,9 @@ class Client:
|
||||
+ str(att_pdu)
|
||||
)
|
||||
|
||||
def on_att_handle_value_notification(self, notification):
|
||||
def on_att_handle_value_notification(
|
||||
self, notification: att.ATT_Handle_Value_Notification
|
||||
):
|
||||
# Call all subscribers
|
||||
subscribers = self.notification_subscribers.get(
|
||||
notification.attribute_handle, set()
|
||||
@@ -1141,7 +1127,9 @@ class Client:
|
||||
else:
|
||||
subscriber.emit(subscriber.EVENT_UPDATE, notification.attribute_value)
|
||||
|
||||
def on_att_handle_value_indication(self, indication):
|
||||
def on_att_handle_value_indication(
|
||||
self, indication: att.ATT_Handle_Value_Indication
|
||||
):
|
||||
# Call all subscribers
|
||||
subscribers = self.indication_subscribers.get(
|
||||
indication.attribute_handle, set()
|
||||
@@ -1157,7 +1145,7 @@ class Client:
|
||||
subscriber.emit(subscriber.EVENT_UPDATE, indication.attribute_value)
|
||||
|
||||
# Confirm that we received the indication
|
||||
self.send_confirmation(ATT_Handle_Value_Confirmation())
|
||||
self.send_confirmation(att.ATT_Handle_Value_Confirmation())
|
||||
|
||||
def cache_value(self, attribute_handle: int, value: bytes) -> None:
|
||||
self.cached_values[attribute_handle] = (
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# GATT - Generic Attribute Profile
|
||||
# GATT - Generic att.Attribute Profile
|
||||
# Server
|
||||
#
|
||||
# See Bluetooth spec @ Vol 3, Part G
|
||||
@@ -24,46 +24,16 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
import struct
|
||||
from typing import (
|
||||
Iterable,
|
||||
Optional,
|
||||
TypeVar,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, Iterable, Optional, TypeVar
|
||||
|
||||
from bumble import att, utils
|
||||
from bumble.colors import color
|
||||
from bumble.core import UUID
|
||||
from bumble.att import (
|
||||
ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
||||
ATT_ATTRIBUTE_NOT_LONG_ERROR,
|
||||
ATT_CID,
|
||||
ATT_DEFAULT_MTU,
|
||||
ATT_INVALID_ATTRIBUTE_LENGTH_ERROR,
|
||||
ATT_INVALID_HANDLE_ERROR,
|
||||
ATT_INVALID_OFFSET_ERROR,
|
||||
ATT_REQUEST_NOT_SUPPORTED_ERROR,
|
||||
ATT_REQUESTS,
|
||||
ATT_PDU,
|
||||
ATT_UNLIKELY_ERROR_ERROR,
|
||||
ATT_UNSUPPORTED_GROUP_TYPE_ERROR,
|
||||
ATT_Error,
|
||||
ATT_Error_Response,
|
||||
ATT_Exchange_MTU_Response,
|
||||
ATT_Find_By_Type_Value_Response,
|
||||
ATT_Find_Information_Response,
|
||||
ATT_Handle_Value_Indication,
|
||||
ATT_Handle_Value_Notification,
|
||||
ATT_Read_Blob_Response,
|
||||
ATT_Read_By_Group_Type_Response,
|
||||
ATT_Read_By_Type_Response,
|
||||
ATT_Read_Response,
|
||||
ATT_Write_Response,
|
||||
Attribute,
|
||||
)
|
||||
from bumble.gatt import (
|
||||
GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
|
||||
GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
|
||||
@@ -74,14 +44,13 @@ from bumble.gatt import (
|
||||
Characteristic,
|
||||
CharacteristicDeclaration,
|
||||
CharacteristicValue,
|
||||
IncludedServiceDeclaration,
|
||||
Descriptor,
|
||||
IncludedServiceDeclaration,
|
||||
Service,
|
||||
)
|
||||
from bumble import utils
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bumble.device import Device, Connection
|
||||
from bumble.device import Connection, Device
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -99,9 +68,9 @@ GATT_SERVER_DEFAULT_MAX_MTU = 517
|
||||
# GATT Server
|
||||
# -----------------------------------------------------------------------------
|
||||
class Server(utils.EventEmitter):
|
||||
attributes: list[Attribute]
|
||||
attributes: list[att.Attribute]
|
||||
services: list[Service]
|
||||
attributes_by_handle: dict[int, Attribute]
|
||||
attributes_by_handle: dict[int, att.Attribute]
|
||||
subscribers: dict[int, dict[int, bytes]]
|
||||
indication_semaphores: defaultdict[int, asyncio.Semaphore]
|
||||
pending_confirmations: defaultdict[int, Optional[asyncio.futures.Future]]
|
||||
@@ -112,7 +81,7 @@ class Server(utils.EventEmitter):
|
||||
super().__init__()
|
||||
self.device = device
|
||||
self.services = []
|
||||
self.attributes = [] # Attributes, ordered by increasing handle values
|
||||
self.attributes = [] # att.Attributes, ordered by increasing handle values
|
||||
self.attributes_by_handle = {} # Map for fast attribute access by handle
|
||||
self.max_mtu = (
|
||||
GATT_SERVER_DEFAULT_MAX_MTU # The max MTU we're willing to negotiate
|
||||
@@ -127,12 +96,12 @@ class Server(utils.EventEmitter):
|
||||
return "\n".join(map(str, self.attributes))
|
||||
|
||||
def send_gatt_pdu(self, connection_handle: int, pdu: bytes) -> None:
|
||||
self.device.send_l2cap_pdu(connection_handle, ATT_CID, pdu)
|
||||
self.device.send_l2cap_pdu(connection_handle, att.ATT_CID, pdu)
|
||||
|
||||
def next_handle(self) -> int:
|
||||
return 1 + len(self.attributes)
|
||||
|
||||
def get_advertising_service_data(self) -> dict[Attribute, bytes]:
|
||||
def get_advertising_service_data(self) -> dict[att.Attribute, bytes]:
|
||||
return {
|
||||
attribute: data
|
||||
for attribute in self.attributes
|
||||
@@ -140,7 +109,7 @@ class Server(utils.EventEmitter):
|
||||
and (data := attribute.get_advertising_data())
|
||||
}
|
||||
|
||||
def get_attribute(self, handle: int) -> Optional[Attribute]:
|
||||
def get_attribute(self, handle: int) -> Optional[att.Attribute]:
|
||||
attribute = self.attributes_by_handle.get(handle)
|
||||
if attribute:
|
||||
return attribute
|
||||
@@ -231,7 +200,7 @@ class Server(utils.EventEmitter):
|
||||
None,
|
||||
)
|
||||
|
||||
def add_attribute(self, attribute: Attribute) -> None:
|
||||
def add_attribute(self, attribute: att.Attribute) -> None:
|
||||
# Assign a handle to this attribute
|
||||
attribute.handle = self.next_handle()
|
||||
attribute.end_group_handle = (
|
||||
@@ -286,7 +255,7 @@ class Server(utils.EventEmitter):
|
||||
# pylint: disable=line-too-long
|
||||
Descriptor(
|
||||
GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
|
||||
Attribute.READABLE | Attribute.WRITEABLE,
|
||||
att.Attribute.READABLE | att.Attribute.WRITEABLE,
|
||||
CharacteristicValue(
|
||||
read=lambda connection, characteristic=characteristic: self.read_cccd(
|
||||
connection, characteristic
|
||||
@@ -355,7 +324,7 @@ class Server(utils.EventEmitter):
|
||||
indicate_enabled,
|
||||
)
|
||||
|
||||
def send_response(self, connection: Connection, response: ATT_PDU) -> None:
|
||||
def send_response(self, connection: Connection, response: att.ATT_PDU) -> None:
|
||||
logger.debug(
|
||||
f'GATT Response from server: [0x{connection.handle:04X}] {response}'
|
||||
)
|
||||
@@ -364,7 +333,7 @@ class Server(utils.EventEmitter):
|
||||
async def notify_subscriber(
|
||||
self,
|
||||
connection: Connection,
|
||||
attribute: Attribute,
|
||||
attribute: att.Attribute,
|
||||
value: Optional[bytes] = None,
|
||||
force: bool = False,
|
||||
) -> None:
|
||||
@@ -396,7 +365,7 @@ class Server(utils.EventEmitter):
|
||||
value = value[: connection.att_mtu - 3]
|
||||
|
||||
# Notify
|
||||
notification = ATT_Handle_Value_Notification(
|
||||
notification = att.ATT_Handle_Value_Notification(
|
||||
attribute_handle=attribute.handle, attribute_value=value
|
||||
)
|
||||
logger.debug(
|
||||
@@ -407,7 +376,7 @@ class Server(utils.EventEmitter):
|
||||
async def indicate_subscriber(
|
||||
self,
|
||||
connection: Connection,
|
||||
attribute: Attribute,
|
||||
attribute: att.Attribute,
|
||||
value: Optional[bytes] = None,
|
||||
force: bool = False,
|
||||
) -> None:
|
||||
@@ -439,7 +408,7 @@ class Server(utils.EventEmitter):
|
||||
value = value[: connection.att_mtu - 3]
|
||||
|
||||
# Indicate
|
||||
indication = ATT_Handle_Value_Indication(
|
||||
indication = att.ATT_Handle_Value_Indication(
|
||||
attribute_handle=attribute.handle, attribute_value=value
|
||||
)
|
||||
logger.debug(
|
||||
@@ -467,7 +436,7 @@ class Server(utils.EventEmitter):
|
||||
async def _notify_or_indicate_subscribers(
|
||||
self,
|
||||
indicate: bool,
|
||||
attribute: Attribute,
|
||||
attribute: att.Attribute,
|
||||
value: Optional[bytes] = None,
|
||||
force: bool = False,
|
||||
) -> None:
|
||||
@@ -494,7 +463,7 @@ class Server(utils.EventEmitter):
|
||||
|
||||
async def notify_subscribers(
|
||||
self,
|
||||
attribute: Attribute,
|
||||
attribute: att.Attribute,
|
||||
value: Optional[bytes] = None,
|
||||
force: bool = False,
|
||||
):
|
||||
@@ -504,7 +473,7 @@ class Server(utils.EventEmitter):
|
||||
|
||||
async def indicate_subscribers(
|
||||
self,
|
||||
attribute: Attribute,
|
||||
attribute: att.Attribute,
|
||||
value: Optional[bytes] = None,
|
||||
force: bool = False,
|
||||
):
|
||||
@@ -518,33 +487,33 @@ class Server(utils.EventEmitter):
|
||||
if connection.handle in self.pending_confirmations:
|
||||
del self.pending_confirmations[connection.handle]
|
||||
|
||||
def on_gatt_pdu(self, connection: Connection, att_pdu: ATT_PDU) -> None:
|
||||
def on_gatt_pdu(self, connection: Connection, att_pdu: att.ATT_PDU) -> None:
|
||||
logger.debug(f'GATT Request to server: [0x{connection.handle:04X}] {att_pdu}')
|
||||
handler_name = f'on_{att_pdu.name.lower()}'
|
||||
handler = getattr(self, handler_name, None)
|
||||
if handler is not None:
|
||||
try:
|
||||
handler(connection, att_pdu)
|
||||
except ATT_Error as error:
|
||||
except att.ATT_Error as error:
|
||||
logger.debug(f'normal exception returned by handler: {error}')
|
||||
response = ATT_Error_Response(
|
||||
response = att.ATT_Error_Response(
|
||||
request_opcode_in_error=att_pdu.op_code,
|
||||
attribute_handle_in_error=error.att_handle,
|
||||
error_code=error.error_code,
|
||||
)
|
||||
self.send_response(connection, response)
|
||||
except Exception as error:
|
||||
logger.warning(f'{color("!!! Exception in handler:", "red")} {error}')
|
||||
response = ATT_Error_Response(
|
||||
except Exception:
|
||||
logger.exception(color("!!! Exception in handler:", "red"))
|
||||
response = att.ATT_Error_Response(
|
||||
request_opcode_in_error=att_pdu.op_code,
|
||||
attribute_handle_in_error=0x0000,
|
||||
error_code=ATT_UNLIKELY_ERROR_ERROR,
|
||||
error_code=att.ATT_UNLIKELY_ERROR_ERROR,
|
||||
)
|
||||
self.send_response(connection, response)
|
||||
raise error
|
||||
raise
|
||||
else:
|
||||
# No specific handler registered
|
||||
if att_pdu.op_code in ATT_REQUESTS:
|
||||
if att_pdu.op_code in att.ATT_REQUESTS:
|
||||
# Invoke the generic handler
|
||||
self.on_att_request(connection, att_pdu)
|
||||
else:
|
||||
@@ -560,7 +529,7 @@ class Server(utils.EventEmitter):
|
||||
#######################################################
|
||||
# ATT handlers
|
||||
#######################################################
|
||||
def on_att_request(self, connection: Connection, pdu: ATT_PDU) -> None:
|
||||
def on_att_request(self, connection: Connection, pdu: att.ATT_PDU) -> None:
|
||||
'''
|
||||
Handler for requests without a more specific handler
|
||||
'''
|
||||
@@ -570,23 +539,25 @@ class Server(utils.EventEmitter):
|
||||
)
|
||||
+ str(pdu)
|
||||
)
|
||||
response = ATT_Error_Response(
|
||||
response = att.ATT_Error_Response(
|
||||
request_opcode_in_error=pdu.op_code,
|
||||
attribute_handle_in_error=0x0000,
|
||||
error_code=ATT_REQUEST_NOT_SUPPORTED_ERROR,
|
||||
error_code=att.ATT_REQUEST_NOT_SUPPORTED_ERROR,
|
||||
)
|
||||
self.send_response(connection, response)
|
||||
|
||||
def on_att_exchange_mtu_request(self, connection, request):
|
||||
def on_att_exchange_mtu_request(
|
||||
self, connection: Connection, request: att.ATT_Exchange_MTU_Request
|
||||
):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.2.1 Exchange MTU Request
|
||||
'''
|
||||
self.send_response(
|
||||
connection, ATT_Exchange_MTU_Response(server_rx_mtu=self.max_mtu)
|
||||
connection, att.ATT_Exchange_MTU_Response(server_rx_mtu=self.max_mtu)
|
||||
)
|
||||
|
||||
# Compute the final MTU
|
||||
if request.client_rx_mtu >= ATT_DEFAULT_MTU:
|
||||
if request.client_rx_mtu >= att.ATT_DEFAULT_MTU:
|
||||
mtu = min(self.max_mtu, request.client_rx_mtu)
|
||||
|
||||
# Notify the device
|
||||
@@ -594,11 +565,14 @@ class Server(utils.EventEmitter):
|
||||
else:
|
||||
logger.warning('invalid client_rx_mtu received, MTU not changed')
|
||||
|
||||
def on_att_find_information_request(self, connection, request):
|
||||
def on_att_find_information_request(
|
||||
self, connection: Connection, request: att.ATT_Find_Information_Request
|
||||
):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.3.1 Find Information Request
|
||||
'''
|
||||
|
||||
response: att.ATT_PDU
|
||||
# Check the request parameters
|
||||
if (
|
||||
request.starting_handle == 0
|
||||
@@ -606,17 +580,17 @@ class Server(utils.EventEmitter):
|
||||
):
|
||||
self.send_response(
|
||||
connection,
|
||||
ATT_Error_Response(
|
||||
att.ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.starting_handle,
|
||||
error_code=ATT_INVALID_HANDLE_ERROR,
|
||||
error_code=att.ATT_INVALID_HANDLE_ERROR,
|
||||
),
|
||||
)
|
||||
return
|
||||
|
||||
# Build list of returned attributes
|
||||
pdu_space_available = connection.att_mtu - 2
|
||||
attributes = []
|
||||
attributes: list[att.Attribute] = []
|
||||
uuid_size = 0
|
||||
for attribute in (
|
||||
attribute
|
||||
@@ -646,21 +620,23 @@ class Server(utils.EventEmitter):
|
||||
struct.pack('<H', attribute.handle) + attribute.type.to_pdu_bytes()
|
||||
for attribute in attributes
|
||||
]
|
||||
response = ATT_Find_Information_Response(
|
||||
response = att.ATT_Find_Information_Response(
|
||||
format=1 if len(attributes[0].type.to_pdu_bytes()) == 2 else 2,
|
||||
information_data=b''.join(information_data_list),
|
||||
)
|
||||
else:
|
||||
response = ATT_Error_Response(
|
||||
response = att.ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.starting_handle,
|
||||
error_code=ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
||||
error_code=att.ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
||||
)
|
||||
|
||||
self.send_response(connection, response)
|
||||
|
||||
@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: Connection, request: att.ATT_Find_By_Type_Value_Request
|
||||
):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.3.3 Find By Type Value Request
|
||||
'''
|
||||
@@ -668,6 +644,7 @@ class Server(utils.EventEmitter):
|
||||
# Build list of returned attributes
|
||||
pdu_space_available = connection.att_mtu - 2
|
||||
attributes = []
|
||||
response: att.ATT_PDU
|
||||
async for attribute in (
|
||||
attribute
|
||||
for attribute in self.attributes
|
||||
@@ -700,33 +677,35 @@ class Server(utils.EventEmitter):
|
||||
handles_information_list.append(
|
||||
struct.pack('<HH', attribute.handle, group_end_handle)
|
||||
)
|
||||
response = ATT_Find_By_Type_Value_Response(
|
||||
response = att.ATT_Find_By_Type_Value_Response(
|
||||
handles_information_list=b''.join(handles_information_list)
|
||||
)
|
||||
else:
|
||||
response = ATT_Error_Response(
|
||||
response = att.ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.starting_handle,
|
||||
error_code=ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
||||
error_code=att.ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
||||
)
|
||||
|
||||
self.send_response(connection, response)
|
||||
|
||||
@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: Connection, request: att.ATT_Read_By_Type_Request
|
||||
):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.4.1 Read By Type Request
|
||||
'''
|
||||
|
||||
pdu_space_available = connection.att_mtu - 2
|
||||
|
||||
response = ATT_Error_Response(
|
||||
response: att.ATT_PDU = att.ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.starting_handle,
|
||||
error_code=ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
||||
error_code=att.ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
||||
)
|
||||
|
||||
attributes = []
|
||||
attributes: list[tuple[int, bytes]] = []
|
||||
for attribute in (
|
||||
attribute
|
||||
for attribute in self.attributes
|
||||
@@ -737,11 +716,11 @@ class Server(utils.EventEmitter):
|
||||
):
|
||||
try:
|
||||
attribute_value = await attribute.read_value(connection)
|
||||
except ATT_Error as error:
|
||||
except att.ATT_Error as error:
|
||||
# If the first attribute is unreadable, return an error
|
||||
# Otherwise return attributes up to this point
|
||||
if not attributes:
|
||||
response = ATT_Error_Response(
|
||||
response = att.ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=attribute.handle,
|
||||
error_code=error.error_code,
|
||||
@@ -770,7 +749,7 @@ class Server(utils.EventEmitter):
|
||||
attribute_data_list = [
|
||||
struct.pack('<H', handle) + value for handle, value in attributes
|
||||
]
|
||||
response = ATT_Read_By_Type_Response(
|
||||
response = att.ATT_Read_By_Type_Response(
|
||||
length=entry_size, attribute_data_list=b''.join(attribute_data_list)
|
||||
)
|
||||
else:
|
||||
@@ -779,95 +758,104 @@ class Server(utils.EventEmitter):
|
||||
self.send_response(connection, response)
|
||||
|
||||
@utils.AsyncRunner.run_in_task()
|
||||
async def on_att_read_request(self, connection, request):
|
||||
async def on_att_read_request(
|
||||
self, connection: Connection, request: att.ATT_Read_Request
|
||||
):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.4.3 Read Request
|
||||
'''
|
||||
|
||||
response: att.ATT_PDU
|
||||
if attribute := self.get_attribute(request.attribute_handle):
|
||||
try:
|
||||
value = await attribute.read_value(connection)
|
||||
except ATT_Error as error:
|
||||
response = ATT_Error_Response(
|
||||
except att.ATT_Error as error:
|
||||
response = att.ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.attribute_handle,
|
||||
error_code=error.error_code,
|
||||
)
|
||||
else:
|
||||
value_size = min(connection.att_mtu - 1, len(value))
|
||||
response = ATT_Read_Response(attribute_value=value[:value_size])
|
||||
response = att.ATT_Read_Response(attribute_value=value[:value_size])
|
||||
else:
|
||||
response = ATT_Error_Response(
|
||||
response = att.ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.attribute_handle,
|
||||
error_code=ATT_INVALID_HANDLE_ERROR,
|
||||
error_code=att.ATT_INVALID_HANDLE_ERROR,
|
||||
)
|
||||
self.send_response(connection, response)
|
||||
|
||||
@utils.AsyncRunner.run_in_task()
|
||||
async def on_att_read_blob_request(self, connection, request):
|
||||
async def on_att_read_blob_request(
|
||||
self, connection: Connection, request: att.ATT_Read_Blob_Request
|
||||
):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.4.5 Read Blob Request
|
||||
'''
|
||||
|
||||
response: att.ATT_PDU
|
||||
if attribute := self.get_attribute(request.attribute_handle):
|
||||
try:
|
||||
value = await attribute.read_value(connection)
|
||||
except ATT_Error as error:
|
||||
response = ATT_Error_Response(
|
||||
except att.ATT_Error as error:
|
||||
response = att.ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.attribute_handle,
|
||||
error_code=error.error_code,
|
||||
)
|
||||
else:
|
||||
if request.value_offset > len(value):
|
||||
response = ATT_Error_Response(
|
||||
response = att.ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.attribute_handle,
|
||||
error_code=ATT_INVALID_OFFSET_ERROR,
|
||||
error_code=att.ATT_INVALID_OFFSET_ERROR,
|
||||
)
|
||||
elif len(value) <= connection.att_mtu - 1:
|
||||
response = ATT_Error_Response(
|
||||
response = att.ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.attribute_handle,
|
||||
error_code=ATT_ATTRIBUTE_NOT_LONG_ERROR,
|
||||
error_code=att.ATT_ATTRIBUTE_NOT_LONG_ERROR,
|
||||
)
|
||||
else:
|
||||
part_size = min(
|
||||
connection.att_mtu - 1, len(value) - request.value_offset
|
||||
)
|
||||
response = ATT_Read_Blob_Response(
|
||||
response = att.ATT_Read_Blob_Response(
|
||||
part_attribute_value=value[
|
||||
request.value_offset : request.value_offset + part_size
|
||||
]
|
||||
)
|
||||
else:
|
||||
response = ATT_Error_Response(
|
||||
response = att.ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.attribute_handle,
|
||||
error_code=ATT_INVALID_HANDLE_ERROR,
|
||||
error_code=att.ATT_INVALID_HANDLE_ERROR,
|
||||
)
|
||||
self.send_response(connection, response)
|
||||
|
||||
@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: Connection, request: att.ATT_Read_By_Group_Type_Request
|
||||
):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.4.9 Read by Group Type Request
|
||||
'''
|
||||
response: att.ATT_PDU
|
||||
if request.attribute_group_type not in (
|
||||
GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
|
||||
GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE,
|
||||
):
|
||||
response = ATT_Error_Response(
|
||||
response = att.ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.starting_handle,
|
||||
error_code=ATT_UNSUPPORTED_GROUP_TYPE_ERROR,
|
||||
error_code=att.ATT_UNSUPPORTED_GROUP_TYPE_ERROR,
|
||||
)
|
||||
self.send_response(connection, response)
|
||||
return
|
||||
|
||||
pdu_space_available = connection.att_mtu - 2
|
||||
attributes = []
|
||||
attributes: list[tuple[int, int, bytes]] = []
|
||||
for attribute in (
|
||||
attribute
|
||||
for attribute in self.attributes
|
||||
@@ -904,21 +892,23 @@ class Server(utils.EventEmitter):
|
||||
struct.pack('<HH', handle, end_group_handle) + value
|
||||
for handle, end_group_handle, value in attributes
|
||||
]
|
||||
response = ATT_Read_By_Group_Type_Response(
|
||||
response = att.ATT_Read_By_Group_Type_Response(
|
||||
length=len(attribute_data_list[0]),
|
||||
attribute_data_list=b''.join(attribute_data_list),
|
||||
)
|
||||
else:
|
||||
response = ATT_Error_Response(
|
||||
response = att.ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.starting_handle,
|
||||
error_code=ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
||||
error_code=att.ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
||||
)
|
||||
|
||||
self.send_response(connection, response)
|
||||
|
||||
@utils.AsyncRunner.run_in_task()
|
||||
async def on_att_write_request(self, connection, request):
|
||||
async def on_att_write_request(
|
||||
self, connection: Connection, request: att.ATT_Write_Request
|
||||
):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.5.1 Write Request
|
||||
'''
|
||||
@@ -928,10 +918,10 @@ class Server(utils.EventEmitter):
|
||||
if attribute is None:
|
||||
self.send_response(
|
||||
connection,
|
||||
ATT_Error_Response(
|
||||
att.ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.attribute_handle,
|
||||
error_code=ATT_INVALID_HANDLE_ERROR,
|
||||
error_code=att.ATT_INVALID_HANDLE_ERROR,
|
||||
),
|
||||
)
|
||||
return
|
||||
@@ -942,30 +932,33 @@ class Server(utils.EventEmitter):
|
||||
if len(request.attribute_value) > GATT_MAX_ATTRIBUTE_VALUE_SIZE:
|
||||
self.send_response(
|
||||
connection,
|
||||
ATT_Error_Response(
|
||||
att.ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.attribute_handle,
|
||||
error_code=ATT_INVALID_ATTRIBUTE_LENGTH_ERROR,
|
||||
error_code=att.ATT_INVALID_ATTRIBUTE_LENGTH_ERROR,
|
||||
),
|
||||
)
|
||||
return
|
||||
|
||||
response: att.ATT_PDU
|
||||
try:
|
||||
# Accept the value
|
||||
await attribute.write_value(connection, request.attribute_value)
|
||||
except ATT_Error as error:
|
||||
response = ATT_Error_Response(
|
||||
except att.ATT_Error as error:
|
||||
response = att.ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.attribute_handle,
|
||||
error_code=error.error_code,
|
||||
)
|
||||
else:
|
||||
# Done
|
||||
response = ATT_Write_Response()
|
||||
response = att.ATT_Write_Response()
|
||||
self.send_response(connection, response)
|
||||
|
||||
@utils.AsyncRunner.run_in_task()
|
||||
async def on_att_write_command(self, connection, request):
|
||||
async def on_att_write_command(
|
||||
self, connection: Connection, request: att.ATT_Write_Command
|
||||
):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.5.3 Write Command
|
||||
'''
|
||||
@@ -984,18 +977,25 @@ class Server(utils.EventEmitter):
|
||||
# Accept the value
|
||||
try:
|
||||
await attribute.write_value(connection, request.attribute_value)
|
||||
except Exception as error:
|
||||
logger.exception(f'!!! ignoring exception: {error}')
|
||||
except Exception:
|
||||
logger.exception('!!! ignoring exception')
|
||||
|
||||
def on_att_handle_value_confirmation(self, connection, _confirmation):
|
||||
def on_att_handle_value_confirmation(
|
||||
self,
|
||||
connection: Connection,
|
||||
confirmation: att.ATT_Handle_Value_Confirmation,
|
||||
):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.7.3 Handle Value Confirmation
|
||||
'''
|
||||
if self.pending_confirmations[connection.handle] is None:
|
||||
del confirmation # Unused.
|
||||
if (
|
||||
pending_confirmation := self.pending_confirmations[connection.handle]
|
||||
) is None:
|
||||
# Not expected!
|
||||
logger.warning(
|
||||
'!!! unexpected confirmation, there is no pending indication'
|
||||
)
|
||||
return
|
||||
|
||||
self.pending_confirmations[connection.handle].set_result(None)
|
||||
pending_confirmation.set_result(None)
|
||||
|
||||
132
bumble/hci.py
132
bumble/hci.py
@@ -16,19 +16,31 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import collections
|
||||
import dataclasses
|
||||
from dataclasses import field
|
||||
import enum
|
||||
import functools
|
||||
import logging
|
||||
import secrets
|
||||
import struct
|
||||
from collections.abc import Sequence
|
||||
from typing import Any, Callable, Iterable, Optional, Union, TypeVar, ClassVar, cast
|
||||
from dataclasses import field
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
ClassVar,
|
||||
Iterable,
|
||||
Literal,
|
||||
Optional,
|
||||
TypeVar,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from bumble import crypto
|
||||
from bumble import crypto, utils
|
||||
from bumble.colors import color
|
||||
from bumble.core import (
|
||||
DeviceClass,
|
||||
@@ -40,8 +52,6 @@ from bumble.core import (
|
||||
name_or_number,
|
||||
padded_bytes,
|
||||
)
|
||||
from bumble import utils
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -111,23 +121,57 @@ def phy_list_to_bits(phys: Optional[Iterable[Phy]]) -> int:
|
||||
class SpecableEnum(utils.OpenIntEnum):
|
||||
|
||||
@classmethod
|
||||
def type_spec(cls, size: int):
|
||||
return {'size': size, 'mapper': lambda x: cls(x).name}
|
||||
def type_spec(cls, size: int, byteorder: Literal['little', 'big'] = 'little'):
|
||||
return {
|
||||
'serializer': lambda x: x.to_bytes(size, byteorder),
|
||||
'parser': lambda data, offset: (
|
||||
offset + size,
|
||||
cls(int.from_bytes(data[offset : offset + size], byteorder)),
|
||||
),
|
||||
'mapper': lambda x: cls(x).name,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def type_metadata(cls, size: int, list_begin: bool = False, list_end: bool = False):
|
||||
return metadata(cls.type_spec(size), list_begin=list_begin, list_end=list_end)
|
||||
def type_metadata(
|
||||
cls,
|
||||
size: int,
|
||||
list_begin: bool = False,
|
||||
list_end: bool = False,
|
||||
byteorder: Literal['little', 'big'] = 'little',
|
||||
):
|
||||
return metadata(
|
||||
cls.type_spec(size, byteorder),
|
||||
list_begin=list_begin,
|
||||
list_end=list_end,
|
||||
)
|
||||
|
||||
|
||||
class SpecableFlag(enum.IntFlag):
|
||||
|
||||
@classmethod
|
||||
def type_spec(cls, size: int):
|
||||
return {'size': size, 'mapper': lambda x: cls(x).name}
|
||||
def type_spec(cls, size: int, byteorder: Literal['little', 'big'] = 'little'):
|
||||
return {
|
||||
'serializer': lambda x: x.to_bytes(size, byteorder),
|
||||
'parser': lambda data, offset: (
|
||||
offset + size,
|
||||
cls(int.from_bytes(data[offset : offset + size], byteorder)),
|
||||
),
|
||||
'mapper': lambda x: cls(x).name,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def type_metadata(cls, size: int, list_begin: bool = False, list_end: bool = False):
|
||||
return metadata(cls.type_spec(size), list_begin=list_begin, list_end=list_end)
|
||||
def type_metadata(
|
||||
cls,
|
||||
size: int,
|
||||
list_begin: bool = False,
|
||||
list_end: bool = False,
|
||||
byteorder: Literal['little', 'big'] = 'little',
|
||||
):
|
||||
return metadata(
|
||||
cls.type_spec(size, byteorder),
|
||||
list_begin=list_begin,
|
||||
list_end=list_end,
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -1322,7 +1366,7 @@ class LeFeature(SpecableEnum):
|
||||
MONITORING_ADVERTISERS = 64
|
||||
FRAME_SPACE_UPDATE = 65
|
||||
|
||||
class LeFeatureMask(enum.IntFlag):
|
||||
class LeFeatureMask(utils.CompatibleIntFlag):
|
||||
LE_ENCRYPTION = 1 << LeFeature.LE_ENCRYPTION
|
||||
CONNECTION_PARAMETERS_REQUEST_PROCEDURE = 1 << LeFeature.CONNECTION_PARAMETERS_REQUEST_PROCEDURE
|
||||
EXTENDED_REJECT_INDICATION = 1 << LeFeature.EXTENDED_REJECT_INDICATION
|
||||
@@ -1463,7 +1507,7 @@ class LmpFeature(SpecableEnum):
|
||||
SLOT_AVAILABILITY_MASK = 138
|
||||
TRAIN_NUDGING = 139
|
||||
|
||||
class LmpFeatureMask(enum.IntFlag):
|
||||
class LmpFeatureMask(utils.CompatibleIntFlag):
|
||||
# Page 0 (Legacy LMP features)
|
||||
LMP_3_SLOT_PACKETS = (1 << LmpFeature.LMP_3_SLOT_PACKETS)
|
||||
LMP_5_SLOT_PACKETS = (1 << LmpFeature.LMP_5_SLOT_PACKETS)
|
||||
@@ -2135,6 +2179,7 @@ class Address:
|
||||
if len(address) == 12 + 5:
|
||||
# Form with ':' separators
|
||||
address = address.replace(':', '')
|
||||
|
||||
self.address_bytes = bytes(reversed(bytes.fromhex(address)))
|
||||
|
||||
if len(self.address_bytes) != 6:
|
||||
@@ -5257,7 +5302,7 @@ class HCI_LE_BIG_Terminate_Sync_Command(HCI_Command):
|
||||
|
||||
return_parameters_fields = [
|
||||
('status', STATUS_SPEC),
|
||||
('big_handle', 2),
|
||||
('big_handle', 1),
|
||||
]
|
||||
|
||||
|
||||
@@ -5315,6 +5360,37 @@ class HCI_LE_Set_Host_Feature_Command(HCI_Command):
|
||||
bit_value: int = field(metadata=metadata(1))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command
|
||||
@dataclasses.dataclass
|
||||
class HCI_LE_Set_Default_Subrate_Command(HCI_Command):
|
||||
'''
|
||||
See Bluetooth spec @ 7.8.123 LE Set Default Subrate command
|
||||
'''
|
||||
|
||||
subrate_min: int = field(metadata=metadata(2))
|
||||
subrate_max: int = field(metadata=metadata(2))
|
||||
max_latency: int = field(metadata=metadata(2))
|
||||
continuation_number: int = field(metadata=metadata(2))
|
||||
supervision_timeout: int = field(metadata=metadata(2))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command
|
||||
@dataclasses.dataclass
|
||||
class HCI_LE_Subrate_Request_Command(HCI_Command):
|
||||
'''
|
||||
See Bluetooth spec @ 7.8.124 LE Subrate Request command
|
||||
'''
|
||||
|
||||
connection_handle: int = field(metadata=metadata(2))
|
||||
subrate_min: int = field(metadata=metadata(2))
|
||||
subrate_max: int = field(metadata=metadata(2))
|
||||
max_latency: int = field(metadata=metadata(2))
|
||||
continuation_number: int = field(metadata=metadata(2))
|
||||
supervision_timeout: int = field(metadata=metadata(2))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command
|
||||
@dataclasses.dataclass
|
||||
@@ -6390,7 +6466,9 @@ class HCI_LE_Create_BIG_Complete_Event(HCI_LE_Meta_Event):
|
||||
irc: int = field(metadata=metadata(1))
|
||||
max_pdu: int = field(metadata=metadata(2))
|
||||
iso_interval: int = field(metadata=metadata(2))
|
||||
connection_handle: int = field(metadata=metadata(2, list_begin=True, list_end=True))
|
||||
connection_handle: Sequence[int] = field(
|
||||
metadata=metadata(2, list_begin=True, list_end=True)
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -6422,7 +6500,9 @@ class HCI_LE_BIG_Sync_Established_Event(HCI_LE_Meta_Event):
|
||||
irc: int = field(metadata=metadata(1))
|
||||
max_pdu: int = field(metadata=metadata(2))
|
||||
iso_interval: int = field(metadata=metadata(2))
|
||||
connection_handle: int = field(metadata=metadata(2, list_begin=True, list_end=True))
|
||||
connection_handle: Sequence[int] = field(
|
||||
metadata=metadata(2, list_begin=True, list_end=True)
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -6460,6 +6540,22 @@ class HCI_LE_BIGInfo_Advertising_Report_Event(HCI_LE_Meta_Event):
|
||||
encryption: int = field(metadata=metadata(1))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_LE_Meta_Event.event
|
||||
@dataclasses.dataclass
|
||||
class HCI_LE_Subrate_Change_Event(HCI_LE_Meta_Event):
|
||||
'''
|
||||
See Bluetooth spec @ 7.7.65.35 LE Subrate Change event
|
||||
'''
|
||||
|
||||
status: int = field(metadata=metadata(STATUS_SPEC))
|
||||
connection_handle: int = field(metadata=metadata(2))
|
||||
subrate_factor: int = field(metadata=metadata(2))
|
||||
peripheral_latency: int = field(metadata=metadata(2))
|
||||
continuation_number: int = field(metadata=metadata(2))
|
||||
supervision_timeout: int = field(metadata=metadata(2))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_LE_Meta_Event.event
|
||||
@dataclasses.dataclass
|
||||
|
||||
@@ -17,43 +17,36 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, MutableMapping
|
||||
import datetime
|
||||
from typing import cast, Any, Optional
|
||||
import logging
|
||||
from collections.abc import Callable, MutableMapping
|
||||
from typing import Any, Optional, cast
|
||||
|
||||
from bumble import avc
|
||||
from bumble import avctp
|
||||
from bumble import avdtp
|
||||
from bumble import avrcp
|
||||
from bumble import crypto
|
||||
from bumble import rfcomm
|
||||
from bumble import sdp
|
||||
from bumble.colors import color
|
||||
from bumble import avc, avctp, avdtp, avrcp, crypto, rfcomm, sdp
|
||||
from bumble.att import ATT_CID, ATT_PDU
|
||||
from bumble.smp import SMP_CID, SMP_Command
|
||||
from bumble.colors import color
|
||||
from bumble.core import name_or_number
|
||||
from bumble.l2cap import (
|
||||
CommandCode,
|
||||
L2CAP_PDU,
|
||||
L2CAP_SIGNALING_CID,
|
||||
L2CAP_LE_SIGNALING_CID,
|
||||
L2CAP_Control_Frame,
|
||||
L2CAP_Connection_Request,
|
||||
L2CAP_Connection_Response,
|
||||
)
|
||||
from bumble.hci import (
|
||||
Address,
|
||||
HCI_EVENT_PACKET,
|
||||
HCI_ACL_DATA_PACKET,
|
||||
HCI_DISCONNECTION_COMPLETE_EVENT,
|
||||
HCI_AclDataPacketAssembler,
|
||||
HCI_Packet,
|
||||
HCI_Event,
|
||||
HCI_EVENT_PACKET,
|
||||
Address,
|
||||
HCI_AclDataPacket,
|
||||
HCI_AclDataPacketAssembler,
|
||||
HCI_Disconnection_Complete_Event,
|
||||
HCI_Event,
|
||||
HCI_Packet,
|
||||
)
|
||||
|
||||
from bumble.l2cap import (
|
||||
L2CAP_LE_SIGNALING_CID,
|
||||
L2CAP_PDU,
|
||||
L2CAP_SIGNALING_CID,
|
||||
CommandCode,
|
||||
L2CAP_Connection_Request,
|
||||
L2CAP_Connection_Response,
|
||||
L2CAP_Control_Frame,
|
||||
)
|
||||
from bumble.smp import SMP_CID, SMP_Command
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
|
||||
@@ -17,45 +17,34 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import collections
|
||||
import collections.abc
|
||||
import logging
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import enum
|
||||
import traceback
|
||||
import logging
|
||||
import re
|
||||
from typing import (
|
||||
Union,
|
||||
Any,
|
||||
Optional,
|
||||
ClassVar,
|
||||
Iterable,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
import traceback
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, Iterable, Optional, Union
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from bumble import at
|
||||
from bumble import device
|
||||
from bumble import rfcomm
|
||||
from bumble import sdp
|
||||
from bumble import utils
|
||||
from bumble import at, device, rfcomm, sdp, utils
|
||||
from bumble.colors import color
|
||||
from bumble.core import (
|
||||
ProtocolError,
|
||||
BT_GENERIC_AUDIO_SERVICE,
|
||||
BT_HANDSFREE_SERVICE,
|
||||
BT_HANDSFREE_AUDIO_GATEWAY_SERVICE,
|
||||
BT_HANDSFREE_SERVICE,
|
||||
BT_L2CAP_PROTOCOL_ID,
|
||||
BT_RFCOMM_PROTOCOL_ID,
|
||||
ProtocolError,
|
||||
)
|
||||
from bumble.hci import (
|
||||
HCI_Enhanced_Setup_Synchronous_Connection_Command,
|
||||
CodingFormat,
|
||||
CodecID,
|
||||
CodingFormat,
|
||||
HCI_Enhanced_Setup_Synchronous_Connection_Command,
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -16,22 +16,20 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
import enum
|
||||
import struct
|
||||
|
||||
import enum
|
||||
import logging
|
||||
import struct
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional, Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable, Optional
|
||||
|
||||
from typing_extensions import override
|
||||
|
||||
from bumble import l2cap
|
||||
from bumble import device
|
||||
from bumble import utils
|
||||
from bumble import device, l2cap, utils
|
||||
from bumble.core import InvalidStateError, ProtocolError
|
||||
from bumble.hci import Address
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -219,33 +217,41 @@ class HID(ABC, utils.EventEmitter):
|
||||
self.role = role
|
||||
|
||||
# Register ourselves with the L2CAP channel manager
|
||||
device.register_l2cap_server(HID_CONTROL_PSM, self.on_l2cap_connection)
|
||||
device.register_l2cap_server(HID_INTERRUPT_PSM, self.on_l2cap_connection)
|
||||
device.create_l2cap_server(
|
||||
l2cap.ClassicChannelSpec(HID_CONTROL_PSM), self.on_l2cap_connection
|
||||
)
|
||||
device.create_l2cap_server(
|
||||
l2cap.ClassicChannelSpec(HID_INTERRUPT_PSM), self.on_l2cap_connection
|
||||
)
|
||||
|
||||
device.on(device.EVENT_CONNECTION, self.on_device_connection)
|
||||
|
||||
async def connect_control_channel(self) -> None:
|
||||
if not self.connection:
|
||||
raise InvalidStateError("Connection is not established!")
|
||||
# Create a new L2CAP connection - control channel
|
||||
try:
|
||||
channel = await self.device.l2cap_channel_manager.connect(
|
||||
self.connection, HID_CONTROL_PSM
|
||||
channel = await self.connection.create_l2cap_channel(
|
||||
l2cap.ClassicChannelSpec(HID_CONTROL_PSM)
|
||||
)
|
||||
channel.sink = self.on_ctrl_pdu
|
||||
self.l2cap_ctrl_channel = channel
|
||||
except ProtocolError:
|
||||
logging.exception(f'L2CAP connection failed.')
|
||||
logging.exception('L2CAP connection failed.')
|
||||
raise
|
||||
|
||||
async def connect_interrupt_channel(self) -> None:
|
||||
if not self.connection:
|
||||
raise InvalidStateError("Connection is not established!")
|
||||
# Create a new L2CAP connection - interrupt channel
|
||||
try:
|
||||
channel = await self.device.l2cap_channel_manager.connect(
|
||||
self.connection, HID_INTERRUPT_PSM
|
||||
channel = await self.connection.create_l2cap_channel(
|
||||
l2cap.ClassicChannelSpec(HID_CONTROL_PSM)
|
||||
)
|
||||
channel.sink = self.on_intr_pdu
|
||||
self.l2cap_intr_channel = channel
|
||||
except ProtocolError:
|
||||
logging.exception(f'L2CAP connection failed.')
|
||||
logging.exception('L2CAP connection failed.')
|
||||
raise
|
||||
|
||||
async def disconnect_interrupt_channel(self) -> None:
|
||||
|
||||
273
bumble/host.py
273
bumble/host.py
@@ -16,33 +16,24 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import collections
|
||||
import dataclasses
|
||||
import logging
|
||||
import struct
|
||||
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Optional, Union, cast
|
||||
|
||||
from typing import (
|
||||
Any,
|
||||
Awaitable,
|
||||
Callable,
|
||||
Optional,
|
||||
cast,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
|
||||
from bumble import drivers, hci, utils
|
||||
from bumble.colors import color
|
||||
from bumble.core import (
|
||||
ConnectionParameters,
|
||||
ConnectionPHY,
|
||||
InvalidStateError,
|
||||
PhysicalTransport,
|
||||
)
|
||||
from bumble.l2cap import L2CAP_PDU
|
||||
from bumble.snoop import Snooper
|
||||
from bumble import drivers
|
||||
from bumble import hci
|
||||
from bumble.core import (
|
||||
PhysicalTransport,
|
||||
ConnectionPHY,
|
||||
ConnectionParameters,
|
||||
)
|
||||
from bumble import utils
|
||||
from bumble.transport.common import TransportLostError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -707,11 +698,9 @@ class Host(utils.EventEmitter):
|
||||
raise hci.HCI_Error(status)
|
||||
|
||||
return response
|
||||
except Exception as error:
|
||||
logger.exception(
|
||||
f'{color("!!! Exception while sending command:", "red")} {error}'
|
||||
)
|
||||
raise error
|
||||
except Exception:
|
||||
logger.exception(color("!!! Exception while sending command:", "red"))
|
||||
raise
|
||||
finally:
|
||||
self.pending_command = None
|
||||
self.pending_response = None
|
||||
@@ -918,10 +907,14 @@ class Host(utils.EventEmitter):
|
||||
def on_l2cap_pdu(self, connection: Connection, cid: int, pdu: bytes) -> None:
|
||||
self.emit('l2cap_pdu', connection.handle, cid, pdu)
|
||||
|
||||
def on_command_processed(self, event):
|
||||
def on_command_processed(
|
||||
self, event: Union[hci.HCI_Command_Complete_Event, hci.HCI_Command_Status_Event]
|
||||
):
|
||||
if self.pending_response:
|
||||
# Check that it is what we were expecting
|
||||
if self.pending_command.op_code != event.command_opcode:
|
||||
if self.pending_command is None:
|
||||
logger.warning('!!! pending_command is None ')
|
||||
elif self.pending_command.op_code != event.command_opcode:
|
||||
logger.warning(
|
||||
'!!! command result mismatch, expected '
|
||||
f'0x{self.pending_command.op_code:X} but got '
|
||||
@@ -935,10 +928,10 @@ class Host(utils.EventEmitter):
|
||||
############################################################
|
||||
# HCI handlers
|
||||
############################################################
|
||||
def on_hci_event(self, event):
|
||||
def on_hci_event(self, event: hci.HCI_Event):
|
||||
logger.warning(f'{color(f"--- Ignoring event {event}", "red")}')
|
||||
|
||||
def on_hci_command_complete_event(self, event):
|
||||
def on_hci_command_complete_event(self, event: hci.HCI_Command_Complete_Event):
|
||||
if event.command_opcode == 0:
|
||||
# This is used just for the Num_HCI_Command_Packets field, not related to
|
||||
# an actual command
|
||||
@@ -947,7 +940,7 @@ class Host(utils.EventEmitter):
|
||||
|
||||
return self.on_command_processed(event)
|
||||
|
||||
def on_hci_command_status_event(self, event):
|
||||
def on_hci_command_status_event(self, event: hci.HCI_Command_Status_Event):
|
||||
return self.on_command_processed(event)
|
||||
|
||||
def on_hci_number_of_completed_packets_event(
|
||||
@@ -967,7 +960,7 @@ class Host(utils.EventEmitter):
|
||||
)
|
||||
|
||||
# Classic only
|
||||
def on_hci_connection_request_event(self, event):
|
||||
def on_hci_connection_request_event(self, event: hci.HCI_Connection_Request_Event):
|
||||
# Notify the listeners
|
||||
self.emit(
|
||||
'connection_request',
|
||||
@@ -976,7 +969,14 @@ class Host(utils.EventEmitter):
|
||||
event.link_type,
|
||||
)
|
||||
|
||||
def on_hci_le_connection_complete_event(self, event):
|
||||
def on_hci_le_connection_complete_event(
|
||||
self,
|
||||
event: Union[
|
||||
hci.HCI_LE_Connection_Complete_Event,
|
||||
hci.HCI_LE_Enhanced_Connection_Complete_Event,
|
||||
hci.HCI_LE_Enhanced_Connection_Complete_V2_Event,
|
||||
],
|
||||
):
|
||||
# Check if this is a cancellation
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
# Create/update the connection
|
||||
@@ -1022,15 +1022,25 @@ class Host(utils.EventEmitter):
|
||||
event.status,
|
||||
)
|
||||
|
||||
def on_hci_le_enhanced_connection_complete_event(self, event):
|
||||
def on_hci_le_enhanced_connection_complete_event(
|
||||
self,
|
||||
event: Union[
|
||||
hci.HCI_LE_Enhanced_Connection_Complete_Event,
|
||||
hci.HCI_LE_Enhanced_Connection_Complete_V2_Event,
|
||||
],
|
||||
):
|
||||
# Just use the same implementation as for the non-enhanced event for now
|
||||
self.on_hci_le_connection_complete_event(event)
|
||||
|
||||
def on_hci_le_enhanced_connection_complete_v2_event(self, event):
|
||||
def on_hci_le_enhanced_connection_complete_v2_event(
|
||||
self, event: hci.HCI_LE_Enhanced_Connection_Complete_V2_Event
|
||||
):
|
||||
# Just use the same implementation as for the v1 event for now
|
||||
self.on_hci_le_enhanced_connection_complete_event(event)
|
||||
|
||||
def on_hci_connection_complete_event(self, event):
|
||||
def on_hci_connection_complete_event(
|
||||
self, event: hci.HCI_Connection_Complete_Event
|
||||
):
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
# Create/update the connection
|
||||
logger.debug(
|
||||
@@ -1070,7 +1080,9 @@ class Host(utils.EventEmitter):
|
||||
event.status,
|
||||
)
|
||||
|
||||
def on_hci_disconnection_complete_event(self, event):
|
||||
def on_hci_disconnection_complete_event(
|
||||
self, event: hci.HCI_Disconnection_Complete_Event
|
||||
):
|
||||
# Find the connection
|
||||
handle = event.connection_handle
|
||||
if (
|
||||
@@ -1109,7 +1121,9 @@ class Host(utils.EventEmitter):
|
||||
# Notify the listeners
|
||||
self.emit('disconnection_failure', handle, event.status)
|
||||
|
||||
def on_hci_le_connection_update_complete_event(self, event):
|
||||
def on_hci_le_connection_update_complete_event(
|
||||
self, event: hci.HCI_LE_Connection_Update_Complete_Event
|
||||
):
|
||||
if (connection := self.connections.get(event.connection_handle)) is None:
|
||||
logger.warning('!!! CONNECTION PARAMETERS UPDATE COMPLETE: unknown handle')
|
||||
return
|
||||
@@ -1129,7 +1143,9 @@ class Host(utils.EventEmitter):
|
||||
'connection_parameters_update_failure', connection.handle, event.status
|
||||
)
|
||||
|
||||
def on_hci_le_phy_update_complete_event(self, event):
|
||||
def on_hci_le_phy_update_complete_event(
|
||||
self, event: hci.HCI_LE_PHY_Update_Complete_Event
|
||||
):
|
||||
if (connection := self.connections.get(event.connection_handle)) is None:
|
||||
logger.warning('!!! CONNECTION PHY UPDATE COMPLETE: unknown handle')
|
||||
return
|
||||
@@ -1159,7 +1175,9 @@ class Host(utils.EventEmitter):
|
||||
):
|
||||
self.on_hci_le_advertising_report_event(event)
|
||||
|
||||
def on_hci_le_advertising_set_terminated_event(self, event):
|
||||
def on_hci_le_advertising_set_terminated_event(
|
||||
self, event: hci.HCI_LE_Advertising_Set_Terminated_Event
|
||||
):
|
||||
self.emit(
|
||||
'advertising_set_termination',
|
||||
event.status,
|
||||
@@ -1168,7 +1186,9 @@ class Host(utils.EventEmitter):
|
||||
event.num_completed_extended_advertising_events,
|
||||
)
|
||||
|
||||
def on_hci_le_periodic_advertising_sync_established_event(self, event):
|
||||
def on_hci_le_periodic_advertising_sync_established_event(
|
||||
self, event: hci.HCI_LE_Periodic_Advertising_Sync_Established_Event
|
||||
):
|
||||
self.emit(
|
||||
'periodic_advertising_sync_establishment',
|
||||
event.status,
|
||||
@@ -1180,16 +1200,22 @@ class Host(utils.EventEmitter):
|
||||
event.advertiser_clock_accuracy,
|
||||
)
|
||||
|
||||
def on_hci_le_periodic_advertising_sync_lost_event(self, event):
|
||||
def on_hci_le_periodic_advertising_sync_lost_event(
|
||||
self, event: hci.HCI_LE_Periodic_Advertising_Sync_Lost_Event
|
||||
):
|
||||
self.emit('periodic_advertising_sync_loss', event.sync_handle)
|
||||
|
||||
def on_hci_le_periodic_advertising_report_event(self, event):
|
||||
def on_hci_le_periodic_advertising_report_event(
|
||||
self, event: hci.HCI_LE_Periodic_Advertising_Report_Event
|
||||
):
|
||||
self.emit('periodic_advertising_report', event.sync_handle, event)
|
||||
|
||||
def on_hci_le_biginfo_advertising_report_event(self, event):
|
||||
def on_hci_le_biginfo_advertising_report_event(
|
||||
self, event: hci.HCI_LE_BIGInfo_Advertising_Report_Event
|
||||
):
|
||||
self.emit('biginfo_advertising_report', event.sync_handle, event)
|
||||
|
||||
def on_hci_le_cis_request_event(self, event):
|
||||
def on_hci_le_cis_request_event(self, event: hci.HCI_LE_CIS_Request_Event):
|
||||
self.emit(
|
||||
'cis_request',
|
||||
event.acl_connection_handle,
|
||||
@@ -1198,10 +1224,12 @@ class Host(utils.EventEmitter):
|
||||
event.cis_id,
|
||||
)
|
||||
|
||||
def on_hci_le_create_big_complete_event(self, event):
|
||||
def on_hci_le_create_big_complete_event(
|
||||
self, event: hci.HCI_LE_Create_BIG_Complete_Event
|
||||
):
|
||||
self.bigs[event.big_handle] = set(event.connection_handle)
|
||||
if self.iso_packet_queue is None:
|
||||
logger.warning("BIS established but ISO packets not supported")
|
||||
raise InvalidStateError("BIS established but ISO packets not supported")
|
||||
|
||||
for connection_handle in event.connection_handle:
|
||||
self.bis_links[connection_handle] = IsoLink(
|
||||
@@ -1224,8 +1252,13 @@ class Host(utils.EventEmitter):
|
||||
event.iso_interval,
|
||||
)
|
||||
|
||||
def on_hci_le_big_sync_established_event(self, event):
|
||||
def on_hci_le_big_sync_established_event(
|
||||
self, event: hci.HCI_LE_BIG_Sync_Established_Event
|
||||
):
|
||||
self.bigs[event.big_handle] = set(event.connection_handle)
|
||||
if self.iso_packet_queue is None:
|
||||
raise InvalidStateError("BIS established but ISO packets not supported")
|
||||
|
||||
for connection_handle in event.connection_handle:
|
||||
self.bis_links[connection_handle] = IsoLink(
|
||||
connection_handle, self.iso_packet_queue
|
||||
@@ -1245,15 +1278,19 @@ class Host(utils.EventEmitter):
|
||||
event.connection_handle,
|
||||
)
|
||||
|
||||
def on_hci_le_big_sync_lost_event(self, event):
|
||||
def on_hci_le_big_sync_lost_event(self, event: hci.HCI_LE_BIG_Sync_Lost_Event):
|
||||
self.remove_big(event.big_handle)
|
||||
self.emit('big_sync_lost', event.big_handle, event.reason)
|
||||
|
||||
def on_hci_le_terminate_big_complete_event(self, event):
|
||||
def on_hci_le_terminate_big_complete_event(
|
||||
self, event: hci.HCI_LE_Terminate_BIG_Complete_Event
|
||||
):
|
||||
self.remove_big(event.big_handle)
|
||||
self.emit('big_termination', event.reason, event.big_handle)
|
||||
|
||||
def on_hci_le_periodic_advertising_sync_transfer_received_event(self, event):
|
||||
def on_hci_le_periodic_advertising_sync_transfer_received_event(
|
||||
self, event: hci.HCI_LE_Periodic_Advertising_Sync_Transfer_Received_Event
|
||||
):
|
||||
self.emit(
|
||||
'periodic_advertising_sync_transfer',
|
||||
event.status,
|
||||
@@ -1266,7 +1303,9 @@ class Host(utils.EventEmitter):
|
||||
event.advertiser_clock_accuracy,
|
||||
)
|
||||
|
||||
def on_hci_le_periodic_advertising_sync_transfer_received_v2_event(self, event):
|
||||
def on_hci_le_periodic_advertising_sync_transfer_received_v2_event(
|
||||
self, event: hci.HCI_LE_Periodic_Advertising_Sync_Transfer_Received_V2_Event
|
||||
):
|
||||
self.emit(
|
||||
'periodic_advertising_sync_transfer',
|
||||
event.status,
|
||||
@@ -1279,11 +1318,11 @@ class Host(utils.EventEmitter):
|
||||
event.advertiser_clock_accuracy,
|
||||
)
|
||||
|
||||
def on_hci_le_cis_established_event(self, event):
|
||||
def on_hci_le_cis_established_event(self, event: hci.HCI_LE_CIS_Established_Event):
|
||||
# The remaining parameters are unused for now.
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
if self.iso_packet_queue is None:
|
||||
logger.warning("CIS established but ISO packets not supported")
|
||||
raise InvalidStateError("CIS established but ISO packets not supported")
|
||||
self.cis_links[event.connection_handle] = IsoLink(
|
||||
handle=event.connection_handle, packet_queue=self.iso_packet_queue
|
||||
)
|
||||
@@ -1310,7 +1349,9 @@ class Host(utils.EventEmitter):
|
||||
'cis_establishment_failure', event.connection_handle, event.status
|
||||
)
|
||||
|
||||
def on_hci_le_remote_connection_parameter_request_event(self, event):
|
||||
def on_hci_le_remote_connection_parameter_request_event(
|
||||
self, event: hci.HCI_LE_Remote_Connection_Parameter_Request_Event
|
||||
):
|
||||
if event.connection_handle not in self.connections:
|
||||
logger.warning('!!! REMOTE CONNECTION PARAMETER REQUEST: unknown handle')
|
||||
return
|
||||
@@ -1329,7 +1370,9 @@ class Host(utils.EventEmitter):
|
||||
)
|
||||
)
|
||||
|
||||
def on_hci_le_long_term_key_request_event(self, event):
|
||||
def on_hci_le_long_term_key_request_event(
|
||||
self, event: hci.HCI_LE_Long_Term_Key_Request_Event
|
||||
):
|
||||
if (connection := self.connections.get(event.connection_handle)) is None:
|
||||
logger.warning('!!! LE LONG TERM KEY REQUEST: unknown handle')
|
||||
return
|
||||
@@ -1363,7 +1406,9 @@ class Host(utils.EventEmitter):
|
||||
|
||||
asyncio.create_task(send_long_term_key())
|
||||
|
||||
def on_hci_synchronous_connection_complete_event(self, event):
|
||||
def on_hci_synchronous_connection_complete_event(
|
||||
self, event: hci.HCI_Synchronous_Connection_Complete_Event
|
||||
):
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
# Create/update the connection
|
||||
logger.debug(
|
||||
@@ -1389,7 +1434,9 @@ class Host(utils.EventEmitter):
|
||||
# Notify the client
|
||||
self.emit('sco_connection_failure', event.bd_addr, event.status)
|
||||
|
||||
def on_hci_synchronous_connection_changed_event(self, event):
|
||||
def on_hci_synchronous_connection_changed_event(
|
||||
self, event: hci.HCI_Synchronous_Connection_Changed_Event
|
||||
):
|
||||
pass
|
||||
|
||||
def on_hci_mode_change_event(self, event: hci.HCI_Mode_Change_Event):
|
||||
@@ -1401,7 +1448,7 @@ class Host(utils.EventEmitter):
|
||||
event.interval,
|
||||
)
|
||||
|
||||
def on_hci_role_change_event(self, event):
|
||||
def on_hci_role_change_event(self, event: hci.HCI_Role_Change_Event):
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
logger.debug(
|
||||
f'role change for {event.bd_addr}: '
|
||||
@@ -1415,7 +1462,9 @@ class Host(utils.EventEmitter):
|
||||
)
|
||||
self.emit('role_change_failure', event.bd_addr, event.status)
|
||||
|
||||
def on_hci_le_data_length_change_event(self, event):
|
||||
def on_hci_le_data_length_change_event(
|
||||
self, event: hci.HCI_LE_Data_Length_Change_Event
|
||||
):
|
||||
if (connection := self.connections.get(event.connection_handle)) is None:
|
||||
logger.warning('!!! DATA LENGTH CHANGE: unknown handle')
|
||||
return
|
||||
@@ -1429,7 +1478,9 @@ class Host(utils.EventEmitter):
|
||||
event.max_rx_time,
|
||||
)
|
||||
|
||||
def on_hci_authentication_complete_event(self, event):
|
||||
def on_hci_authentication_complete_event(
|
||||
self, event: hci.HCI_Authentication_Complete_Event
|
||||
):
|
||||
# Notify the client
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
self.emit('connection_authentication', event.connection_handle)
|
||||
@@ -1470,7 +1521,9 @@ class Host(utils.EventEmitter):
|
||||
'connection_encryption_failure', event.connection_handle, event.status
|
||||
)
|
||||
|
||||
def on_hci_encryption_key_refresh_complete_event(self, event):
|
||||
def on_hci_encryption_key_refresh_complete_event(
|
||||
self, event: hci.HCI_Encryption_Key_Refresh_Complete_Event
|
||||
):
|
||||
# Notify the client
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
self.emit('connection_encryption_key_refresh', event.connection_handle)
|
||||
@@ -1481,7 +1534,7 @@ class Host(utils.EventEmitter):
|
||||
event.status,
|
||||
)
|
||||
|
||||
def on_hci_qos_setup_complete_event(self, event):
|
||||
def on_hci_qos_setup_complete_event(self, event: hci.HCI_QOS_Setup_Complete_Event):
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
self.emit(
|
||||
'connection_qos_setup', event.connection_handle, event.service_type
|
||||
@@ -1493,23 +1546,31 @@ class Host(utils.EventEmitter):
|
||||
event.status,
|
||||
)
|
||||
|
||||
def on_hci_link_supervision_timeout_changed_event(self, event):
|
||||
def on_hci_link_supervision_timeout_changed_event(
|
||||
self, event: hci.HCI_Link_Supervision_Timeout_Changed_Event
|
||||
):
|
||||
pass
|
||||
|
||||
def on_hci_max_slots_change_event(self, event):
|
||||
def on_hci_max_slots_change_event(self, event: hci.HCI_Max_Slots_Change_Event):
|
||||
pass
|
||||
|
||||
def on_hci_page_scan_repetition_mode_change_event(self, event):
|
||||
def on_hci_page_scan_repetition_mode_change_event(
|
||||
self, event: hci.HCI_Page_Scan_Repetition_Mode_Change_Event
|
||||
):
|
||||
pass
|
||||
|
||||
def on_hci_link_key_notification_event(self, event):
|
||||
def on_hci_link_key_notification_event(
|
||||
self, event: hci.HCI_Link_Key_Notification_Event
|
||||
):
|
||||
logger.debug(
|
||||
f'link key for {event.bd_addr}: {event.link_key.hex()}, '
|
||||
f'type={hci.HCI_Constant.link_key_type_name(event.key_type)}'
|
||||
)
|
||||
self.emit('link_key', event.bd_addr, event.link_key, event.key_type)
|
||||
|
||||
def on_hci_simple_pairing_complete_event(self, event):
|
||||
def on_hci_simple_pairing_complete_event(
|
||||
self, event: hci.HCI_Simple_Pairing_Complete_Event
|
||||
):
|
||||
logger.debug(
|
||||
f'simple pairing complete for {event.bd_addr}: '
|
||||
f'status={hci.HCI_Constant.status_name(event.status)}'
|
||||
@@ -1519,10 +1580,10 @@ class Host(utils.EventEmitter):
|
||||
else:
|
||||
self.emit('classic_pairing_failure', event.bd_addr, event.status)
|
||||
|
||||
def on_hci_pin_code_request_event(self, event):
|
||||
def on_hci_pin_code_request_event(self, event: hci.HCI_PIN_Code_Request_Event):
|
||||
self.emit('pin_code_request', event.bd_addr)
|
||||
|
||||
def on_hci_link_key_request_event(self, event):
|
||||
def on_hci_link_key_request_event(self, event: hci.HCI_Link_Key_Request_Event):
|
||||
async def send_link_key():
|
||||
if self.link_key_provider is None:
|
||||
logger.debug('no link key provider')
|
||||
@@ -1547,10 +1608,14 @@ class Host(utils.EventEmitter):
|
||||
|
||||
asyncio.create_task(send_link_key())
|
||||
|
||||
def on_hci_io_capability_request_event(self, event):
|
||||
def on_hci_io_capability_request_event(
|
||||
self, event: hci.HCI_IO_Capability_Request_Event
|
||||
):
|
||||
self.emit('authentication_io_capability_request', event.bd_addr)
|
||||
|
||||
def on_hci_io_capability_response_event(self, event):
|
||||
def on_hci_io_capability_response_event(
|
||||
self, event: hci.HCI_IO_Capability_Response_Event
|
||||
):
|
||||
self.emit(
|
||||
'authentication_io_capability_response',
|
||||
event.bd_addr,
|
||||
@@ -1558,25 +1623,33 @@ class Host(utils.EventEmitter):
|
||||
event.authentication_requirements,
|
||||
)
|
||||
|
||||
def on_hci_user_confirmation_request_event(self, event):
|
||||
def on_hci_user_confirmation_request_event(
|
||||
self, event: hci.HCI_User_Confirmation_Request_Event
|
||||
):
|
||||
self.emit(
|
||||
'authentication_user_confirmation_request',
|
||||
event.bd_addr,
|
||||
event.numeric_value,
|
||||
)
|
||||
|
||||
def on_hci_user_passkey_request_event(self, event):
|
||||
def on_hci_user_passkey_request_event(
|
||||
self, event: hci.HCI_User_Passkey_Request_Event
|
||||
):
|
||||
self.emit('authentication_user_passkey_request', event.bd_addr)
|
||||
|
||||
def on_hci_user_passkey_notification_event(self, event):
|
||||
def on_hci_user_passkey_notification_event(
|
||||
self, event: hci.HCI_User_Passkey_Notification_Event
|
||||
):
|
||||
self.emit(
|
||||
'authentication_user_passkey_notification', event.bd_addr, event.passkey
|
||||
)
|
||||
|
||||
def on_hci_inquiry_complete_event(self, _event):
|
||||
def on_hci_inquiry_complete_event(self, _event: hci.HCI_Inquiry_Complete_Event):
|
||||
self.emit('inquiry_complete')
|
||||
|
||||
def on_hci_inquiry_result_with_rssi_event(self, event):
|
||||
def on_hci_inquiry_result_with_rssi_event(
|
||||
self, event: hci.HCI_Inquiry_Result_With_RSSI_Event
|
||||
):
|
||||
for bd_addr, class_of_device, rssi in zip(
|
||||
event.bd_addr, event.class_of_device, event.rssi
|
||||
):
|
||||
@@ -1588,7 +1661,9 @@ class Host(utils.EventEmitter):
|
||||
rssi,
|
||||
)
|
||||
|
||||
def on_hci_extended_inquiry_result_event(self, event):
|
||||
def on_hci_extended_inquiry_result_event(
|
||||
self, event: hci.HCI_Extended_Inquiry_Result_Event
|
||||
):
|
||||
self.emit(
|
||||
'inquiry_result',
|
||||
event.bd_addr,
|
||||
@@ -1597,7 +1672,9 @@ class Host(utils.EventEmitter):
|
||||
event.rssi,
|
||||
)
|
||||
|
||||
def on_hci_remote_name_request_complete_event(self, event):
|
||||
def on_hci_remote_name_request_complete_event(
|
||||
self, event: hci.HCI_Remote_Name_Request_Complete_Event
|
||||
):
|
||||
if event.status != hci.HCI_SUCCESS:
|
||||
self.emit('remote_name_failure', event.bd_addr, event.status)
|
||||
else:
|
||||
@@ -1608,14 +1685,18 @@ class Host(utils.EventEmitter):
|
||||
|
||||
self.emit('remote_name', event.bd_addr, utf8_name)
|
||||
|
||||
def on_hci_remote_host_supported_features_notification_event(self, event):
|
||||
def on_hci_remote_host_supported_features_notification_event(
|
||||
self, event: hci.HCI_Remote_Host_Supported_Features_Notification_Event
|
||||
):
|
||||
self.emit(
|
||||
'remote_host_supported_features',
|
||||
event.bd_addr,
|
||||
event.host_supported_features,
|
||||
)
|
||||
|
||||
def on_hci_le_read_remote_features_complete_event(self, event):
|
||||
def on_hci_le_read_remote_features_complete_event(
|
||||
self, event: hci.HCI_LE_Read_Remote_Features_Complete_Event
|
||||
):
|
||||
if event.status != hci.HCI_SUCCESS:
|
||||
self.emit(
|
||||
'le_remote_features_failure', event.connection_handle, event.status
|
||||
@@ -1627,23 +1708,45 @@ class Host(utils.EventEmitter):
|
||||
int.from_bytes(event.le_features, 'little'),
|
||||
)
|
||||
|
||||
def on_hci_le_cs_read_remote_supported_capabilities_complete_event(self, event):
|
||||
def on_hci_le_cs_read_remote_supported_capabilities_complete_event(
|
||||
self, event: hci.HCI_LE_CS_Read_Remote_Supported_Capabilities_Complete_Event
|
||||
):
|
||||
self.emit('cs_remote_supported_capabilities', event)
|
||||
|
||||
def on_hci_le_cs_security_enable_complete_event(self, event):
|
||||
def on_hci_le_cs_security_enable_complete_event(
|
||||
self, event: hci.HCI_LE_CS_Security_Enable_Complete_Event
|
||||
):
|
||||
self.emit('cs_security', event)
|
||||
|
||||
def on_hci_le_cs_config_complete_event(self, event):
|
||||
def on_hci_le_cs_config_complete_event(
|
||||
self, event: hci.HCI_LE_CS_Config_Complete_Event
|
||||
):
|
||||
self.emit('cs_config', event)
|
||||
|
||||
def on_hci_le_cs_procedure_enable_complete_event(self, event):
|
||||
def on_hci_le_cs_procedure_enable_complete_event(
|
||||
self, event: hci.HCI_LE_CS_Procedure_Enable_Complete_Event
|
||||
):
|
||||
self.emit('cs_procedure', event)
|
||||
|
||||
def on_hci_le_cs_subevent_result_event(self, event):
|
||||
def on_hci_le_cs_subevent_result_event(
|
||||
self, event: hci.HCI_LE_CS_Subevent_Result_Event
|
||||
):
|
||||
self.emit('cs_subevent_result', event)
|
||||
|
||||
def on_hci_le_cs_subevent_result_continue_event(self, event):
|
||||
def on_hci_le_cs_subevent_result_continue_event(
|
||||
self, event: hci.HCI_LE_CS_Subevent_Result_Continue_Event
|
||||
):
|
||||
self.emit('cs_subevent_result_continue', event)
|
||||
|
||||
def on_hci_vendor_event(self, event):
|
||||
def on_hci_le_subrate_change_event(self, event: hci.HCI_LE_Subrate_Change_Event):
|
||||
self.emit(
|
||||
'le_subrate_change',
|
||||
event.connection_handle,
|
||||
event.subrate_factor,
|
||||
event.peripheral_latency,
|
||||
event.continuation_number,
|
||||
event.supervision_timeout,
|
||||
)
|
||||
|
||||
def on_hci_vendor_event(self, event: hci.HCI_Vendor_Event):
|
||||
self.emit('vendor_event', event)
|
||||
|
||||
@@ -21,16 +21,18 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import json
|
||||
from typing import TYPE_CHECKING, Optional, Any
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from bumble.colors import color
|
||||
from bumble import hci
|
||||
from bumble.colors import color
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bumble.device import Device
|
||||
|
||||
242
bumble/l2cap.py
242
bumble/l2cap.py
@@ -16,32 +16,32 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import enum
|
||||
import logging
|
||||
import struct
|
||||
|
||||
from collections import deque
|
||||
from collections.abc import Sequence
|
||||
from typing import (
|
||||
Optional,
|
||||
Callable,
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Union,
|
||||
Callable,
|
||||
ClassVar,
|
||||
Iterable,
|
||||
Optional,
|
||||
SupportsBytes,
|
||||
TypeVar,
|
||||
ClassVar,
|
||||
TYPE_CHECKING,
|
||||
Union,
|
||||
)
|
||||
|
||||
from bumble import utils
|
||||
from bumble import hci
|
||||
from bumble import hci, utils
|
||||
from bumble.colors import color
|
||||
from bumble.core import (
|
||||
InvalidStateError,
|
||||
InvalidArgumentError,
|
||||
InvalidPacketError,
|
||||
InvalidStateError,
|
||||
OutOfResourcesError,
|
||||
ProtocolError,
|
||||
)
|
||||
@@ -112,6 +112,10 @@ class CommandCode(hci.SpecableEnum):
|
||||
L2CAP_LE_CREDIT_BASED_CONNECTION_REQUEST = 0x14
|
||||
L2CAP_LE_CREDIT_BASED_CONNECTION_RESPONSE = 0x15
|
||||
L2CAP_LE_FLOW_CONTROL_CREDIT = 0x16
|
||||
L2CAP_CREDIT_BASED_CONNECTION_REQUEST = 0x17
|
||||
L2CAP_CREDIT_BASED_CONNECTION_RESPONSE = 0x18
|
||||
L2CAP_CREDIT_BASED_RECONFIGURE_REQUEST = 0x19
|
||||
L2CAP_CREDIT_BASED_RECONFIGURE_RESPONSE = 0x1A
|
||||
|
||||
L2CAP_CONNECTION_PARAMETERS_ACCEPTED_RESULT = 0x0000
|
||||
L2CAP_CONNECTION_PARAMETERS_REJECTED_RESULT = 0x0001
|
||||
@@ -213,7 +217,7 @@ class L2CAP_Control_Frame:
|
||||
fields: ClassVar[hci.Fields] = ()
|
||||
code: int = dataclasses.field(default=0, init=False)
|
||||
name: str = dataclasses.field(default='', init=False)
|
||||
_data: Optional[bytes] = dataclasses.field(default=None, init=False)
|
||||
_payload: Optional[bytes] = dataclasses.field(default=None, init=False)
|
||||
|
||||
identifier: int
|
||||
|
||||
@@ -223,7 +227,8 @@ class L2CAP_Control_Frame:
|
||||
|
||||
subclass = L2CAP_Control_Frame.classes.get(code)
|
||||
if subclass is None:
|
||||
instance = L2CAP_Control_Frame(pdu)
|
||||
instance = L2CAP_Control_Frame(identifier=identifier)
|
||||
instance.payload = pdu[4:]
|
||||
instance.code = CommandCode(code)
|
||||
instance.name = instance.code.name
|
||||
return instance
|
||||
@@ -232,11 +237,11 @@ class L2CAP_Control_Frame:
|
||||
identifier=identifier,
|
||||
)
|
||||
frame.identifier = identifier
|
||||
frame.data = pdu[4:]
|
||||
if length != len(pdu):
|
||||
frame.payload = pdu[4:]
|
||||
if length != len(frame.payload):
|
||||
logger.warning(
|
||||
color(
|
||||
f'!!! length mismatch: expected {len(pdu) - 4} but got {length}',
|
||||
f'!!! length mismatch: expected {length} but got {len(frame.payload)}',
|
||||
'red',
|
||||
)
|
||||
)
|
||||
@@ -273,34 +278,20 @@ class L2CAP_Control_Frame:
|
||||
|
||||
return subclass
|
||||
|
||||
def __init__(self, pdu: Optional[bytes] = None, **kwargs) -> None:
|
||||
self.identifier = kwargs.get('identifier', 0)
|
||||
if self.fields:
|
||||
if kwargs:
|
||||
hci.HCI_Object.init_from_fields(self, self.fields, kwargs)
|
||||
if pdu is None:
|
||||
data = hci.HCI_Object.dict_to_bytes(kwargs, self.fields)
|
||||
pdu = (
|
||||
bytes([self.code, self.identifier])
|
||||
+ struct.pack('<H', len(data))
|
||||
+ data
|
||||
)
|
||||
self.data = pdu[4:] if pdu else b''
|
||||
|
||||
@property
|
||||
def data(self) -> bytes:
|
||||
if self._data is None:
|
||||
self._data = hci.HCI_Object.dict_to_bytes(self.__dict__, self.fields)
|
||||
return self._data
|
||||
def payload(self) -> bytes:
|
||||
if self._payload is None:
|
||||
self._payload = hci.HCI_Object.dict_to_bytes(self.__dict__, self.fields)
|
||||
return self._payload
|
||||
|
||||
@data.setter
|
||||
def data(self, parameters: bytes) -> None:
|
||||
self._data = parameters
|
||||
@payload.setter
|
||||
def payload(self, payload: bytes) -> None:
|
||||
self._payload = payload
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
return (
|
||||
struct.pack('<BBH', self.code, self.identifier, len(self.data) + 4)
|
||||
+ self.data
|
||||
struct.pack('<BBH', self.code, self.identifier, len(self.payload))
|
||||
+ self.payload
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
@@ -308,8 +299,8 @@ class L2CAP_Control_Frame:
|
||||
if fields := getattr(self, 'fields', None):
|
||||
result += ':\n' + hci.HCI_Object.format_fields(self.__dict__, fields, ' ')
|
||||
else:
|
||||
if len(self.data) > 1:
|
||||
result += f': {self.data.hex()}'
|
||||
if len(self.payload) > 1:
|
||||
result += f': {self.payload.hex()}'
|
||||
return result
|
||||
|
||||
|
||||
@@ -608,6 +599,109 @@ class L2CAP_LE_Flow_Control_Credit(L2CAP_Control_Frame):
|
||||
credits: int = dataclasses.field(metadata=hci.metadata(2))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@L2CAP_Control_Frame.subclass
|
||||
@dataclasses.dataclass
|
||||
class L2CAP_Credit_Based_Connection_Request(L2CAP_Control_Frame):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part A - 4.25 L2CAP_CREDIT_BASED_CONNECTION_REQ (0x17).
|
||||
'''
|
||||
|
||||
@classmethod
|
||||
def parse_cid_list(cls, data: bytes, offset: int) -> tuple[int, list[int]]:
|
||||
count = (len(data) - offset) // 2
|
||||
return len(data), list(struct.unpack_from("<" + ("H" * count), data, offset))
|
||||
|
||||
@classmethod
|
||||
def serialize_cid_list(cls, cids: Sequence[int]) -> bytes:
|
||||
return b"".join([struct.pack("<H", cid) for cid in cids])
|
||||
|
||||
CID_METADATA: ClassVar[dict[str, Any]] = hci.metadata(
|
||||
{
|
||||
'parser': lambda data, offset: L2CAP_Credit_Based_Connection_Request.parse_cid_list(
|
||||
data, offset
|
||||
),
|
||||
'serializer': lambda value: L2CAP_Credit_Based_Connection_Request.serialize_cid_list(
|
||||
value
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
spsm: int = dataclasses.field(metadata=hci.metadata(2))
|
||||
mtu: int = dataclasses.field(metadata=hci.metadata(2))
|
||||
mps: int = dataclasses.field(metadata=hci.metadata(2))
|
||||
initial_credits: int = dataclasses.field(metadata=hci.metadata(2))
|
||||
source_cid: Sequence[int] = dataclasses.field(metadata=CID_METADATA)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@L2CAP_Control_Frame.subclass
|
||||
@dataclasses.dataclass
|
||||
class L2CAP_Credit_Based_Connection_Response(L2CAP_Control_Frame):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part A - 4.26 L2CAP_CREDIT_BASED_CONNECTION_RSP (0x18).
|
||||
'''
|
||||
|
||||
class Result(hci.SpecableEnum):
|
||||
ALL_CONNECTIONS_SUCCESSFUL = 0x0000
|
||||
ALL_CONNECTIONS_REFUSED_SPSM_NOT_SUPPORTED = 0x0002
|
||||
SOME_CONNECTIONS_REFUSED_INSUFFICIENT_RESOURCES_AVAILABLE = 0x0004
|
||||
ALL_CONNECTIONS_REFUSED_INSUFFICIENT_AUTHENTICATION = 0x0005
|
||||
ALL_CONNECTIONS_REFUSED_INSUFFICIENT_AUTHORIZATION = 0x0006
|
||||
ALL_CONNECTIONS_REFUSED_ENCRYPTION_KEY_SIZE_TOO_SHORT = 0x0007
|
||||
ALL_CONNECTIONS_REFUSED_INSUFFICIENT_ENCRYPTION = 0x0008
|
||||
SOME_CONNECTIONS_REFUSED_INVALID_SOURCE_CID = 0x0009
|
||||
SOME_CONNECTIONS_REFUSED_SOURCE_CID_ALREADY_ALLOCATED = 0x000A
|
||||
ALL_CONNECTIONS_REFUSED_UNACCEPTABLE_PARAMETERS = 0x000B
|
||||
ALL_CONNECTIONS_REFUSED_INVALID_PARAMETERS = 0x000C
|
||||
ALL_CONNECTIONS_PENDING_NO_FURTHER_INFORMATION_AVAILABLE = 0x000D
|
||||
ALL_CONNECTIONS_PENDING_AUTHENTICATION_PENDING = 0x000E
|
||||
ALL_CONNECTIONS_PENDING_AUTHORIZATION_PENDING = 0x000F
|
||||
|
||||
mtu: int = dataclasses.field(metadata=hci.metadata(2))
|
||||
mps: int = dataclasses.field(metadata=hci.metadata(2))
|
||||
initial_credits: int = dataclasses.field(metadata=hci.metadata(2))
|
||||
result: int = dataclasses.field(metadata=Result.type_metadata(2))
|
||||
destination_cid: Sequence[int] = dataclasses.field(
|
||||
metadata=L2CAP_Credit_Based_Connection_Request.CID_METADATA
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@L2CAP_Control_Frame.subclass
|
||||
@dataclasses.dataclass
|
||||
class L2CAP_Credit_Based_Reconfigure_Request(L2CAP_Control_Frame):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part A - 4.27 L2CAP_CREDIT_BASED_RECONFIGURE_REQ (0x19).
|
||||
'''
|
||||
|
||||
mtu: int = dataclasses.field(metadata=hci.metadata(2))
|
||||
mps: int = dataclasses.field(metadata=hci.metadata(2))
|
||||
destination_cid: Sequence[int] = dataclasses.field(
|
||||
metadata=L2CAP_Credit_Based_Connection_Request.CID_METADATA
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@L2CAP_Control_Frame.subclass
|
||||
@dataclasses.dataclass
|
||||
class L2CAP_Credit_Based_Reconfigure_Response(L2CAP_Control_Frame):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part A - 4.28 L2CAP_CREDIT_BASED_RECONFIGURE_RSP (0x1A).
|
||||
'''
|
||||
|
||||
class Result(hci.SpecableEnum):
|
||||
RECONFIGURATION_SUCCESSFUL = 0x0000
|
||||
RECONFIGURATION_FAILED_REDUCTION_IN_SIZE_OF_MTU_NOT_ALLOWED = 0x0001
|
||||
RECONFIGURATION_FAILED_REDUCTION_IN_SIZE_OF_MPS_NOT_ALLOWED_FOR_MORE_THAN_ONE_CHANNEL_AT_A_TIME = (
|
||||
0x0002
|
||||
)
|
||||
RECONFIGURATION_FAILED_ONE_OR_MORE_DESTINATION_CIDS_INVALID = 0x0003
|
||||
RECONFIGURATION_FAILED_OTHER_UNACCEPTABLE_PARAMETERS = 0x0004
|
||||
|
||||
result: int = dataclasses.field(metadata=Result.type_metadata(2))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class ClassicChannel(utils.EventEmitter):
|
||||
class State(enum.IntEnum):
|
||||
@@ -1437,16 +1531,6 @@ class ChannelManager:
|
||||
if cid in self.fixed_channels:
|
||||
del self.fixed_channels[cid]
|
||||
|
||||
@utils.deprecated("Please use create_classic_server")
|
||||
def register_server(
|
||||
self,
|
||||
psm: int,
|
||||
server: Callable[[ClassicChannel], Any],
|
||||
) -> int:
|
||||
return self.create_classic_server(
|
||||
handler=server, spec=ClassicChannelSpec(psm=psm)
|
||||
).psm
|
||||
|
||||
def create_classic_server(
|
||||
self,
|
||||
spec: ClassicChannelSpec,
|
||||
@@ -1483,22 +1567,6 @@ class ChannelManager:
|
||||
|
||||
return self.servers[spec.psm]
|
||||
|
||||
@utils.deprecated("Please use create_le_credit_based_server()")
|
||||
def register_le_coc_server(
|
||||
self,
|
||||
psm: int,
|
||||
server: Callable[[LeCreditBasedChannel], Any],
|
||||
max_credits: int,
|
||||
mtu: int,
|
||||
mps: int,
|
||||
) -> int:
|
||||
return self.create_le_credit_based_server(
|
||||
spec=LeCreditBasedChannelSpec(
|
||||
psm=None if psm == 0 else psm, mtu=mtu, mps=mps, max_credits=max_credits
|
||||
),
|
||||
handler=server,
|
||||
).psm
|
||||
|
||||
def create_le_credit_based_server(
|
||||
self,
|
||||
spec: LeCreditBasedChannelSpec,
|
||||
@@ -1600,8 +1668,8 @@ class ChannelManager:
|
||||
if handler:
|
||||
try:
|
||||
handler(connection, cid, control_frame)
|
||||
except Exception as error:
|
||||
logger.warning(f'{color("!!! Exception in handler:", "red")} {error}')
|
||||
except Exception:
|
||||
logger.exception(color("!!! Exception in handler:", "red"))
|
||||
self.send_control_frame(
|
||||
connection,
|
||||
cid,
|
||||
@@ -1611,7 +1679,7 @@ class ChannelManager:
|
||||
data=b'',
|
||||
),
|
||||
)
|
||||
raise error
|
||||
raise
|
||||
else:
|
||||
logger.error(color('Channel Manager command not handled???', 'red'))
|
||||
self.send_control_frame(
|
||||
@@ -2051,17 +2119,6 @@ class ChannelManager:
|
||||
if channel.source_cid in connection_channels:
|
||||
del connection_channels[channel.source_cid]
|
||||
|
||||
@utils.deprecated("Please use create_le_credit_based_channel()")
|
||||
async def open_le_coc(
|
||||
self, connection: Connection, psm: int, max_credits: int, mtu: int, mps: int
|
||||
) -> LeCreditBasedChannel:
|
||||
return await self.create_le_credit_based_channel(
|
||||
connection=connection,
|
||||
spec=LeCreditBasedChannelSpec(
|
||||
psm=psm, max_credits=max_credits, mtu=mtu, mps=mps
|
||||
),
|
||||
)
|
||||
|
||||
async def create_le_credit_based_channel(
|
||||
self,
|
||||
connection: Connection,
|
||||
@@ -2097,8 +2154,8 @@ class ChannelManager:
|
||||
# Connect
|
||||
try:
|
||||
await channel.connect()
|
||||
except Exception as error:
|
||||
logger.warning(f'connection failed: {error}')
|
||||
except Exception:
|
||||
logger.exception('connection failed')
|
||||
del connection_channels[source_cid]
|
||||
raise
|
||||
|
||||
@@ -2108,12 +2165,6 @@ class ChannelManager:
|
||||
|
||||
return channel
|
||||
|
||||
@utils.deprecated("Please use create_classic_channel()")
|
||||
async def connect(self, connection: Connection, psm: int) -> ClassicChannel:
|
||||
return await self.create_classic_channel(
|
||||
connection=connection, spec=ClassicChannelSpec(psm=psm)
|
||||
)
|
||||
|
||||
async def create_classic_channel(
|
||||
self, connection: Connection, spec: ClassicChannelSpec
|
||||
) -> ClassicChannel:
|
||||
@@ -2150,20 +2201,3 @@ class ChannelManager:
|
||||
raise e
|
||||
|
||||
return channel
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Deprecated Classes
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class Channel(ClassicChannel):
|
||||
@utils.deprecated("Please use ClassicChannel")
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class LeConnectionOrientedChannel(LeCreditBasedChannel):
|
||||
@utils.deprecated("Please use LeCreditBasedChannel")
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -12,26 +12,25 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import asyncio
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import logging
|
||||
import asyncio
|
||||
|
||||
from bumble import core
|
||||
from bumble.hci import (
|
||||
Address,
|
||||
Role,
|
||||
HCI_SUCCESS,
|
||||
HCI_CONNECTION_ACCEPT_TIMEOUT_ERROR,
|
||||
HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR,
|
||||
HCI_PAGE_TIMEOUT_ERROR,
|
||||
HCI_Connection_Complete_Event,
|
||||
)
|
||||
from bumble import controller
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from bumble import controller, core
|
||||
from bumble.hci import (
|
||||
HCI_CONNECTION_ACCEPT_TIMEOUT_ERROR,
|
||||
HCI_PAGE_TIMEOUT_ERROR,
|
||||
HCI_SUCCESS,
|
||||
HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR,
|
||||
Address,
|
||||
HCI_Connection_Complete_Event,
|
||||
Role,
|
||||
)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -159,29 +158,29 @@ class LocalLink:
|
||||
asyncio.get_running_loop().call_soon(self.on_connection_complete)
|
||||
|
||||
def on_disconnection_complete(
|
||||
self, central_address, peripheral_address, disconnect_command
|
||||
self, initiating_address, target_address, disconnect_command
|
||||
):
|
||||
# Find the controller that initiated the disconnection
|
||||
if not (central_controller := self.find_controller(central_address)):
|
||||
if not (initiating_controller := self.find_controller(initiating_address)):
|
||||
logger.warning('!!! Initiating controller not found')
|
||||
return
|
||||
|
||||
# Disconnect from the first controller with a matching address
|
||||
if peripheral_controller := self.find_controller(peripheral_address):
|
||||
peripheral_controller.on_link_central_disconnected(
|
||||
central_address, disconnect_command.reason
|
||||
if target_controller := self.find_controller(target_address):
|
||||
target_controller.on_link_disconnected(
|
||||
initiating_address, disconnect_command.reason
|
||||
)
|
||||
|
||||
central_controller.on_link_peripheral_disconnection_complete(
|
||||
initiating_controller.on_link_disconnection_complete(
|
||||
disconnect_command, HCI_SUCCESS
|
||||
)
|
||||
|
||||
def disconnect(self, central_address, peripheral_address, disconnect_command):
|
||||
def disconnect(self, initiating_address, target_address, disconnect_command):
|
||||
logger.debug(
|
||||
f'$$$ DISCONNECTION {central_address} -> '
|
||||
f'{peripheral_address}: reason = {disconnect_command.reason}'
|
||||
f'$$$ DISCONNECTION {initiating_address} -> '
|
||||
f'{target_address}: reason = {disconnect_command.reason}'
|
||||
)
|
||||
args = [central_address, peripheral_address, disconnect_command]
|
||||
args = [initiating_address, target_address, disconnect_command]
|
||||
asyncio.get_running_loop().call_soon(self.on_disconnection_complete, *args)
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
|
||||
65
bumble/logging.py
Normal file
65
bumble/logging.py
Normal file
@@ -0,0 +1,65 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import functools
|
||||
import logging
|
||||
import os
|
||||
|
||||
from bumble import colors
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class ColorFormatter(logging.Formatter):
|
||||
_colorizers = {
|
||||
logging.DEBUG: functools.partial(colors.color, fg="white"),
|
||||
logging.INFO: functools.partial(colors.color, fg="green"),
|
||||
logging.WARNING: functools.partial(colors.color, fg="yellow"),
|
||||
logging.ERROR: functools.partial(colors.color, fg="red"),
|
||||
logging.CRITICAL: functools.partial(colors.color, fg="black", bg="red"),
|
||||
}
|
||||
|
||||
_formatters = {
|
||||
level: logging.Formatter(
|
||||
fmt=colorizer("{asctime}.{msecs:03.0f} {levelname:.1} {name}: ")
|
||||
+ "{message}",
|
||||
datefmt="%H:%M:%S",
|
||||
style="{",
|
||||
)
|
||||
for level, colorizer in _colorizers.items()
|
||||
}
|
||||
|
||||
def format(self, record: logging.LogRecord) -> str:
|
||||
return self._formatters[record.levelno].format(record)
|
||||
|
||||
|
||||
def setup_basic_logging(default_level: str = "INFO") -> None:
|
||||
"""
|
||||
Set up basic logging with logging.basicConfig, configured with a simple formatter
|
||||
that prints out the date and log level in color.
|
||||
If the BUMBLE_LOGLEVEL environment variable is set to the name of a log level, it
|
||||
is used. Otherwise the default_level argument is used.
|
||||
|
||||
Args:
|
||||
default_level: default logging level
|
||||
|
||||
"""
|
||||
handler = logging.StreamHandler()
|
||||
handler.setFormatter(ColorFormatter())
|
||||
logging.basicConfig(
|
||||
level=os.environ.get("BUMBLE_LOGLEVEL", default_level).upper(),
|
||||
handlers=[handler],
|
||||
)
|
||||
@@ -16,27 +16,28 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
from dataclasses import dataclass
|
||||
import secrets
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from bumble import hci
|
||||
from bumble.core import AdvertisingData, LeRole
|
||||
from bumble.smp import (
|
||||
SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
|
||||
SMP_KEYBOARD_ONLY_IO_CAPABILITY,
|
||||
SMP_DISPLAY_ONLY_IO_CAPABILITY,
|
||||
SMP_DISPLAY_YES_NO_IO_CAPABILITY,
|
||||
SMP_KEYBOARD_DISPLAY_IO_CAPABILITY,
|
||||
SMP_ENC_KEY_DISTRIBUTION_FLAG,
|
||||
SMP_ID_KEY_DISTRIBUTION_FLAG,
|
||||
SMP_SIGN_KEY_DISTRIBUTION_FLAG,
|
||||
SMP_KEYBOARD_DISPLAY_IO_CAPABILITY,
|
||||
SMP_KEYBOARD_ONLY_IO_CAPABILITY,
|
||||
SMP_LINK_KEY_DISTRIBUTION_FLAG,
|
||||
SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
|
||||
SMP_SIGN_KEY_DISTRIBUTION_FLAG,
|
||||
OobContext,
|
||||
OobLegacyContext,
|
||||
OobSharedData,
|
||||
)
|
||||
from bumble.core import AdvertisingData, LeRole
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -19,21 +19,22 @@ This module implement the Pandora Bluetooth test APIs for the Bumble stack.
|
||||
|
||||
__version__ = "0.0.1"
|
||||
|
||||
from typing import Callable, List, Optional
|
||||
|
||||
import grpc
|
||||
import grpc.aio
|
||||
|
||||
from bumble.pandora.config import Config
|
||||
from bumble.pandora.device import PandoraDevice
|
||||
from bumble.pandora.host import HostService
|
||||
from bumble.pandora.l2cap import L2CAPService
|
||||
from bumble.pandora.security import SecurityService, SecurityStorageService
|
||||
from pandora.host_grpc_aio import add_HostServicer_to_server
|
||||
from pandora.l2cap_grpc_aio import add_L2CAPServicer_to_server
|
||||
from pandora.security_grpc_aio import (
|
||||
add_SecurityServicer_to_server,
|
||||
add_SecurityStorageServicer_to_server,
|
||||
)
|
||||
from typing import Callable, List, Optional
|
||||
|
||||
from bumble.pandora.config import Config
|
||||
from bumble.pandora.device import PandoraDevice
|
||||
from bumble.pandora.host import HostService
|
||||
from bumble.pandora.l2cap import L2CAPService
|
||||
from bumble.pandora.security import SecurityService, SecurityStorageService
|
||||
|
||||
# public symbols
|
||||
__all__ = [
|
||||
@@ -49,7 +50,7 @@ _SERVICERS_HOOKS: list[Callable[[PandoraDevice, Config, grpc.aio.Server], None]]
|
||||
|
||||
|
||||
def register_servicer_hook(
|
||||
hook: Callable[[PandoraDevice, Config, grpc.aio.Server], None]
|
||||
hook: Callable[[PandoraDevice, Config, grpc.aio.Server], None],
|
||||
) -> None:
|
||||
_SERVICERS_HOOKS.append(hook)
|
||||
|
||||
|
||||
@@ -13,10 +13,12 @@
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
from bumble.pairing import PairingConfig, PairingDelegate
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from bumble.pairing import PairingConfig, PairingDelegate
|
||||
|
||||
|
||||
@dataclass
|
||||
class Config:
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
"""Generic & dependency free Bumble (reference) device."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Optional
|
||||
|
||||
from bumble import transport
|
||||
from bumble.core import (
|
||||
BT_GENERIC_AUDIO_SERVICE,
|
||||
@@ -32,8 +35,6 @@ from bumble.sdp import (
|
||||
DataElement,
|
||||
ServiceAttribute,
|
||||
)
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
# Default rootcanal HCI TCP address
|
||||
ROOTCANAL_HCI_ADDRESS = "localhost:6402"
|
||||
|
||||
@@ -13,51 +13,23 @@
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import bumble.device
|
||||
import grpc
|
||||
import grpc.aio
|
||||
import logging
|
||||
import struct
|
||||
from typing import AsyncGenerator, Optional, cast
|
||||
|
||||
import bumble.utils
|
||||
from bumble.pandora import utils
|
||||
from bumble.pandora.config import Config
|
||||
from bumble.core import (
|
||||
PhysicalTransport,
|
||||
UUID,
|
||||
AdvertisingData,
|
||||
Appearance,
|
||||
ConnectionError,
|
||||
)
|
||||
from bumble.device import (
|
||||
DEVICE_DEFAULT_SCAN_INTERVAL,
|
||||
DEVICE_DEFAULT_SCAN_WINDOW,
|
||||
Advertisement,
|
||||
AdvertisingParameters,
|
||||
AdvertisingEventProperties,
|
||||
AdvertisingType,
|
||||
Device,
|
||||
)
|
||||
from bumble.gatt import Service
|
||||
from bumble.hci import (
|
||||
HCI_CONNECTION_ALREADY_EXISTS_ERROR,
|
||||
HCI_PAGE_TIMEOUT_ERROR,
|
||||
HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR,
|
||||
Address,
|
||||
Phy,
|
||||
Role,
|
||||
OwnAddressType,
|
||||
)
|
||||
import grpc
|
||||
import grpc.aio
|
||||
from google.protobuf import any_pb2 # pytype: disable=pyi-error
|
||||
from google.protobuf import empty_pb2 # pytype: disable=pyi-error
|
||||
from pandora.host_grpc_aio import HostServicer
|
||||
from pandora import host_pb2
|
||||
from pandora.host_grpc_aio import HostServicer
|
||||
from pandora.host_pb2 import (
|
||||
DISCOVERABLE_GENERAL,
|
||||
DISCOVERABLE_LIMITED,
|
||||
NOT_CONNECTABLE,
|
||||
NOT_DISCOVERABLE,
|
||||
DISCOVERABLE_LIMITED,
|
||||
DISCOVERABLE_GENERAL,
|
||||
PRIMARY_1M,
|
||||
PRIMARY_CODED,
|
||||
SECONDARY_1M,
|
||||
@@ -85,7 +57,37 @@ from pandora.host_pb2 import (
|
||||
WaitConnectionResponse,
|
||||
WaitDisconnectionRequest,
|
||||
)
|
||||
from typing import AsyncGenerator, Optional, cast
|
||||
|
||||
import bumble.device
|
||||
import bumble.utils
|
||||
from bumble.core import (
|
||||
UUID,
|
||||
AdvertisingData,
|
||||
Appearance,
|
||||
ConnectionError,
|
||||
PhysicalTransport,
|
||||
)
|
||||
from bumble.device import (
|
||||
DEVICE_DEFAULT_SCAN_INTERVAL,
|
||||
DEVICE_DEFAULT_SCAN_WINDOW,
|
||||
Advertisement,
|
||||
AdvertisingEventProperties,
|
||||
AdvertisingParameters,
|
||||
AdvertisingType,
|
||||
Device,
|
||||
)
|
||||
from bumble.gatt import Service
|
||||
from bumble.hci import (
|
||||
HCI_CONNECTION_ALREADY_EXISTS_ERROR,
|
||||
HCI_PAGE_TIMEOUT_ERROR,
|
||||
HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR,
|
||||
Address,
|
||||
OwnAddressType,
|
||||
Phy,
|
||||
Role,
|
||||
)
|
||||
from bumble.pandora import utils
|
||||
from bumble.pandora.config import Config
|
||||
|
||||
PRIMARY_PHY_MAP: dict[int, PrimaryPhy] = {
|
||||
# Default value reported by Bumble for legacy Advertising reports.
|
||||
|
||||
@@ -12,31 +12,21 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import grpc
|
||||
import json
|
||||
import logging
|
||||
from asyncio import Future
|
||||
from asyncio import Queue as AsyncQueue
|
||||
from dataclasses import dataclass
|
||||
from typing import AsyncGenerator, Optional, Union
|
||||
|
||||
from asyncio import Queue as AsyncQueue, Future
|
||||
|
||||
from bumble.pandora import utils
|
||||
from bumble.pandora.config import Config
|
||||
from bumble.core import OutOfResourcesError, InvalidArgumentError
|
||||
from bumble.device import Device
|
||||
from bumble.l2cap import (
|
||||
ClassicChannel,
|
||||
ClassicChannelServer,
|
||||
ClassicChannelSpec,
|
||||
LeCreditBasedChannel,
|
||||
LeCreditBasedChannelServer,
|
||||
LeCreditBasedChannelSpec,
|
||||
)
|
||||
import grpc
|
||||
from google.protobuf import any_pb2, empty_pb2 # pytype: disable=pyi-error
|
||||
from pandora.l2cap_grpc_aio import L2CAPServicer # pytype: disable=pyi-error
|
||||
from pandora.l2cap_pb2 import ( # pytype: disable=pyi-error
|
||||
COMMAND_NOT_UNDERSTOOD,
|
||||
INVALID_CID_IN_REQUEST,
|
||||
Channel as PandoraChannel,
|
||||
from pandora.l2cap_pb2 import COMMAND_NOT_UNDERSTOOD, INVALID_CID_IN_REQUEST
|
||||
from pandora.l2cap_pb2 import Channel as PandoraChannel # pytype: disable=pyi-error
|
||||
from pandora.l2cap_pb2 import (
|
||||
ConnectRequest,
|
||||
ConnectResponse,
|
||||
CreditBasedChannelRequest,
|
||||
@@ -51,8 +41,19 @@ from pandora.l2cap_pb2 import ( # pytype: disable=pyi-error
|
||||
WaitDisconnectionRequest,
|
||||
WaitDisconnectionResponse,
|
||||
)
|
||||
from typing import AsyncGenerator, Optional, Union
|
||||
from dataclasses import dataclass
|
||||
|
||||
from bumble.core import InvalidArgumentError, OutOfResourcesError
|
||||
from bumble.device import Device
|
||||
from bumble.l2cap import (
|
||||
ClassicChannel,
|
||||
ClassicChannelServer,
|
||||
ClassicChannelSpec,
|
||||
LeCreditBasedChannel,
|
||||
LeCreditBasedChannelServer,
|
||||
LeCreditBasedChannelSpec,
|
||||
)
|
||||
from bumble.pandora import utils
|
||||
from bumble.pandora.config import Config
|
||||
|
||||
L2capChannel = Union[ClassicChannel, LeCreditBasedChannel]
|
||||
|
||||
|
||||
@@ -13,24 +13,14 @@
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
from collections.abc import Awaitable
|
||||
import grpc
|
||||
import logging
|
||||
from collections.abc import Awaitable
|
||||
from typing import Any, AsyncGenerator, AsyncIterator, Callable, Optional, Union
|
||||
|
||||
from bumble.pandora import utils
|
||||
from bumble.pandora.config import Config
|
||||
from bumble import hci
|
||||
from bumble.core import (
|
||||
PhysicalTransport,
|
||||
ProtocolError,
|
||||
InvalidArgumentError,
|
||||
)
|
||||
import bumble.utils
|
||||
from bumble.device import Connection as BumbleConnection, Device
|
||||
from bumble.hci import HCI_Error, Role
|
||||
from bumble.pairing import PairingConfig, PairingDelegate as BasePairingDelegate
|
||||
import grpc
|
||||
from google.protobuf import any_pb2 # pytype: disable=pyi-error
|
||||
from google.protobuf import empty_pb2 # pytype: disable=pyi-error
|
||||
from google.protobuf import wrappers_pb2 # pytype: disable=pyi-error
|
||||
@@ -57,7 +47,17 @@ from pandora.security_pb2 import (
|
||||
WaitSecurityRequest,
|
||||
WaitSecurityResponse,
|
||||
)
|
||||
from typing import Any, AsyncGenerator, AsyncIterator, Callable, Optional, Union
|
||||
|
||||
import bumble.utils
|
||||
from bumble import hci
|
||||
from bumble.core import InvalidArgumentError, PhysicalTransport, ProtocolError
|
||||
from bumble.device import Connection as BumbleConnection
|
||||
from bumble.device import Device
|
||||
from bumble.hci import HCI_Error, Role
|
||||
from bumble.pairing import PairingConfig
|
||||
from bumble.pairing import PairingDelegate as BasePairingDelegate
|
||||
from bumble.pandora import utils
|
||||
from bumble.pandora.config import Config
|
||||
|
||||
|
||||
class PairingDelegate(BasePairingDelegate):
|
||||
|
||||
@@ -13,16 +13,18 @@
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import functools
|
||||
import grpc
|
||||
import inspect
|
||||
import logging
|
||||
from typing import Any, Generator, MutableMapping, Optional
|
||||
|
||||
import grpc
|
||||
from google.protobuf.message import Message # pytype: disable=pyi-error
|
||||
|
||||
from bumble.device import Device
|
||||
from bumble.hci import Address, AddressType
|
||||
from google.protobuf.message import Message # pytype: disable=pyi-error
|
||||
from typing import Any, Generator, MutableMapping, Optional
|
||||
|
||||
ADDRESS_TYPES: dict[str, AddressType] = {
|
||||
"public": Address.PUBLIC_DEVICE_ADDRESS,
|
||||
|
||||
@@ -18,26 +18,27 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import struct
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from bumble.device import Connection
|
||||
from bumble import utils
|
||||
from bumble.att import ATT_Error
|
||||
from bumble.device import Connection
|
||||
from bumble.gatt import (
|
||||
GATT_AUDIO_INPUT_CONTROL_POINT_CHARACTERISTIC,
|
||||
GATT_AUDIO_INPUT_CONTROL_SERVICE,
|
||||
GATT_AUDIO_INPUT_DESCRIPTION_CHARACTERISTIC,
|
||||
GATT_AUDIO_INPUT_STATE_CHARACTERISTIC,
|
||||
GATT_AUDIO_INPUT_STATUS_CHARACTERISTIC,
|
||||
GATT_AUDIO_INPUT_TYPE_CHARACTERISTIC,
|
||||
GATT_GAIN_SETTINGS_ATTRIBUTE_CHARACTERISTIC,
|
||||
Attribute,
|
||||
Characteristic,
|
||||
TemplateService,
|
||||
CharacteristicValue,
|
||||
GATT_AUDIO_INPUT_CONTROL_SERVICE,
|
||||
GATT_AUDIO_INPUT_STATE_CHARACTERISTIC,
|
||||
GATT_GAIN_SETTINGS_ATTRIBUTE_CHARACTERISTIC,
|
||||
GATT_AUDIO_INPUT_TYPE_CHARACTERISTIC,
|
||||
GATT_AUDIO_INPUT_STATUS_CHARACTERISTIC,
|
||||
GATT_AUDIO_INPUT_CONTROL_POINT_CHARACTERISTIC,
|
||||
GATT_AUDIO_INPUT_DESCRIPTION_CHARACTERISTIC,
|
||||
TemplateService,
|
||||
)
|
||||
from bumble.gatt_adapters import (
|
||||
CharacteristicProxy,
|
||||
@@ -48,7 +49,6 @@ from bumble.gatt_adapters import (
|
||||
UTF8CharacteristicProxyAdapter,
|
||||
)
|
||||
from bumble.gatt_client import ProfileServiceProxy, ServiceProxy
|
||||
from bumble import utils
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
|
||||
403
bumble/profiles/ams.py
Normal file
403
bumble/profiles/ams.py
Normal file
@@ -0,0 +1,403 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
Apple Media Service (AMS).
|
||||
"""
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import enum
|
||||
import logging
|
||||
from typing import Iterable, Optional, Union
|
||||
|
||||
from bumble import utils
|
||||
from bumble.device import Peer
|
||||
from bumble.gatt import (
|
||||
GATT_AMS_ENTITY_ATTRIBUTE_CHARACTERISTIC,
|
||||
GATT_AMS_ENTITY_UPDATE_CHARACTERISTIC,
|
||||
GATT_AMS_REMOTE_COMMAND_CHARACTERISTIC,
|
||||
GATT_AMS_SERVICE,
|
||||
Characteristic,
|
||||
TemplateService,
|
||||
)
|
||||
from bumble.gatt_client import CharacteristicProxy, ProfileServiceProxy, ServiceProxy
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Protocol
|
||||
# -----------------------------------------------------------------------------
|
||||
class RemoteCommandId(utils.OpenIntEnum):
|
||||
PLAY = 0
|
||||
PAUSE = 1
|
||||
TOGGLE_PLAY_PAUSE = 2
|
||||
NEXT_TRACK = 3
|
||||
PREVIOUS_TRACK = 4
|
||||
VOLUME_UP = 5
|
||||
VOLUME_DOWN = 6
|
||||
ADVANCE_REPEAT_MODE = 7
|
||||
ADVANCE_SHUFFLE_MODE = 8
|
||||
SKIP_FORWARD = 9
|
||||
SKIP_BACKWARD = 10
|
||||
LIKE_TRACK = 11
|
||||
DISLIKE_TRACK = 12
|
||||
BOOKMARK_TRACK = 13
|
||||
|
||||
|
||||
class EntityId(utils.OpenIntEnum):
|
||||
PLAYER = 0
|
||||
QUEUE = 1
|
||||
TRACK = 2
|
||||
|
||||
|
||||
class ActionId(utils.OpenIntEnum):
|
||||
POSITIVE = 0
|
||||
NEGATIVE = 1
|
||||
|
||||
|
||||
class EntityUpdateFlags(enum.IntFlag):
|
||||
TRUNCATED = 1
|
||||
|
||||
|
||||
class PlayerAttributeId(utils.OpenIntEnum):
|
||||
NAME = 0
|
||||
PLAYBACK_INFO = 1
|
||||
VOLUME = 2
|
||||
|
||||
|
||||
class QueueAttributeId(utils.OpenIntEnum):
|
||||
INDEX = 0
|
||||
COUNT = 1
|
||||
SHUFFLE_MODE = 2
|
||||
REPEAT_MODE = 3
|
||||
|
||||
|
||||
class ShuffleMode(utils.OpenIntEnum):
|
||||
OFF = 0
|
||||
ONE = 1
|
||||
ALL = 2
|
||||
|
||||
|
||||
class RepeatMode(utils.OpenIntEnum):
|
||||
OFF = 0
|
||||
ONE = 1
|
||||
ALL = 2
|
||||
|
||||
|
||||
class TrackAttributeId(utils.OpenIntEnum):
|
||||
ARTIST = 0
|
||||
ALBUM = 1
|
||||
TITLE = 2
|
||||
DURATION = 3
|
||||
|
||||
|
||||
class PlaybackState(utils.OpenIntEnum):
|
||||
PAUSED = 0
|
||||
PLAYING = 1
|
||||
REWINDING = 2
|
||||
FAST_FORWARDING = 3
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class PlaybackInfo:
|
||||
playback_state: PlaybackState = PlaybackState.PAUSED
|
||||
playback_rate: float = 1.0
|
||||
elapsed_time: float = 0.0
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# GATT Server-side
|
||||
# -----------------------------------------------------------------------------
|
||||
class Ams(TemplateService):
|
||||
UUID = GATT_AMS_SERVICE
|
||||
|
||||
remote_command_characteristic: Characteristic
|
||||
entity_update_characteristic: Characteristic
|
||||
entity_attribute_characteristic: Characteristic
|
||||
|
||||
def __init__(self) -> None:
|
||||
# TODO not the final implementation
|
||||
self.remote_command_characteristic = Characteristic(
|
||||
GATT_AMS_REMOTE_COMMAND_CHARACTERISTIC,
|
||||
Characteristic.Properties.NOTIFY
|
||||
| Characteristic.Properties.WRITE_WITHOUT_RESPONSE,
|
||||
Characteristic.Permissions.WRITEABLE,
|
||||
)
|
||||
|
||||
# TODO not the final implementation
|
||||
self.entity_update_characteristic = Characteristic(
|
||||
GATT_AMS_ENTITY_UPDATE_CHARACTERISTIC,
|
||||
Characteristic.Properties.NOTIFY | Characteristic.Properties.WRITE,
|
||||
Characteristic.Permissions.WRITEABLE,
|
||||
)
|
||||
|
||||
# TODO not the final implementation
|
||||
self.entity_attribute_characteristic = Characteristic(
|
||||
GATT_AMS_ENTITY_ATTRIBUTE_CHARACTERISTIC,
|
||||
Characteristic.Properties.READ
|
||||
| Characteristic.Properties.WRITE_WITHOUT_RESPONSE,
|
||||
Characteristic.Permissions.WRITEABLE | Characteristic.Permissions.READABLE,
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
[
|
||||
self.remote_command_characteristic,
|
||||
self.entity_update_characteristic,
|
||||
self.entity_attribute_characteristic,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# GATT Client-side
|
||||
# -----------------------------------------------------------------------------
|
||||
class AmsProxy(ProfileServiceProxy):
|
||||
SERVICE_CLASS = Ams
|
||||
|
||||
# NOTE: these don't use adapters, because the format for write and notifications
|
||||
# are different.
|
||||
remote_command: CharacteristicProxy[bytes]
|
||||
entity_update: CharacteristicProxy[bytes]
|
||||
entity_attribute: CharacteristicProxy[bytes]
|
||||
|
||||
def __init__(self, service_proxy: ServiceProxy):
|
||||
self.remote_command = service_proxy.get_required_characteristic_by_uuid(
|
||||
GATT_AMS_REMOTE_COMMAND_CHARACTERISTIC
|
||||
)
|
||||
|
||||
self.entity_update = service_proxy.get_required_characteristic_by_uuid(
|
||||
GATT_AMS_ENTITY_UPDATE_CHARACTERISTIC
|
||||
)
|
||||
|
||||
self.entity_attribute = service_proxy.get_required_characteristic_by_uuid(
|
||||
GATT_AMS_ENTITY_ATTRIBUTE_CHARACTERISTIC
|
||||
)
|
||||
|
||||
|
||||
class AmsClient(utils.EventEmitter):
|
||||
EVENT_SUPPORTED_COMMANDS = "supported_commands"
|
||||
EVENT_PLAYER_NAME = "player_name"
|
||||
EVENT_PLAYER_PLAYBACK_INFO = "player_playback_info"
|
||||
EVENT_PLAYER_VOLUME = "player_volume"
|
||||
EVENT_QUEUE_COUNT = "queue_count"
|
||||
EVENT_QUEUE_INDEX = "queue_index"
|
||||
EVENT_QUEUE_SHUFFLE_MODE = "queue_shuffle_mode"
|
||||
EVENT_QUEUE_REPEAT_MODE = "queue_repeat_mode"
|
||||
EVENT_TRACK_ARTIST = "track_artist"
|
||||
EVENT_TRACK_ALBUM = "track_album"
|
||||
EVENT_TRACK_TITLE = "track_title"
|
||||
EVENT_TRACK_DURATION = "track_duration"
|
||||
|
||||
supported_commands: set[RemoteCommandId]
|
||||
player_name: str = ""
|
||||
player_playback_info: PlaybackInfo = PlaybackInfo(PlaybackState.PAUSED, 0.0, 0.0)
|
||||
player_volume: float = 1.0
|
||||
queue_count: int = 0
|
||||
queue_index: int = 0
|
||||
queue_shuffle_mode: ShuffleMode = ShuffleMode.OFF
|
||||
queue_repeat_mode: RepeatMode = RepeatMode.OFF
|
||||
track_artist: str = ""
|
||||
track_album: str = ""
|
||||
track_title: str = ""
|
||||
track_duration: float = 0.0
|
||||
|
||||
def __init__(self, ams_proxy: AmsProxy) -> None:
|
||||
super().__init__()
|
||||
self._ams_proxy = ams_proxy
|
||||
self._started = False
|
||||
self._read_attribute_semaphore = asyncio.Semaphore()
|
||||
self.supported_commands = set()
|
||||
|
||||
@classmethod
|
||||
async def for_peer(cls, peer: Peer) -> Optional[AmsClient]:
|
||||
ams_proxy = await peer.discover_service_and_create_proxy(AmsProxy)
|
||||
if ams_proxy is None:
|
||||
return None
|
||||
return cls(ams_proxy)
|
||||
|
||||
async def start(self) -> None:
|
||||
logger.debug("subscribing to remote command characteristic")
|
||||
await self._ams_proxy.remote_command.subscribe(
|
||||
self._on_remote_command_notification
|
||||
)
|
||||
|
||||
logger.debug("subscribing to entity update characteristic")
|
||||
await self._ams_proxy.entity_update.subscribe(
|
||||
lambda data: utils.AsyncRunner.spawn(
|
||||
self._on_entity_update_notification(data)
|
||||
)
|
||||
)
|
||||
|
||||
self._started = True
|
||||
|
||||
async def stop(self) -> None:
|
||||
await self._ams_proxy.remote_command.unsubscribe(
|
||||
self._on_remote_command_notification
|
||||
)
|
||||
await self._ams_proxy.entity_update.unsubscribe(
|
||||
self._on_entity_update_notification
|
||||
)
|
||||
self._started = False
|
||||
|
||||
async def observe(
|
||||
self,
|
||||
entity: EntityId,
|
||||
attributes: Iterable[
|
||||
Union[PlayerAttributeId, QueueAttributeId, TrackAttributeId]
|
||||
],
|
||||
) -> None:
|
||||
await self._ams_proxy.entity_update.write_value(
|
||||
bytes([entity] + list(attributes)), with_response=True
|
||||
)
|
||||
|
||||
async def command(self, command: RemoteCommandId) -> None:
|
||||
await self._ams_proxy.remote_command.write_value(
|
||||
bytes([command]), with_response=True
|
||||
)
|
||||
|
||||
async def play(self) -> None:
|
||||
await self.command(RemoteCommandId.PLAY)
|
||||
|
||||
async def pause(self) -> None:
|
||||
await self.command(RemoteCommandId.PAUSE)
|
||||
|
||||
async def toggle_play_pause(self) -> None:
|
||||
await self.command(RemoteCommandId.TOGGLE_PLAY_PAUSE)
|
||||
|
||||
async def next_track(self) -> None:
|
||||
await self.command(RemoteCommandId.NEXT_TRACK)
|
||||
|
||||
async def previous_track(self) -> None:
|
||||
await self.command(RemoteCommandId.PREVIOUS_TRACK)
|
||||
|
||||
async def volume_up(self) -> None:
|
||||
await self.command(RemoteCommandId.VOLUME_UP)
|
||||
|
||||
async def volume_down(self) -> None:
|
||||
await self.command(RemoteCommandId.VOLUME_DOWN)
|
||||
|
||||
async def advance_repeat_mode(self) -> None:
|
||||
await self.command(RemoteCommandId.ADVANCE_REPEAT_MODE)
|
||||
|
||||
async def advance_shuffle_mode(self) -> None:
|
||||
await self.command(RemoteCommandId.ADVANCE_SHUFFLE_MODE)
|
||||
|
||||
async def skip_forward(self) -> None:
|
||||
await self.command(RemoteCommandId.SKIP_FORWARD)
|
||||
|
||||
async def skip_backward(self) -> None:
|
||||
await self.command(RemoteCommandId.SKIP_BACKWARD)
|
||||
|
||||
async def like_track(self) -> None:
|
||||
await self.command(RemoteCommandId.LIKE_TRACK)
|
||||
|
||||
async def dislike_track(self) -> None:
|
||||
await self.command(RemoteCommandId.DISLIKE_TRACK)
|
||||
|
||||
async def bookmark_track(self) -> None:
|
||||
await self.command(RemoteCommandId.BOOKMARK_TRACK)
|
||||
|
||||
def _on_remote_command_notification(self, data: bytes) -> None:
|
||||
supported_commands = [RemoteCommandId(command) for command in data]
|
||||
logger.debug(
|
||||
f"supported commands: {[command.name for command in supported_commands]}"
|
||||
)
|
||||
for command in supported_commands:
|
||||
self.supported_commands.add(command)
|
||||
|
||||
self.emit(self.EVENT_SUPPORTED_COMMANDS)
|
||||
|
||||
async def _on_entity_update_notification(self, data: bytes) -> None:
|
||||
entity = EntityId(data[0])
|
||||
flags = EntityUpdateFlags(data[2])
|
||||
value = data[3:]
|
||||
|
||||
if flags & EntityUpdateFlags.TRUNCATED:
|
||||
logger.debug("truncated attribute, fetching full value")
|
||||
|
||||
# Write the entity and attribute we're interested in
|
||||
# (protected by a semaphore, so that we only read one attribute at a time)
|
||||
async with self._read_attribute_semaphore:
|
||||
await self._ams_proxy.entity_attribute.write_value(
|
||||
data[:2], with_response=True
|
||||
)
|
||||
value = await self._ams_proxy.entity_attribute.read_value()
|
||||
|
||||
if entity == EntityId.PLAYER:
|
||||
player_attribute = PlayerAttributeId(data[1])
|
||||
if player_attribute == PlayerAttributeId.NAME:
|
||||
self.player_name = value.decode()
|
||||
self.emit(self.EVENT_PLAYER_NAME)
|
||||
elif player_attribute == PlayerAttributeId.PLAYBACK_INFO:
|
||||
playback_state_str, playback_rate_str, elapsed_time_str = (
|
||||
value.decode().split(",")
|
||||
)
|
||||
self.player_playback_info = PlaybackInfo(
|
||||
PlaybackState(int(playback_state_str)),
|
||||
float(playback_rate_str),
|
||||
float(elapsed_time_str),
|
||||
)
|
||||
self.emit(self.EVENT_PLAYER_PLAYBACK_INFO)
|
||||
elif player_attribute == PlayerAttributeId.VOLUME:
|
||||
self.player_volume = float(value.decode())
|
||||
self.emit(self.EVENT_PLAYER_VOLUME)
|
||||
else:
|
||||
logger.warning(f"received unknown player attribute {player_attribute}")
|
||||
|
||||
elif entity == EntityId.QUEUE:
|
||||
queue_attribute = QueueAttributeId(data[1])
|
||||
if queue_attribute == QueueAttributeId.COUNT:
|
||||
self.queue_count = int(value)
|
||||
self.emit(self.EVENT_QUEUE_COUNT)
|
||||
elif queue_attribute == QueueAttributeId.INDEX:
|
||||
self.queue_index = int(value)
|
||||
self.emit(self.EVENT_QUEUE_INDEX)
|
||||
elif queue_attribute == QueueAttributeId.REPEAT_MODE:
|
||||
self.queue_repeat_mode = RepeatMode(int(value))
|
||||
self.emit(self.EVENT_QUEUE_REPEAT_MODE)
|
||||
elif queue_attribute == QueueAttributeId.SHUFFLE_MODE:
|
||||
self.queue_shuffle_mode = ShuffleMode(int(value))
|
||||
self.emit(self.EVENT_QUEUE_SHUFFLE_MODE)
|
||||
else:
|
||||
logger.warning(f"received unknown queue attribute {queue_attribute}")
|
||||
|
||||
elif entity == EntityId.TRACK:
|
||||
track_attribute = TrackAttributeId(data[1])
|
||||
if track_attribute == TrackAttributeId.ARTIST:
|
||||
self.track_artist = value.decode()
|
||||
self.emit(self.EVENT_TRACK_ARTIST)
|
||||
elif track_attribute == TrackAttributeId.ALBUM:
|
||||
self.track_album = value.decode()
|
||||
self.emit(self.EVENT_TRACK_ALBUM)
|
||||
elif track_attribute == TrackAttributeId.TITLE:
|
||||
self.track_title = value.decode()
|
||||
self.emit(self.EVENT_TRACK_TITLE)
|
||||
elif track_attribute == TrackAttributeId.DURATION:
|
||||
self.track_duration = float(value.decode())
|
||||
self.emit(self.EVENT_TRACK_DURATION)
|
||||
else:
|
||||
logger.warning(f"received unknown track attribute {track_attribute}")
|
||||
|
||||
else:
|
||||
logger.warning(f"received unknown attribute ID {data[1]}")
|
||||
@@ -20,6 +20,7 @@ Apple Notification Center Service (ANCS).
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import datetime
|
||||
@@ -28,21 +29,19 @@ import logging
|
||||
import struct
|
||||
from typing import Optional, Sequence, Union
|
||||
|
||||
|
||||
from bumble import utils
|
||||
from bumble.att import ATT_Error
|
||||
from bumble.device import Peer
|
||||
from bumble.gatt import (
|
||||
Characteristic,
|
||||
GATT_ANCS_SERVICE,
|
||||
GATT_ANCS_NOTIFICATION_SOURCE_CHARACTERISTIC,
|
||||
GATT_ANCS_CONTROL_POINT_CHARACTERISTIC,
|
||||
GATT_ANCS_DATA_SOURCE_CHARACTERISTIC,
|
||||
GATT_ANCS_NOTIFICATION_SOURCE_CHARACTERISTIC,
|
||||
GATT_ANCS_SERVICE,
|
||||
Characteristic,
|
||||
TemplateService,
|
||||
)
|
||||
from bumble.gatt_client import CharacteristicProxy, ProfileServiceProxy, ServiceProxy
|
||||
from bumble.gatt_adapters import SerializableCharacteristicProxyAdapter
|
||||
from bumble import utils
|
||||
|
||||
from bumble.gatt_client import CharacteristicProxy, ProfileServiceProxy, ServiceProxy
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
|
||||
@@ -18,22 +18,17 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
import enum
|
||||
import functools
|
||||
import logging
|
||||
import struct
|
||||
from typing import Any, Optional, Union, TypeVar
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Optional, TypeVar, Union
|
||||
|
||||
from bumble import utils
|
||||
from bumble import colors
|
||||
from bumble.profiles.bap import CodecSpecificConfiguration
|
||||
from bumble import colors, device, gatt, gatt_client, hci, utils
|
||||
from bumble.profiles import le_audio
|
||||
from bumble import device
|
||||
from bumble import gatt
|
||||
from bumble import gatt_client
|
||||
from bumble import hci
|
||||
from bumble.profiles.bap import CodecSpecificConfiguration
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -452,6 +447,16 @@ class AseStateMachine(gatt.Characteristic):
|
||||
|
||||
self.metadata = le_audio.Metadata.from_bytes(metadata)
|
||||
self.state = self.State.ENABLING
|
||||
# CIS could be established before enable.
|
||||
if cis_link := next(
|
||||
(
|
||||
cis_link
|
||||
for cis_link in self.service.device.cis_links.values()
|
||||
if cis_link.cig_id == self.cig_id and cis_link.cis_id == self.cis_id
|
||||
),
|
||||
None,
|
||||
):
|
||||
self.on_cis_establishment(cis_link)
|
||||
|
||||
return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
|
||||
|
||||
|
||||
@@ -17,16 +17,13 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import enum
|
||||
import struct
|
||||
import logging
|
||||
from typing import Optional, Callable, Union, Any
|
||||
import struct
|
||||
from typing import Any, Callable, Optional, Union
|
||||
|
||||
from bumble import l2cap
|
||||
from bumble import utils
|
||||
from bumble import gatt
|
||||
from bumble import gatt_client
|
||||
from bumble import data_types, gatt, gatt_client, l2cap, utils
|
||||
from bumble.core import AdvertisingData
|
||||
from bumble.device import Device, Connection
|
||||
from bumble.device import Connection, Device
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -188,12 +185,11 @@ class AshaService(gatt.TemplateService):
|
||||
return bytes(
|
||||
AdvertisingData(
|
||||
[
|
||||
(
|
||||
AdvertisingData.SERVICE_DATA_16_BIT_UUID,
|
||||
bytes(gatt.GATT_ASHA_SERVICE)
|
||||
+ bytes([self.protocol_version, self.capability])
|
||||
data_types.ServiceData16BitUUID(
|
||||
gatt.GATT_ASHA_SERVICE,
|
||||
bytes([self.protocol_version, self.capability])
|
||||
+ self.hisyncid[:4],
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
@@ -18,21 +18,18 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
import dataclasses
|
||||
import enum
|
||||
import struct
|
||||
import functools
|
||||
import logging
|
||||
import struct
|
||||
from collections.abc import Sequence
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from bumble import core
|
||||
from bumble import hci
|
||||
from bumble import gatt
|
||||
from bumble import utils
|
||||
from bumble import core, data_types, gatt, hci, utils
|
||||
from bumble.profiles import le_audio
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -260,11 +257,10 @@ class UnicastServerAdvertisingData:
|
||||
return bytes(
|
||||
core.AdvertisingData(
|
||||
[
|
||||
(
|
||||
core.AdvertisingData.SERVICE_DATA_16_BIT_UUID,
|
||||
data_types.ServiceData16BitUUID(
|
||||
gatt.GATT_AUDIO_STREAM_CONTROL_SERVICE,
|
||||
struct.pack(
|
||||
'<2sBIB',
|
||||
bytes(gatt.GATT_AUDIO_STREAM_CONTROL_SERVICE),
|
||||
'<BIB',
|
||||
self.announcement_type,
|
||||
self.available_audio_contexts,
|
||||
len(self.metadata),
|
||||
@@ -493,12 +489,8 @@ class BroadcastAudioAnnouncement:
|
||||
return bytes(
|
||||
core.AdvertisingData(
|
||||
[
|
||||
(
|
||||
core.AdvertisingData.SERVICE_DATA_16_BIT_UUID,
|
||||
(
|
||||
bytes(gatt.GATT_BROADCAST_AUDIO_ANNOUNCEMENT_SERVICE)
|
||||
+ bytes(self)
|
||||
),
|
||||
data_types.ServiceData16BitUUID(
|
||||
gatt.GATT_BROADCAST_AUDIO_ANNOUNCEMENT_SERVICE, bytes(self)
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -610,12 +602,8 @@ class BasicAudioAnnouncement:
|
||||
return bytes(
|
||||
core.AdvertisingData(
|
||||
[
|
||||
(
|
||||
core.AdvertisingData.SERVICE_DATA_16_BIT_UUID,
|
||||
(
|
||||
bytes(gatt.GATT_BASIC_AUDIO_ANNOUNCEMENT_SERVICE)
|
||||
+ bytes(self)
|
||||
),
|
||||
data_types.ServiceData16BitUUID(
|
||||
gatt.GATT_BASIC_AUDIO_ANNOUNCEMENT_SERVICE, bytes(self)
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
@@ -17,18 +17,13 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import logging
|
||||
import struct
|
||||
from typing import ClassVar, Optional, Sequence
|
||||
|
||||
from bumble import core
|
||||
from bumble import device
|
||||
from bumble import gatt
|
||||
from bumble import gatt_adapters
|
||||
from bumble import gatt_client
|
||||
from bumble import hci
|
||||
from bumble import utils
|
||||
from bumble import core, device, gatt, gatt_adapters, gatt_client, hci, utils
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
|
||||
@@ -18,19 +18,18 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
from typing import Optional
|
||||
|
||||
from bumble.gatt_client import ProfileServiceProxy
|
||||
from bumble.gatt import (
|
||||
GATT_BATTERY_SERVICE,
|
||||
GATT_BATTERY_LEVEL_CHARACTERISTIC,
|
||||
TemplateService,
|
||||
GATT_BATTERY_SERVICE,
|
||||
Characteristic,
|
||||
CharacteristicValue,
|
||||
TemplateService,
|
||||
)
|
||||
from bumble.gatt_client import CharacteristicProxy
|
||||
from bumble.gatt_adapters import (
|
||||
PackedCharacteristicAdapter,
|
||||
PackedCharacteristicProxyAdapter,
|
||||
)
|
||||
from bumble.gatt_client import CharacteristicProxy, ProfileServiceProxy
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -18,8 +18,7 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
from bumble import gatt
|
||||
from bumble import gatt_client
|
||||
from bumble import gatt, gatt_client
|
||||
from bumble.profiles import csip
|
||||
|
||||
|
||||
|
||||
@@ -17,16 +17,12 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
import struct
|
||||
from typing import Optional
|
||||
|
||||
from bumble import core
|
||||
from bumble import crypto
|
||||
from bumble import device
|
||||
from bumble import gatt
|
||||
from bumble import gatt_client
|
||||
|
||||
from bumble import core, crypto, device, gatt, gatt_client
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
|
||||
@@ -25,12 +25,12 @@ from bumble.gatt import (
|
||||
GATT_HARDWARE_REVISION_STRING_CHARACTERISTIC,
|
||||
GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC,
|
||||
GATT_MODEL_NUMBER_STRING_CHARACTERISTIC,
|
||||
GATT_REGULATORY_CERTIFICATION_DATA_LIST_CHARACTERISTIC,
|
||||
GATT_SERIAL_NUMBER_STRING_CHARACTERISTIC,
|
||||
GATT_SOFTWARE_REVISION_STRING_CHARACTERISTIC,
|
||||
GATT_SYSTEM_ID_CHARACTERISTIC,
|
||||
GATT_REGULATORY_CERTIFICATION_DATA_LIST_CHARACTERISTIC,
|
||||
TemplateService,
|
||||
Characteristic,
|
||||
TemplateService,
|
||||
)
|
||||
from bumble.gatt_adapters import (
|
||||
DelegatedCharacteristicProxyAdapter,
|
||||
|
||||
@@ -23,11 +23,11 @@ from typing import Optional, Union
|
||||
|
||||
from bumble.core import Appearance
|
||||
from bumble.gatt import (
|
||||
TemplateService,
|
||||
Characteristic,
|
||||
GATT_GENERIC_ACCESS_SERVICE,
|
||||
GATT_DEVICE_NAME_CHARACTERISTIC,
|
||||
GATT_APPEARANCE_CHARACTERISTIC,
|
||||
GATT_DEVICE_NAME_CHARACTERISTIC,
|
||||
GATT_GENERIC_ACCESS_SERVICE,
|
||||
Characteristic,
|
||||
TemplateService,
|
||||
)
|
||||
from bumble.gatt_adapters import (
|
||||
DelegatedCharacteristicProxyAdapter,
|
||||
|
||||
@@ -17,10 +17,7 @@ from __future__ import annotations
|
||||
import struct
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bumble import att
|
||||
from bumble import gatt
|
||||
from bumble import gatt_client
|
||||
from bumble import crypto
|
||||
from bumble import att, crypto, gatt, gatt_client
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bumble import device
|
||||
|
||||
@@ -18,21 +18,21 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import struct
|
||||
from enum import IntFlag
|
||||
from typing import Optional
|
||||
|
||||
from bumble.gatt import (
|
||||
TemplateService,
|
||||
Characteristic,
|
||||
GATT_BGR_FEATURES_CHARACTERISTIC,
|
||||
GATT_BGS_FEATURES_CHARACTERISTIC,
|
||||
GATT_GAMING_AUDIO_SERVICE,
|
||||
GATT_GMAP_ROLE_CHARACTERISTIC,
|
||||
GATT_UGG_FEATURES_CHARACTERISTIC,
|
||||
GATT_UGT_FEATURES_CHARACTERISTIC,
|
||||
GATT_BGS_FEATURES_CHARACTERISTIC,
|
||||
GATT_BGR_FEATURES_CHARACTERISTIC,
|
||||
Characteristic,
|
||||
TemplateService,
|
||||
)
|
||||
from bumble.gatt_adapters import DelegatedCharacteristicProxyAdapter
|
||||
from bumble.gatt_client import CharacteristicProxy, ProfileServiceProxy, ServiceProxy
|
||||
from enum import IntFlag
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -16,16 +16,15 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import functools
|
||||
from dataclasses import dataclass, field
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
from bumble import att, gatt, gatt_adapters, gatt_client
|
||||
from bumble import att, gatt, gatt_adapters, gatt_client, utils
|
||||
from bumble.core import InvalidArgumentError, InvalidStateError
|
||||
from bumble.device import Device, Connection
|
||||
from bumble import utils
|
||||
from bumble.device import Connection, Device
|
||||
from bumble.hci import Address
|
||||
|
||||
|
||||
@@ -272,7 +271,7 @@ class HearingAccessService(gatt.TemplateService):
|
||||
def on_connection(connection: Connection) -> None:
|
||||
@connection.on(connection.EVENT_DISCONNECTION)
|
||||
def on_disconnection(_reason) -> None:
|
||||
self.currently_connected_clients.remove(connection)
|
||||
self.currently_connected_clients.discard(connection)
|
||||
|
||||
@connection.on(connection.EVENT_PAIRING)
|
||||
def on_pairing(*_: Any) -> None:
|
||||
@@ -373,8 +372,7 @@ class HearingAccessService(gatt.TemplateService):
|
||||
self.preset_records[key]
|
||||
for key in sorted(self.preset_records.keys())
|
||||
if self.preset_records[key].index >= start_index
|
||||
]
|
||||
del presets[num_presets:]
|
||||
][:num_presets]
|
||||
if len(presets) == 0:
|
||||
raise att.ATT_Error(att.ErrorCode.OUT_OF_RANGE)
|
||||
|
||||
@@ -383,7 +381,10 @@ class HearingAccessService(gatt.TemplateService):
|
||||
async def _read_preset_response(
|
||||
self, connection: Connection, presets: list[PresetRecord]
|
||||
):
|
||||
# If the ATT bearer is terminated before all notifications or indications are sent, then the server shall consider the Read Presets Request operation aborted and shall not either continue or restart the operation when the client reconnects.
|
||||
# If the ATT bearer is terminated before all notifications or indications are
|
||||
# sent, then the server shall consider the Read Presets Request operation
|
||||
# aborted and shall not either continue or restart the operation when the client
|
||||
# reconnects.
|
||||
try:
|
||||
for i, preset in enumerate(presets):
|
||||
await connection.device.indicate_subscriber(
|
||||
@@ -404,7 +405,7 @@ class HearingAccessService(gatt.TemplateService):
|
||||
|
||||
async def generic_update(self, op: PresetChangedOperation) -> None:
|
||||
'''Server API to perform a generic update. It is the responsibility of the caller to modify the preset_records to match the PresetChangedOperation being sent'''
|
||||
await self._notifyPresetOperations(op)
|
||||
await self._notify_preset_operations(op)
|
||||
|
||||
async def delete_preset(self, index: int) -> None:
|
||||
'''Server API to delete a preset. It should not be the current active preset'''
|
||||
@@ -413,14 +414,14 @@ class HearingAccessService(gatt.TemplateService):
|
||||
raise InvalidStateError('Cannot delete active preset')
|
||||
|
||||
del self.preset_records[index]
|
||||
await self._notifyPresetOperations(PresetChangedOperationDeleted(index))
|
||||
await self._notify_preset_operations(PresetChangedOperationDeleted(index))
|
||||
|
||||
async def available_preset(self, index: int) -> None:
|
||||
'''Server API to make a preset available'''
|
||||
|
||||
preset = self.preset_records[index]
|
||||
preset.properties.is_available = PresetRecord.Property.IsAvailable.IS_AVAILABLE
|
||||
await self._notifyPresetOperations(PresetChangedOperationAvailable(index))
|
||||
await self._notify_preset_operations(PresetChangedOperationAvailable(index))
|
||||
|
||||
async def unavailable_preset(self, index: int) -> None:
|
||||
'''Server API to make a preset unavailable. It should not be the current active preset'''
|
||||
@@ -432,7 +433,7 @@ class HearingAccessService(gatt.TemplateService):
|
||||
preset.properties.is_available = (
|
||||
PresetRecord.Property.IsAvailable.IS_UNAVAILABLE
|
||||
)
|
||||
await self._notifyPresetOperations(PresetChangedOperationUnavailable(index))
|
||||
await self._notify_preset_operations(PresetChangedOperationUnavailable(index))
|
||||
|
||||
async def _preset_changed_operation(self, connection: Connection) -> None:
|
||||
'''Send all PresetChangedOperation saved for a given connection'''
|
||||
@@ -447,8 +448,10 @@ class HearingAccessService(gatt.TemplateService):
|
||||
return op.additional_parameters
|
||||
|
||||
op_list.sort(key=get_op_index)
|
||||
# If the ATT bearer is terminated before all notifications or indications are sent, then the server shall consider the Preset Changed operation aborted and shall continue the operation when the client reconnects.
|
||||
while len(op_list) > 0:
|
||||
# If the ATT bearer is terminated before all notifications or indications are
|
||||
# sent, then the server shall consider the Preset Changed operation aborted and
|
||||
# shall continue the operation when the client reconnects.
|
||||
while op_list:
|
||||
try:
|
||||
await connection.device.indicate_subscriber(
|
||||
connection,
|
||||
@@ -460,14 +463,15 @@ class HearingAccessService(gatt.TemplateService):
|
||||
except TimeoutError:
|
||||
break
|
||||
|
||||
async def _notifyPresetOperations(self, op: PresetChangedOperation) -> None:
|
||||
for historyList in self.preset_changed_operations_history_per_device.values():
|
||||
historyList.append(op)
|
||||
async def _notify_preset_operations(self, op: PresetChangedOperation) -> None:
|
||||
for history_list in self.preset_changed_operations_history_per_device.values():
|
||||
history_list.append(op)
|
||||
|
||||
for connection in self.currently_connected_clients:
|
||||
await self._preset_changed_operation(connection)
|
||||
|
||||
async def _on_write_preset_name(self, connection: Connection, value: bytes):
|
||||
del connection # Unused
|
||||
|
||||
if self.read_presets_request_in_progress:
|
||||
raise att.ATT_Error(att.ErrorCode.PROCEDURE_ALREADY_IN_PROGRESS)
|
||||
@@ -532,48 +536,51 @@ class HearingAccessService(gatt.TemplateService):
|
||||
self.active_preset_index = index
|
||||
await self.notify_active_preset()
|
||||
|
||||
async def _on_set_active_preset(self, _: Connection, value: bytes):
|
||||
async def _on_set_active_preset(self, connection: Connection, value: bytes):
|
||||
del connection # Unused
|
||||
await self.set_active_preset(value)
|
||||
|
||||
async def set_next_or_previous_preset(self, is_previous):
|
||||
async def set_next_or_previous_preset(self, is_previous: bool) -> None:
|
||||
'''Set the next or the previous preset as active'''
|
||||
|
||||
if self.active_preset_index == 0x00:
|
||||
raise att.ATT_Error(ErrorCode.PRESET_OPERATION_NOT_POSSIBLE)
|
||||
|
||||
first_preset: Optional[PresetRecord] = None # To loop to first preset
|
||||
next_preset: Optional[PresetRecord] = None
|
||||
for index, record in sorted(self.preset_records.items(), reverse=is_previous):
|
||||
if not record.is_available():
|
||||
continue
|
||||
if first_preset == None:
|
||||
first_preset = record
|
||||
if is_previous:
|
||||
if index >= self.active_preset_index:
|
||||
continue
|
||||
elif index <= self.active_preset_index:
|
||||
continue
|
||||
next_preset = record
|
||||
break
|
||||
presets = sorted(
|
||||
[
|
||||
record
|
||||
for record in self.preset_records.values()
|
||||
if record.is_available()
|
||||
],
|
||||
key=lambda record: record.index,
|
||||
)
|
||||
current_preset = self.preset_records[self.active_preset_index]
|
||||
current_preset_pos = presets.index(current_preset)
|
||||
if is_previous:
|
||||
new_preset = presets[(current_preset_pos - 1) % len(presets)]
|
||||
else:
|
||||
new_preset = presets[(current_preset_pos + 1) % len(presets)]
|
||||
|
||||
if not first_preset: # If no other preset are available
|
||||
if current_preset == new_preset: # If no other preset are available
|
||||
raise att.ATT_Error(ErrorCode.PRESET_OPERATION_NOT_POSSIBLE)
|
||||
|
||||
if next_preset:
|
||||
self.active_preset_index = next_preset.index
|
||||
else:
|
||||
self.active_preset_index = first_preset.index
|
||||
self.active_preset_index = new_preset.index
|
||||
await self.notify_active_preset()
|
||||
|
||||
async def _on_set_next_preset(self, _: Connection, __value__: bytes) -> None:
|
||||
async def _on_set_next_preset(self, connection: Connection, value: bytes) -> None:
|
||||
del connection, value # Unused.
|
||||
await self.set_next_or_previous_preset(False)
|
||||
|
||||
async def _on_set_previous_preset(self, _: Connection, __value__: bytes) -> None:
|
||||
async def _on_set_previous_preset(
|
||||
self, connection: Connection, value: bytes
|
||||
) -> None:
|
||||
del connection, value # Unused.
|
||||
await self.set_next_or_previous_preset(True)
|
||||
|
||||
async def _on_set_active_preset_synchronized_locally(
|
||||
self, _: Connection, value: bytes
|
||||
self, connection: Connection, value: bytes
|
||||
):
|
||||
del connection # Unused.
|
||||
if (
|
||||
self.server_features.preset_synchronization_support
|
||||
== PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_NOT_SUPPORTED
|
||||
@@ -584,8 +591,9 @@ class HearingAccessService(gatt.TemplateService):
|
||||
await self.other_server_in_binaural_set.set_active_preset(value)
|
||||
|
||||
async def _on_set_next_preset_synchronized_locally(
|
||||
self, _: Connection, __value__: bytes
|
||||
self, connection: Connection, value: bytes
|
||||
):
|
||||
del connection, value # Unused.
|
||||
if (
|
||||
self.server_features.preset_synchronization_support
|
||||
== PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_NOT_SUPPORTED
|
||||
@@ -596,8 +604,9 @@ class HearingAccessService(gatt.TemplateService):
|
||||
await self.other_server_in_binaural_set.set_next_or_previous_preset(False)
|
||||
|
||||
async def _on_set_previous_preset_synchronized_locally(
|
||||
self, _: Connection, __value__: bytes
|
||||
self, connection: Connection, value: bytes
|
||||
):
|
||||
del connection, value # Unused.
|
||||
if (
|
||||
self.server_features.preset_synchronization_support
|
||||
== PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_NOT_SUPPORTED
|
||||
@@ -615,11 +624,13 @@ class HearingAccessServiceProxy(gatt_client.ProfileServiceProxy):
|
||||
SERVICE_CLASS = HearingAccessService
|
||||
|
||||
hearing_aid_preset_control_point: gatt_client.CharacteristicProxy
|
||||
preset_control_point_indications: asyncio.Queue
|
||||
active_preset_index_notification: asyncio.Queue
|
||||
preset_control_point_indications: asyncio.Queue[bytes]
|
||||
active_preset_index_notification: asyncio.Queue[bytes]
|
||||
|
||||
def __init__(self, service_proxy: gatt_client.ServiceProxy) -> None:
|
||||
self.service_proxy = service_proxy
|
||||
self.preset_control_point_indications = asyncio.Queue()
|
||||
self.active_preset_index_notification = asyncio.Queue()
|
||||
|
||||
self.server_features = gatt_adapters.PackedCharacteristicProxyAdapter(
|
||||
service_proxy.get_characteristics_by_uuid(
|
||||
@@ -641,20 +652,12 @@ class HearingAccessServiceProxy(gatt_client.ProfileServiceProxy):
|
||||
'B',
|
||||
)
|
||||
|
||||
async def setup_subscription(self):
|
||||
self.preset_control_point_indications = asyncio.Queue()
|
||||
self.active_preset_index_notification = asyncio.Queue()
|
||||
|
||||
def on_active_preset_index_notification(data: bytes):
|
||||
self.active_preset_index_notification.put_nowait(data)
|
||||
|
||||
def on_preset_control_point_indication(data: bytes):
|
||||
self.preset_control_point_indications.put_nowait(data)
|
||||
|
||||
async def setup_subscription(self) -> None:
|
||||
await self.hearing_aid_preset_control_point.subscribe(
|
||||
functools.partial(on_preset_control_point_indication), prefer_notify=False
|
||||
self.preset_control_point_indications.put_nowait,
|
||||
prefer_notify=False,
|
||||
)
|
||||
|
||||
await self.active_preset_index.subscribe(
|
||||
functools.partial(on_active_preset_index_notification)
|
||||
self.active_preset_index_notification.put_nowait
|
||||
)
|
||||
|
||||
@@ -17,20 +17,21 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
from enum import IntEnum
|
||||
|
||||
import struct
|
||||
from enum import IntEnum
|
||||
from typing import Optional
|
||||
|
||||
from bumble import core
|
||||
from bumble.att import ATT_Error
|
||||
from bumble.gatt import (
|
||||
GATT_HEART_RATE_SERVICE,
|
||||
GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC,
|
||||
GATT_BODY_SENSOR_LOCATION_CHARACTERISTIC,
|
||||
GATT_HEART_RATE_CONTROL_POINT_CHARACTERISTIC,
|
||||
TemplateService,
|
||||
GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC,
|
||||
GATT_HEART_RATE_SERVICE,
|
||||
Characteristic,
|
||||
CharacteristicValue,
|
||||
TemplateService,
|
||||
)
|
||||
from bumble.gatt_adapters import (
|
||||
DelegatedCharacteristicAdapter,
|
||||
|
||||
@@ -16,14 +16,16 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import enum
|
||||
import struct
|
||||
from typing import Any
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from bumble.profiles import bap
|
||||
from bumble import utils
|
||||
from bumble.profiles import bap
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -22,16 +22,12 @@ import asyncio
|
||||
import dataclasses
|
||||
import enum
|
||||
import struct
|
||||
from typing import TYPE_CHECKING, ClassVar, Optional
|
||||
|
||||
from bumble import core
|
||||
from bumble import device
|
||||
from bumble import gatt
|
||||
from bumble import gatt_client
|
||||
from bumble import utils
|
||||
|
||||
from typing import Optional, ClassVar, TYPE_CHECKING
|
||||
from typing_extensions import Self
|
||||
|
||||
from bumble import core, device, gatt, gatt_client, utils
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -17,18 +17,15 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import logging
|
||||
import struct
|
||||
from typing import Optional, Sequence, Union
|
||||
|
||||
from bumble.profiles.bap import AudioLocation, CodecSpecificCapabilities, ContextType
|
||||
from bumble import gatt, gatt_adapters, gatt_client, hci
|
||||
from bumble.profiles import le_audio
|
||||
from bumble import gatt
|
||||
from bumble import gatt_adapters
|
||||
from bumble import gatt_client
|
||||
from bumble import hci
|
||||
|
||||
from bumble.profiles.bap import AudioLocation, CodecSpecificCapabilities, ContextType
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
|
||||
@@ -16,8 +16,10 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import enum
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from bumble.profiles import le_audio
|
||||
|
||||
@@ -22,15 +22,14 @@ import logging
|
||||
import struct
|
||||
|
||||
from bumble.gatt import (
|
||||
TemplateService,
|
||||
Characteristic,
|
||||
GATT_TELEPHONY_AND_MEDIA_AUDIO_SERVICE,
|
||||
GATT_TMAP_ROLE_CHARACTERISTIC,
|
||||
Characteristic,
|
||||
TemplateService,
|
||||
)
|
||||
from bumble.gatt_adapters import DelegatedCharacteristicProxyAdapter
|
||||
from bumble.gatt_client import CharacteristicProxy, ProfileServiceProxy, ServiceProxy
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -17,18 +17,12 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import enum
|
||||
|
||||
from typing import Sequence
|
||||
|
||||
from bumble import att
|
||||
from bumble import utils
|
||||
from bumble import device
|
||||
from bumble import gatt
|
||||
from bumble import gatt_adapters
|
||||
from bumble import gatt_client
|
||||
|
||||
from bumble import att, device, gatt, gatt_adapters, gatt_client, utils
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
|
||||
@@ -20,17 +20,18 @@ import struct
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from bumble.device import Connection
|
||||
from bumble import utils
|
||||
from bumble.att import ATT_Error
|
||||
from bumble.device import Connection
|
||||
from bumble.gatt import (
|
||||
Characteristic,
|
||||
TemplateService,
|
||||
CharacteristicValue,
|
||||
GATT_AUDIO_LOCATION_CHARACTERISTIC,
|
||||
GATT_AUDIO_OUTPUT_DESCRIPTION_CHARACTERISTIC,
|
||||
GATT_VOLUME_OFFSET_CONTROL_POINT_CHARACTERISTIC,
|
||||
GATT_VOLUME_OFFSET_CONTROL_SERVICE,
|
||||
GATT_VOLUME_OFFSET_STATE_CHARACTERISTIC,
|
||||
GATT_AUDIO_LOCATION_CHARACTERISTIC,
|
||||
GATT_VOLUME_OFFSET_CONTROL_POINT_CHARACTERISTIC,
|
||||
GATT_AUDIO_OUTPUT_DESCRIPTION_CHARACTERISTIC,
|
||||
Characteristic,
|
||||
CharacteristicValue,
|
||||
TemplateService,
|
||||
)
|
||||
from bumble.gatt_adapters import (
|
||||
DelegatedCharacteristicProxyAdapter,
|
||||
@@ -38,7 +39,6 @@ from bumble.gatt_adapters import (
|
||||
UTF8CharacteristicProxyAdapter,
|
||||
)
|
||||
from bumble.gatt_client import ProfileServiceProxy, ServiceProxy
|
||||
from bumble import utils
|
||||
from bumble.profiles.bap import AudioLocation
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -17,33 +17,30 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import asyncio
|
||||
import collections
|
||||
import dataclasses
|
||||
import enum
|
||||
from typing import Callable, Optional, Union, TYPE_CHECKING
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Callable, Optional, Union
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
|
||||
from bumble import core
|
||||
from bumble import l2cap
|
||||
from bumble import sdp
|
||||
from bumble import utils
|
||||
from bumble import core, l2cap, sdp, utils
|
||||
from bumble.colors import color
|
||||
from bumble.core import (
|
||||
UUID,
|
||||
BT_RFCOMM_PROTOCOL_ID,
|
||||
PhysicalTransport,
|
||||
BT_L2CAP_PROTOCOL_ID,
|
||||
BT_RFCOMM_PROTOCOL_ID,
|
||||
UUID,
|
||||
InvalidArgumentError,
|
||||
InvalidStateError,
|
||||
InvalidPacketError,
|
||||
InvalidStateError,
|
||||
PhysicalTransport,
|
||||
ProtocolError,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bumble.device import Device, Connection
|
||||
from bumble.device import Connection, Device
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -1047,8 +1044,8 @@ class Client:
|
||||
self.l2cap_channel = await self.connection.create_l2cap_channel(
|
||||
spec=l2cap.ClassicChannelSpec(psm=RFCOMM_PSM, mtu=self.l2cap_mtu)
|
||||
)
|
||||
except ProtocolError as error:
|
||||
logger.warning(f'L2CAP connection failed: {error}')
|
||||
except ProtocolError:
|
||||
logger.exception('L2CAP connection failed')
|
||||
raise
|
||||
|
||||
assert self.l2cap_channel is not None
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
|
||||
|
||||
|
||||
@@ -16,24 +16,26 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import struct
|
||||
from typing import Iterable, NewType, Optional, Union, Sequence, TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Iterable, NewType, Optional, Sequence, Union
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from bumble import core, l2cap
|
||||
from bumble.colors import color
|
||||
from bumble.core import (
|
||||
InvalidStateError,
|
||||
InvalidArgumentError,
|
||||
InvalidPacketError,
|
||||
InvalidStateError,
|
||||
ProtocolError,
|
||||
)
|
||||
from bumble.hci import HCI_Object, name_or_number, key_with_value
|
||||
from bumble.hci import HCI_Object, key_with_value, name_or_number
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bumble.device import Device, Connection
|
||||
from bumble.device import Connection, Device
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -1084,8 +1086,8 @@ class Server:
|
||||
def on_pdu(self, pdu):
|
||||
try:
|
||||
sdp_pdu = SDP_PDU.from_bytes(pdu)
|
||||
except Exception as error:
|
||||
logger.warning(color(f'failed to parse SDP Request PDU: {error}', 'red'))
|
||||
except Exception:
|
||||
logger.exception(color('failed to parse SDP Request PDU', 'red'))
|
||||
self.send_response(
|
||||
SDP_ErrorResponse(
|
||||
transaction_id=0, error_code=SDP_INVALID_REQUEST_SYNTAX_ERROR
|
||||
@@ -1100,8 +1102,8 @@ class Server:
|
||||
if handler:
|
||||
try:
|
||||
handler(sdp_pdu)
|
||||
except Exception as error:
|
||||
logger.exception(f'{color("!!! Exception in handler:", "red")} {error}')
|
||||
except Exception:
|
||||
logger.exception(color("!!! Exception in handler:", "red"))
|
||||
self.send_response(
|
||||
SDP_ErrorResponse(
|
||||
transaction_id=sdp_pdu.transaction_id,
|
||||
|
||||
310
bumble/smp.py
310
bumble/smp.py
@@ -23,38 +23,41 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
import logging
|
||||
|
||||
import asyncio
|
||||
import enum
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Awaitable,
|
||||
Callable,
|
||||
ClassVar,
|
||||
Optional,
|
||||
TypeVar,
|
||||
cast,
|
||||
)
|
||||
|
||||
|
||||
from bumble import crypto, utils
|
||||
from bumble.colors import color
|
||||
from bumble.hci import (
|
||||
Address,
|
||||
Role,
|
||||
HCI_LE_Enable_Encryption_Command,
|
||||
HCI_Object,
|
||||
key_with_value,
|
||||
)
|
||||
from bumble.core import (
|
||||
PhysicalTransport,
|
||||
AdvertisingData,
|
||||
InvalidArgumentError,
|
||||
PhysicalTransport,
|
||||
ProtocolError,
|
||||
name_or_number,
|
||||
)
|
||||
from bumble.hci import (
|
||||
Address,
|
||||
Fields,
|
||||
HCI_LE_Enable_Encryption_Command,
|
||||
HCI_Object,
|
||||
Role,
|
||||
key_with_value,
|
||||
metadata,
|
||||
)
|
||||
from bumble.keys import PairingKeys
|
||||
from bumble import crypto
|
||||
from bumble import utils
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bumble.device import Connection, Device
|
||||
@@ -200,31 +203,32 @@ def error_name(error_code: int) -> str:
|
||||
# -----------------------------------------------------------------------------
|
||||
# Classes
|
||||
# -----------------------------------------------------------------------------
|
||||
@dataclass
|
||||
class SMP_Command:
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part H - 3 SECURITY MANAGER PROTOCOL
|
||||
'''
|
||||
|
||||
smp_classes: dict[int, type[SMP_Command]] = {}
|
||||
fields: Any
|
||||
code = 0
|
||||
name = ''
|
||||
smp_classes: ClassVar[dict[int, type[SMP_Command]]] = {}
|
||||
fields: ClassVar[Fields]
|
||||
code: int = field(default=0, init=False)
|
||||
name: str = field(default='', init=False)
|
||||
_payload: Optional[bytes] = field(default=None, init=False)
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(pdu: bytes) -> "SMP_Command":
|
||||
@classmethod
|
||||
def from_bytes(cls, pdu: bytes) -> "SMP_Command":
|
||||
code = pdu[0]
|
||||
|
||||
cls = SMP_Command.smp_classes.get(code)
|
||||
if cls is None:
|
||||
instance = SMP_Command(pdu)
|
||||
subclass = SMP_Command.smp_classes.get(code)
|
||||
if subclass is None:
|
||||
instance = SMP_Command()
|
||||
instance.name = SMP_Command.command_name(code)
|
||||
instance.code = code
|
||||
instance.payload = pdu
|
||||
return instance
|
||||
self = cls.__new__(cls)
|
||||
SMP_Command.__init__(self, pdu)
|
||||
if hasattr(self, 'fields'):
|
||||
self.init_from_bytes(pdu, 1)
|
||||
return self
|
||||
instance = subclass(**HCI_Object.dict_from_bytes(pdu, 1, subclass.fields))
|
||||
instance.payload = pdu[1:]
|
||||
return instance
|
||||
|
||||
@staticmethod
|
||||
def command_name(code: int) -> str:
|
||||
@@ -264,36 +268,35 @@ class SMP_Command:
|
||||
def keypress_notification_type_name(notification_type: int) -> str:
|
||||
return name_or_number(SMP_KEYPRESS_NOTIFICATION_TYPE_NAMES, notification_type)
|
||||
|
||||
@staticmethod
|
||||
def subclass(fields):
|
||||
def inner(cls):
|
||||
cls.name = cls.__name__.upper()
|
||||
cls.code = key_with_value(SMP_COMMAND_NAMES, cls.name)
|
||||
if cls.code is None:
|
||||
raise KeyError(
|
||||
f'Command name {cls.name} not found in SMP_COMMAND_NAMES'
|
||||
)
|
||||
cls.fields = fields
|
||||
_Command = TypeVar("_Command", bound="SMP_Command")
|
||||
|
||||
# Register a factory for this class
|
||||
SMP_Command.smp_classes[cls.code] = cls
|
||||
@classmethod
|
||||
def subclass(cls, subclass: type[_Command]) -> type[_Command]:
|
||||
subclass.name = subclass.__name__.upper()
|
||||
subclass.code = key_with_value(SMP_COMMAND_NAMES, subclass.name)
|
||||
if subclass.code is None:
|
||||
raise KeyError(
|
||||
f'Command name {subclass.name} not found in SMP_COMMAND_NAMES'
|
||||
)
|
||||
subclass.fields = HCI_Object.fields_from_dataclass(subclass)
|
||||
|
||||
return cls
|
||||
# Register a factory for this class
|
||||
SMP_Command.smp_classes[subclass.code] = subclass
|
||||
|
||||
return inner
|
||||
return subclass
|
||||
|
||||
def __init__(self, pdu: Optional[bytes] = None, **kwargs: Any) -> None:
|
||||
if hasattr(self, 'fields') and kwargs:
|
||||
HCI_Object.init_from_fields(self, self.fields, kwargs)
|
||||
if pdu is None:
|
||||
pdu = bytes([self.code]) + HCI_Object.dict_to_bytes(kwargs, self.fields)
|
||||
self.pdu = pdu
|
||||
@property
|
||||
def payload(self) -> bytes:
|
||||
if self._payload is None:
|
||||
self._payload = HCI_Object.dict_to_bytes(self.__dict__, self.fields)
|
||||
return self._payload
|
||||
|
||||
def init_from_bytes(self, pdu: bytes, offset: int) -> None:
|
||||
return HCI_Object.init_from_bytes(self, pdu, offset, self.fields)
|
||||
@payload.setter
|
||||
def payload(self, value: bytes) -> None:
|
||||
self._payload = value
|
||||
|
||||
def __bytes__(self):
|
||||
return self.pdu
|
||||
return bytes([self.code]) + self.payload
|
||||
|
||||
def __str__(self):
|
||||
result = color(self.name, 'yellow')
|
||||
@@ -306,206 +309,192 @@ class SMP_Command:
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@SMP_Command.subclass(
|
||||
[
|
||||
('io_capability', {'size': 1, 'mapper': SMP_Command.io_capability_name}),
|
||||
('oob_data_flag', 1),
|
||||
('auth_req', {'size': 1, 'mapper': SMP_Command.auth_req_str}),
|
||||
('maximum_encryption_key_size', 1),
|
||||
(
|
||||
'initiator_key_distribution',
|
||||
{'size': 1, 'mapper': SMP_Command.key_distribution_str},
|
||||
),
|
||||
(
|
||||
'responder_key_distribution',
|
||||
{'size': 1, 'mapper': SMP_Command.key_distribution_str},
|
||||
),
|
||||
]
|
||||
)
|
||||
@SMP_Command.subclass
|
||||
@dataclass
|
||||
class SMP_Pairing_Request_Command(SMP_Command):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part H - 3.5.1 Pairing Request
|
||||
'''
|
||||
|
||||
io_capability: int
|
||||
oob_data_flag: int
|
||||
auth_req: int
|
||||
maximum_encryption_key_size: int
|
||||
initiator_key_distribution: int
|
||||
responder_key_distribution: int
|
||||
io_capability: int = field(
|
||||
metadata=metadata({'size': 1, 'mapper': SMP_Command.io_capability_name})
|
||||
)
|
||||
oob_data_flag: int = field(metadata=metadata(1))
|
||||
auth_req: int = field(
|
||||
metadata=metadata({'size': 1, 'mapper': SMP_Command.auth_req_str})
|
||||
)
|
||||
maximum_encryption_key_size: int = field(metadata=metadata(1))
|
||||
initiator_key_distribution: int = field(
|
||||
metadata=metadata({'size': 1, 'mapper': SMP_Command.key_distribution_str})
|
||||
)
|
||||
responder_key_distribution: int = field(
|
||||
metadata=metadata({'size': 1, 'mapper': SMP_Command.key_distribution_str})
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@SMP_Command.subclass(
|
||||
[
|
||||
('io_capability', {'size': 1, 'mapper': SMP_Command.io_capability_name}),
|
||||
('oob_data_flag', 1),
|
||||
('auth_req', {'size': 1, 'mapper': SMP_Command.auth_req_str}),
|
||||
('maximum_encryption_key_size', 1),
|
||||
(
|
||||
'initiator_key_distribution',
|
||||
{'size': 1, 'mapper': SMP_Command.key_distribution_str},
|
||||
),
|
||||
(
|
||||
'responder_key_distribution',
|
||||
{'size': 1, 'mapper': SMP_Command.key_distribution_str},
|
||||
),
|
||||
]
|
||||
)
|
||||
@SMP_Command.subclass
|
||||
@dataclass
|
||||
class SMP_Pairing_Response_Command(SMP_Command):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part H - 3.5.2 Pairing Response
|
||||
'''
|
||||
|
||||
io_capability: int
|
||||
oob_data_flag: int
|
||||
auth_req: int
|
||||
maximum_encryption_key_size: int
|
||||
initiator_key_distribution: int
|
||||
responder_key_distribution: int
|
||||
io_capability: int = field(
|
||||
metadata=metadata({'size': 1, 'mapper': SMP_Command.io_capability_name})
|
||||
)
|
||||
oob_data_flag: int = field(metadata=metadata(1))
|
||||
auth_req: int = field(
|
||||
metadata=metadata({'size': 1, 'mapper': SMP_Command.auth_req_str})
|
||||
)
|
||||
maximum_encryption_key_size: int = field(metadata=metadata(1))
|
||||
initiator_key_distribution: int = field(
|
||||
metadata=metadata({'size': 1, 'mapper': SMP_Command.key_distribution_str})
|
||||
)
|
||||
responder_key_distribution: int = field(
|
||||
metadata=metadata({'size': 1, 'mapper': SMP_Command.key_distribution_str})
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@SMP_Command.subclass([('confirm_value', 16)])
|
||||
@SMP_Command.subclass
|
||||
@dataclass
|
||||
class SMP_Pairing_Confirm_Command(SMP_Command):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part H - 3.5.3 Pairing Confirm
|
||||
'''
|
||||
|
||||
confirm_value: bytes
|
||||
confirm_value: bytes = field(metadata=metadata(16))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@SMP_Command.subclass([('random_value', 16)])
|
||||
@SMP_Command.subclass
|
||||
@dataclass
|
||||
class SMP_Pairing_Random_Command(SMP_Command):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part H - 3.5.4 Pairing Random
|
||||
'''
|
||||
|
||||
random_value: bytes
|
||||
random_value: bytes = field(metadata=metadata(16))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@SMP_Command.subclass([('reason', {'size': 1, 'mapper': error_name})])
|
||||
@SMP_Command.subclass
|
||||
@dataclass
|
||||
class SMP_Pairing_Failed_Command(SMP_Command):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part H - 3.5.5 Pairing Failed
|
||||
'''
|
||||
|
||||
reason: int
|
||||
reason: int = field(metadata=metadata({'size': 1, 'mapper': error_name}))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@SMP_Command.subclass([('public_key_x', 32), ('public_key_y', 32)])
|
||||
@SMP_Command.subclass
|
||||
@dataclass
|
||||
class SMP_Pairing_Public_Key_Command(SMP_Command):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part H - 3.5.6 Pairing Public Key
|
||||
'''
|
||||
|
||||
public_key_x: bytes
|
||||
public_key_y: bytes
|
||||
public_key_x: bytes = field(metadata=metadata(32))
|
||||
public_key_y: bytes = field(metadata=metadata(32))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@SMP_Command.subclass(
|
||||
[
|
||||
('dhkey_check', 16),
|
||||
]
|
||||
)
|
||||
@SMP_Command.subclass
|
||||
@dataclass
|
||||
class SMP_Pairing_DHKey_Check_Command(SMP_Command):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part H - 3.5.7 Pairing DHKey Check
|
||||
'''
|
||||
|
||||
dhkey_check: bytes
|
||||
dhkey_check: bytes = field(metadata=metadata(16))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@SMP_Command.subclass(
|
||||
[
|
||||
(
|
||||
'notification_type',
|
||||
{'size': 1, 'mapper': SMP_Command.keypress_notification_type_name},
|
||||
),
|
||||
]
|
||||
)
|
||||
@SMP_Command.subclass
|
||||
@dataclass
|
||||
class SMP_Pairing_Keypress_Notification_Command(SMP_Command):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part H - 3.5.8 Keypress Notification
|
||||
'''
|
||||
|
||||
notification_type: int
|
||||
notification_type: int = field(
|
||||
metadata=metadata(
|
||||
{'size': 1, 'mapper': SMP_Command.keypress_notification_type_name}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@SMP_Command.subclass([('long_term_key', 16)])
|
||||
@SMP_Command.subclass
|
||||
@dataclass
|
||||
class SMP_Encryption_Information_Command(SMP_Command):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part H - 3.6.2 Encryption Information
|
||||
'''
|
||||
|
||||
long_term_key: bytes
|
||||
long_term_key: bytes = field(metadata=metadata(16))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@SMP_Command.subclass([('ediv', 2), ('rand', 8)])
|
||||
@SMP_Command.subclass
|
||||
@dataclass
|
||||
class SMP_Master_Identification_Command(SMP_Command):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part H - 3.6.3 Master Identification
|
||||
'''
|
||||
|
||||
ediv: int
|
||||
rand: bytes
|
||||
ediv: int = field(metadata=metadata(2))
|
||||
rand: bytes = field(metadata=metadata(8))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@SMP_Command.subclass([('identity_resolving_key', 16)])
|
||||
@SMP_Command.subclass
|
||||
@dataclass
|
||||
class SMP_Identity_Information_Command(SMP_Command):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part H - 3.6.4 Identity Information
|
||||
'''
|
||||
|
||||
identity_resolving_key: bytes
|
||||
identity_resolving_key: bytes = field(metadata=metadata(16))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@SMP_Command.subclass(
|
||||
[
|
||||
('addr_type', Address.ADDRESS_TYPE_SPEC),
|
||||
('bd_addr', Address.parse_address_preceded_by_type),
|
||||
]
|
||||
)
|
||||
@SMP_Command.subclass
|
||||
@dataclass
|
||||
class SMP_Identity_Address_Information_Command(SMP_Command):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part H - 3.6.5 Identity Address Information
|
||||
'''
|
||||
|
||||
addr_type: int
|
||||
bd_addr: Address
|
||||
addr_type: int = field(metadata=metadata(Address.ADDRESS_TYPE_SPEC))
|
||||
bd_addr: Address = field(metadata=metadata(Address.parse_address_preceded_by_type))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@SMP_Command.subclass([('signature_key', 16)])
|
||||
@SMP_Command.subclass
|
||||
@dataclass
|
||||
class SMP_Signing_Information_Command(SMP_Command):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part H - 3.6.6 Signing Information
|
||||
'''
|
||||
|
||||
signature_key: bytes
|
||||
signature_key: bytes = field(metadata=metadata(16))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@SMP_Command.subclass(
|
||||
[
|
||||
('auth_req', {'size': 1, 'mapper': SMP_Command.auth_req_str}),
|
||||
]
|
||||
)
|
||||
@SMP_Command.subclass
|
||||
@dataclass
|
||||
class SMP_Security_Request_Command(SMP_Command):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part H - 3.6.7 Security Request
|
||||
'''
|
||||
|
||||
auth_req: int
|
||||
auth_req: int = field(
|
||||
metadata=metadata({'size': 1, 'mapper': SMP_Command.auth_req_str})
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -892,8 +881,8 @@ class Session:
|
||||
if response:
|
||||
next_steps()
|
||||
return
|
||||
except Exception as error:
|
||||
logger.warning(f'exception while confirm: {error}')
|
||||
except Exception:
|
||||
logger.exception('exception while confirm')
|
||||
|
||||
self.send_pairing_failed(SMP_CONFIRM_VALUE_FAILED_ERROR)
|
||||
|
||||
@@ -911,8 +900,8 @@ class Session:
|
||||
if response:
|
||||
next_steps()
|
||||
return
|
||||
except Exception as error:
|
||||
logger.warning(f'exception while prompting: {error}')
|
||||
except Exception:
|
||||
logger.exception('exception while prompting')
|
||||
|
||||
self.send_pairing_failed(SMP_CONFIRM_VALUE_FAILED_ERROR)
|
||||
|
||||
@@ -929,8 +918,8 @@ class Session:
|
||||
return
|
||||
logger.debug(f'user input: {passkey}')
|
||||
next_steps(passkey)
|
||||
except Exception as error:
|
||||
logger.warning(f'exception while prompting: {error}')
|
||||
except Exception:
|
||||
logger.exception('exception while prompting')
|
||||
self.send_pairing_failed(SMP_PASSKEY_ENTRY_FAILED_ERROR)
|
||||
|
||||
self.connection.cancel_on_disconnection(prompt())
|
||||
@@ -946,7 +935,9 @@ class Session:
|
||||
self.tk = self.passkey.to_bytes(16, byteorder='little')
|
||||
logger.debug(f'TK from passkey = {self.tk.hex()}')
|
||||
|
||||
await self.pairing_config.delegate.display_number(self.passkey, digits=6)
|
||||
self.connection.cancel_on_disconnection(
|
||||
self.pairing_config.delegate.display_number(self.passkey, digits=6)
|
||||
)
|
||||
|
||||
def input_passkey(self, next_steps: Optional[Callable[[], None]] = None) -> None:
|
||||
# Prompt the user for the passkey displayed on the peer
|
||||
@@ -976,8 +967,8 @@ class Session:
|
||||
|
||||
try:
|
||||
self.connection.cancel_on_disconnection(display_passkey())
|
||||
except Exception as error:
|
||||
logger.warning(f'exception while displaying passkey: {error}')
|
||||
except Exception:
|
||||
logger.exception('exception while displaying passkey')
|
||||
else:
|
||||
self.input_passkey(next_steps)
|
||||
|
||||
@@ -1422,8 +1413,8 @@ class Session:
|
||||
if handler is not None:
|
||||
try:
|
||||
handler(command)
|
||||
except Exception as error:
|
||||
logger.exception(f'{color("!!! Exception in handler:", "red")} {error}')
|
||||
except Exception:
|
||||
logger.exception(color("!!! Exception in handler:", "red"))
|
||||
response = SMP_Pairing_Failed_Command(
|
||||
reason=SMP_UNSPECIFIED_REASON_ERROR
|
||||
)
|
||||
@@ -1444,8 +1435,8 @@ class Session:
|
||||
# Check if the request should proceed
|
||||
try:
|
||||
accepted = await self.pairing_config.delegate.accept()
|
||||
except Exception as error:
|
||||
logger.warning(f'exception while accepting: {error}')
|
||||
except Exception:
|
||||
logger.exception('exception while accepting')
|
||||
accepted = False
|
||||
if not accepted:
|
||||
logger.debug('pairing rejected by delegate')
|
||||
@@ -1569,11 +1560,12 @@ class Session:
|
||||
if self.pairing_method == PairingMethod.CTKD_OVER_CLASSIC:
|
||||
# Authentication is already done in SMP, so remote shall start keys distribution immediately
|
||||
return
|
||||
elif self.sc:
|
||||
|
||||
if self.sc:
|
||||
self.send_public_key_command()
|
||||
|
||||
if self.pairing_method == PairingMethod.PASSKEY:
|
||||
self.display_or_input_passkey()
|
||||
|
||||
self.send_public_key_command()
|
||||
else:
|
||||
if self.pairing_method == PairingMethod.PASSKEY:
|
||||
self.display_or_input_passkey(self.send_pairing_confirm_command)
|
||||
@@ -1846,10 +1838,10 @@ class Session:
|
||||
elif self.pairing_method == PairingMethod.PASSKEY:
|
||||
self.send_pairing_confirm_command()
|
||||
else:
|
||||
# Send our public key back to the initiator
|
||||
self.send_public_key_command()
|
||||
|
||||
def next_steps() -> None:
|
||||
# Send our public key back to the initiator
|
||||
self.send_public_key_command()
|
||||
|
||||
if self.pairing_method in (
|
||||
PairingMethod.JUST_WORKS,
|
||||
|
||||
@@ -12,21 +12,21 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import struct
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from contextlib import contextmanager
|
||||
from enum import IntEnum
|
||||
import logging
|
||||
import struct
|
||||
import datetime
|
||||
from typing import BinaryIO, Generator
|
||||
import os
|
||||
|
||||
from bumble import core
|
||||
from bumble.hci import HCI_COMMAND_PACKET, HCI_EVENT_PACKET
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -12,21 +12,18 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from contextlib import asynccontextmanager
|
||||
import logging
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
from bumble import utils
|
||||
from bumble.transport.common import (
|
||||
Transport,
|
||||
SnoopingTransport,
|
||||
TransportSpecError,
|
||||
)
|
||||
from bumble.snoop import create_snooper
|
||||
from bumble.transport.common import SnoopingTransport, Transport, TransportSpecError
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -48,8 +45,8 @@ def _wrap_transport(transport: Transport) -> Transport:
|
||||
return SnoopingTransport.create_with(
|
||||
transport, create_snooper(snooper_spec)
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(f'Exception while creating snooper: {exc}')
|
||||
except Exception:
|
||||
logger.exception('Exception while creating snooper')
|
||||
|
||||
return transport
|
||||
|
||||
@@ -185,12 +182,18 @@ async def _open_transport(scheme: str, spec: Optional[str]) -> Transport:
|
||||
|
||||
return await open_android_netsim_transport(spec)
|
||||
|
||||
if scheme == 'unix':
|
||||
if scheme in ('unix', 'unix-client'):
|
||||
from bumble.transport.unix import open_unix_client_transport
|
||||
|
||||
assert spec
|
||||
return await open_unix_client_transport(spec)
|
||||
|
||||
if scheme == 'unix-server':
|
||||
from bumble.transport.unix import open_unix_server_transport
|
||||
|
||||
assert spec
|
||||
return await open_unix_server_transport(spec)
|
||||
|
||||
raise TransportSpecError('unknown transport scheme')
|
||||
|
||||
|
||||
|
||||
@@ -16,28 +16,27 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import logging
|
||||
import grpc.aio
|
||||
|
||||
from typing import Optional, Union
|
||||
|
||||
import grpc.aio
|
||||
|
||||
from bumble.transport.common import (
|
||||
PumpedTransport,
|
||||
PumpedPacketSource,
|
||||
PumpedPacketSink,
|
||||
PumpedPacketSource,
|
||||
PumpedTransport,
|
||||
Transport,
|
||||
TransportSpecError,
|
||||
)
|
||||
|
||||
# pylint: disable=no-name-in-module
|
||||
from bumble.transport.grpc_protobuf.emulated_bluetooth_packets_pb2 import HCIPacket
|
||||
from bumble.transport.grpc_protobuf.emulated_bluetooth_pb2_grpc import (
|
||||
EmulatedBluetoothServiceStub,
|
||||
)
|
||||
from bumble.transport.grpc_protobuf.emulated_bluetooth_packets_pb2 import HCIPacket
|
||||
from bumble.transport.grpc_protobuf.emulated_bluetooth_vhci_pb2_grpc import (
|
||||
VhciForwardingServiceStub,
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -77,21 +76,17 @@ async def open_android_emulator_transport(spec: Optional[str]) -> Transport:
|
||||
|
||||
# Parse the parameters
|
||||
mode = 'host'
|
||||
server_host = 'localhost'
|
||||
server_port = '8554'
|
||||
server_address = 'localhost:8554'
|
||||
if spec:
|
||||
params = spec.split(',')
|
||||
for param in params:
|
||||
if param.startswith('mode='):
|
||||
mode = param.split('=')[1]
|
||||
elif ':' in param:
|
||||
server_host, server_port = param.split(':')
|
||||
else:
|
||||
raise TransportSpecError('invalid parameter')
|
||||
server_address = param
|
||||
|
||||
# Connect to the gRPC server
|
||||
server_address = f'{server_host}:{server_port}'
|
||||
logger.debug(f'connecting to gRPC server at {server_address}')
|
||||
logger.debug('connecting to gRPC server at %s', server_address)
|
||||
channel = grpc.aio.insecure_channel(server_address)
|
||||
|
||||
service: Union[EmulatedBluetoothServiceStub, VhciForwardingServiceStub]
|
||||
|
||||
@@ -29,28 +29,27 @@ import grpc.aio
|
||||
import bumble
|
||||
from bumble.transport.common import (
|
||||
ParserSource,
|
||||
PumpedTransport,
|
||||
PumpedPacketSource,
|
||||
PumpedPacketSink,
|
||||
PumpedPacketSource,
|
||||
PumpedTransport,
|
||||
Transport,
|
||||
TransportSpecError,
|
||||
TransportInitError,
|
||||
TransportSpecError,
|
||||
)
|
||||
|
||||
# pylint: disable=no-name-in-module
|
||||
from bumble.transport.grpc_protobuf.netsim.packet_streamer_pb2_grpc import (
|
||||
PacketStreamerStub,
|
||||
PacketStreamerServicer,
|
||||
add_PacketStreamerServicer_to_server,
|
||||
)
|
||||
from bumble.transport.grpc_protobuf.netsim.common_pb2 import ChipKind
|
||||
from bumble.transport.grpc_protobuf.netsim.hci_packet_pb2 import HCIPacket
|
||||
from bumble.transport.grpc_protobuf.netsim.packet_streamer_pb2 import (
|
||||
PacketRequest,
|
||||
PacketResponse,
|
||||
)
|
||||
from bumble.transport.grpc_protobuf.netsim.hci_packet_pb2 import HCIPacket
|
||||
from bumble.transport.grpc_protobuf.netsim.packet_streamer_pb2_grpc import (
|
||||
PacketStreamerServicer,
|
||||
PacketStreamerStub,
|
||||
add_PacketStreamerServicer_to_server,
|
||||
)
|
||||
from bumble.transport.grpc_protobuf.netsim.startup_pb2 import Chip, ChipInfo, DeviceInfo
|
||||
from bumble.transport.grpc_protobuf.netsim.common_pb2 import ChipKind
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -145,8 +144,6 @@ def publish_grpc_port(grpc_port: int, instance_number: int) -> bool:
|
||||
async def open_android_netsim_controller_transport(
|
||||
server_host: Optional[str], server_port: int, options: dict[str, str]
|
||||
) -> Transport:
|
||||
if not server_port:
|
||||
raise TransportSpecError('invalid port')
|
||||
if server_host == '_' or not server_host:
|
||||
server_host = 'localhost'
|
||||
|
||||
@@ -168,14 +165,16 @@ async def open_android_netsim_controller_transport(
|
||||
await self.pump_loop()
|
||||
except asyncio.CancelledError:
|
||||
logger.debug('Pump task canceled')
|
||||
self.done.set_result(None)
|
||||
if not self.done.done():
|
||||
self.done.set_result(None)
|
||||
|
||||
async def pump_loop(self):
|
||||
while True:
|
||||
request = await self.context.read()
|
||||
if request == grpc.aio.EOF:
|
||||
logger.debug('End of request stream')
|
||||
self.done.set_result(None)
|
||||
if not self.done.done():
|
||||
self.done.set_result(None)
|
||||
return
|
||||
|
||||
# If we're not initialized yet, wait for a init packet.
|
||||
@@ -220,6 +219,8 @@ async def open_android_netsim_controller_transport(
|
||||
async def wait_for_termination(self):
|
||||
await self.done
|
||||
|
||||
server_address = f'{server_host}:{server_port}'
|
||||
|
||||
class Server(PacketStreamerServicer, ParserSource):
|
||||
def __init__(self):
|
||||
PacketStreamerServicer.__init__(self)
|
||||
@@ -230,8 +231,8 @@ async def open_android_netsim_controller_transport(
|
||||
# a server listening on that port, we get an exception.
|
||||
self.grpc_server = grpc.aio.server(options=(('grpc.so_reuseport', 0),))
|
||||
add_PacketStreamerServicer_to_server(self, self.grpc_server)
|
||||
self.grpc_server.add_insecure_port(f'{server_host}:{server_port}')
|
||||
logger.debug(f'gRPC server listening on {server_host}:{server_port}')
|
||||
self.port = self.grpc_server.add_insecure_port(server_address)
|
||||
logger.debug('gRPC server listening on %s', server_address)
|
||||
|
||||
async def start(self):
|
||||
logger.debug('Starting gRPC server')
|
||||
@@ -443,7 +444,7 @@ async def open_android_netsim_transport(spec: Optional[str]) -> Transport:
|
||||
params = spec.split(',') if spec else []
|
||||
if params and ':' in params[0]:
|
||||
# Explicit <host>:<port>
|
||||
host, port_str = params[0].split(':')
|
||||
host, port_str = params[0].rsplit(':', maxsplit=1)
|
||||
port = int(port_str)
|
||||
params_offset = 1
|
||||
else:
|
||||
|
||||
@@ -16,19 +16,18 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
import contextlib
|
||||
import struct
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import contextlib
|
||||
import io
|
||||
import logging
|
||||
import struct
|
||||
from typing import Any, ContextManager, Optional, Protocol
|
||||
|
||||
from bumble import core
|
||||
from bumble import hci
|
||||
from bumble import core, hci
|
||||
from bumble.colors import color
|
||||
from bumble.snoop import Snooper
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -90,8 +89,8 @@ class PacketPump:
|
||||
try:
|
||||
# Deliver the packet to the sink
|
||||
self.sink.on_packet(await self.reader.next_packet())
|
||||
except Exception as error:
|
||||
logger.warning(f'!!! {error}')
|
||||
except Exception:
|
||||
logger.exception('!!!')
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -158,10 +157,8 @@ class PacketParser:
|
||||
if self.sink:
|
||||
try:
|
||||
self.sink.on_packet(bytes(self.packet))
|
||||
except Exception as error:
|
||||
logger.exception(
|
||||
color(f'!!! Exception in on_packet: {error}', 'red')
|
||||
)
|
||||
except Exception:
|
||||
logger.exception(color('!!! Exception in on_packet', 'red'))
|
||||
self.reset()
|
||||
|
||||
def set_packet_sink(self, sink: TransportSink) -> None:
|
||||
@@ -378,7 +375,7 @@ class PumpedPacketSource(ParserSource):
|
||||
self.terminated.set_result(None)
|
||||
break
|
||||
except Exception as error:
|
||||
logger.warning(f'exception while waiting for packet: {error}')
|
||||
logger.exception('exception while waiting for packet')
|
||||
if not self.terminated.done():
|
||||
self.terminated.set_exception(error)
|
||||
break
|
||||
@@ -409,8 +406,8 @@ class PumpedPacketSink:
|
||||
except asyncio.CancelledError:
|
||||
logger.debug('sink pump task done')
|
||||
break
|
||||
except Exception as error:
|
||||
logger.warning(f'exception while sending packet: {error}')
|
||||
except Exception:
|
||||
logger.exception('exception while sending packet')
|
||||
break
|
||||
|
||||
self.pump_task = asyncio.create_task(pump_packets())
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user