Compare commits

..

78 Commits

Author SHA1 Message Date
zxzxwu
768bbd95cc Merge pull request #778 from zxzxwu/rust
Upgrade Rust to 1.80.0
2025-09-17 16:08:15 +08:00
Josh Wu
502b80af0d Upgrade Rust to 1.80.0 2025-09-17 13:34:08 +08:00
zxzxwu
a25427305c Merge pull request #775 from khsiao-google/update
Remove the word 'complete' from function name
2025-09-17 13:18:37 +08:00
zxzxwu
3c47739029 Merge pull request #776 from khsiao-google/test_coverage
Add a2dp_test.py tests for a2dp.py
2025-09-17 13:18:14 +08:00
zxzxwu
8fc1330948 Merge pull request #777 from zxzxwu/iso
Handle ISO data path race condition
2025-09-17 13:17:53 +08:00
Josh Wu
83c5061700 Handle ISO data path race condition 2025-09-16 13:39:09 +08:00
khsiao-google
b80b790dc1 Remove the word 'complete' from function name 2025-09-16 03:45:32 +00:00
khsiao-google
21bf69592c Add a2dp_test.py tests for a2dp.py 2025-09-16 03:23:53 +00:00
zxzxwu
7d8addb849 Merge pull request #762 from zxzxwu/ipv6
Distinguish IPv6 address and metadata
2025-09-10 15:58:41 +08:00
khsiao-google
d86d69d816 Merge pull request #771 from khsiao-google/update
Improve connection related functions and names
2025-09-10 14:56:38 +08:00
Josh Wu
bb08a1c70b Distinguish IPv6 address and metadata 2025-09-09 11:59:51 +08:00
khsiao-google
dc93f32a9a Replace core.ConnectionParameters by Connection.Parameters in device.py 2025-09-08 02:00:49 +00:00
zxzxwu
9838908a26 Merge pull request #772 from zxzxwu/hap
HAP: Slightly Pythonic refactor
2025-09-05 23:08:09 +08:00
Josh Wu
613519f0b3 HAP: Slightly Pythonic refactor
* Add missing type annotations
* Avoid __value__ and _ arguments (this will be a problem for override).
* Replace while-pop with for loop
2025-09-05 21:02:16 +08:00
zxzxwu
a943ea57ef Merge pull request #770 from zxzxwu/avrcp
AVRCP: Implement most commands and responses
2025-09-04 16:18:54 +08:00
Josh Wu
14401910bb AVRCP: Implement most commands and responses 2025-09-03 13:20:10 +08:00
khsiao-google
5d35ed471c Merge pull request #769 from khsiao-google/update
Add typing for host.py
2025-09-02 14:59:27 +08:00
khsiao-google
c720ad5fdc Add typing for host.py 2025-09-02 06:01:39 +00:00
khsiao-google
f02183f95d Merge pull request #764 from khsiao-google/update
Add typing for device.py
2025-09-01 15:19:57 +08:00
khsiao-google
d903937a51 Merge branch 'main' into update 2025-09-01 07:14:19 +00:00
zxzxwu
6381ee0ab1 Merge pull request #767 from zxzxwu/avrcp
Migrate AVRCP packets to dataclasses
2025-09-01 13:26:56 +08:00
Gilles Boccon-Gibod
59d99780e1 Merge pull request #768 from google/gbg/data-types
add support for data type classes
2025-08-30 13:04:32 -07:00
Gilles Boccon-Gibod
4bf0bc03af more python compat 2025-08-30 12:13:34 -07:00
Gilles Boccon-Gibod
91ba2f61f1 python 3.9 and 3.10 compatibility 2025-08-30 12:07:08 -07:00
Gilles Boccon-Gibod
116dc9b319 add support for data type classes 2025-08-29 13:17:17 -07:00
Josh Wu
9f3d8c9b49 Migrate AVRCP responses to dataclasses 2025-08-28 21:42:38 +08:00
Josh Wu
31961febe5 Migrate AVRCP events to dataclasses 2025-08-28 17:00:20 +08:00
Josh Wu
dab0993cba Migrate AVRCP packets to dataclasses 2025-08-28 17:00:20 +08:00
zxzxwu
6f73b736d7 Merge pull request #766 from zxzxwu/l2cap
Remove depreacated L2CAP APIs
2025-08-28 10:58:35 +08:00
Josh Wu
6091e6365d Remove depreacated L2CAP APIs 2025-08-27 14:15:08 +08:00
khsiao-google
3333ba472b Add typing for device.py 2025-08-26 09:22:06 +00:00
Gilles Boccon-Gibod
8bda7d2212 Merge pull request #763 from google/gbg/isort 2025-08-22 13:50:27 -07:00
Gilles Boccon-Gibod
7aba36302a use isort when formatting 2025-08-21 16:38:58 -07:00
zxzxwu
ceefe8b2a5 Merge pull request #760 from zxzxwu/ipv6
Enhance transports
2025-08-21 14:31:50 +08:00
Josh Wu
cd37027795 Add android-netsim self test 2025-08-21 14:07:36 +08:00
Josh Wu
bb2aa8229d Enhance transports
* Support IPv6 schema
* Add transport integration tests
* Add UNIX socket server
2025-08-21 13:44:24 +08:00
zxzxwu
4aed53c48d Merge pull request #759 from zxzxwu/log
Always log exception using logging.exception
2025-08-20 13:22:47 +08:00
Josh Wu
4a88e9a0cf Always log exception using logging.exception 2025-08-18 16:03:58 +08:00
zxzxwu
3b8dd6f3cf Merge pull request #751 from zxzxwu/l2cap
Add L2CAP Credit Based packets definitions (0x17-0x1A)
2025-08-13 12:32:23 +08:00
Josh Wu
f41b7746d2 Add L2CAP credit based packets definitions 2025-08-13 11:59:24 +08:00
zxzxwu
1b727741bf Merge pull request #754 from zxzxwu/big
Fix wrong BIG parameters and flows
2025-08-13 11:57:10 +08:00
zxzxwu
d2bc8175fb Merge pull request #756 from zxzxwu/att
Migrate ATT PDU to dataclasses
2025-08-13 11:56:51 +08:00
zxzxwu
84dfff290a Merge pull request #755 from zxzxwu/smp
Migrate SMP commands to dataclasses
2025-08-13 11:56:42 +08:00
Josh Wu
17563e423a Migrate ATT PDU to dataclasses 2025-08-12 12:37:29 +08:00
Josh Wu
19d3616032 Migrate SMP commands to dataclasses 2025-08-12 12:36:35 +08:00
Josh Wu
4a48309643 Fix wrong BIG parameters and flows 2025-08-11 16:32:56 +08:00
Gilles Boccon-Gibod
870217acb3 Merge pull request #750 from google/gbg/rtk-driver-enhancement
gbg/rtk driver enhancement
2025-08-09 09:00:42 -07:00
Gilles Boccon-Gibod
f8077d7996 use user-agent header with intel FW downloader 2025-08-08 18:02:33 -07:00
Gilles Boccon-Gibod
739907fa31 rtk: print info when fw is already loaded 2025-08-08 18:02:33 -07:00
zxzxwu
a275c399a3 Merge pull request #734 from khsiao-google/le_subrating
Support LE Subrating
2025-08-07 16:52:17 +08:00
zxzxwu
c98275f385 Merge pull request #743 from zxzxwu/ascs
ASCS: Handle when CIS link is established before enable
2025-08-06 12:18:52 +08:00
khsiao-google
0b19347bef Only reset subrate_factor and continuation_number when connection interval changes 2025-08-06 03:55:41 +00:00
Josh Wu
f61fd64c0b ASCS: Handle when CIS link is established before enable 2025-08-05 17:31:42 +08:00
khsiao-google
ec12771be6 Support HCI_LE_Set_Host_Feature_Command 2025-08-05 05:56:00 +00:00
Gilles Boccon-Gibod
5b33e715da Merge pull request #742 from barbibulle/gbg/enable-manual-workflow-run 2025-08-04 20:57:23 -07:00
Gilles Boccon-Gibod
b885f29318 Merge pull request #740 from barbibulle/gbg/fix-735 2025-08-04 20:57:04 -07:00
Gilles Boccon-Gibod
7ca13188d5 Merge pull request #741 from barbibulle/gbg/update-black 2025-08-04 20:56:40 -07:00
Gilles Boccon-Gibod
89586d5d18 enable manual workflow runs 2025-08-04 19:46:04 -07:00
Gilles Boccon-Gibod
381032ceb9 update to black 25.1 2025-08-04 19:32:52 -07:00
Gilles Boccon-Gibod
12ca1c01f0 Revert "update to black formatter 25.1"
This reverts commit c034297bc0.
2025-08-04 19:24:30 -07:00
Gilles Boccon-Gibod
a7111d0107 send public keys earlier 2025-08-04 19:18:12 -07:00
Gilles Boccon-Gibod
c034297bc0 update to black formatter 25.1 2025-08-02 21:11:34 -07:00
Gilles Boccon-Gibod
a1eff958e6 do not wait for display 2025-08-02 21:10:45 -07:00
khsiao-google
d6282a7247 Support LE Subrating reply to comments 2025-08-03 03:39:23 +00:00
Gilles Boccon-Gibod
efdc770fde Merge pull request #737 from leifdreizler/fix-spdx-license
Update license field to use proper SPDX identifier
2025-08-02 11:22:58 -07:00
Leif
357d7f9c22 Update pyproject.toml 2025-08-02 08:18:36 -04:00
Leif Dreizler
3bc08b4e0d Update license field to use proper SPDX identifier
This changes the license field to be a valid [SPDX identifier](https://spdx.org/licenses) aligning with [PEP 639](https://peps.python.org/pep-0639/#project-source-metadata). This populates the `license_expression` field in the PyPI API and is used by downstream tools including deps.dev

These changes were generated by Claude after reviewing the license and manifest files in your repository, but opened and reviewed by me. Please let me know if the analysis is incorrect and thanks for being an OSS maintainer.
2025-08-01 20:19:25 -04:00
khsiao-google
982aaeabc3 Support LE Subrating 2025-07-31 02:52:42 +00:00
Gilles Boccon-Gibod
1dc0950177 Merge pull request #730 from google/gbg/apple-media-service
basic AMS implementation
2025-07-29 22:34:25 -07:00
zxzxwu
df0fd74533 Merge pull request #733 from zxzxwu/l2cap
Fix L2CAP_Control_Frame errors
2025-07-30 13:12:44 +08:00
Josh Wu
822f97fa84 Fix L2CAP errors 2025-07-30 12:00:20 +08:00
Gilles Boccon-Gibod
4a6b0ef840 Merge pull request #732 from google/gbg/722
fix #722
2025-07-29 10:50:02 -07:00
Gilles Boccon-Gibod
a6ead0147e fix #722 2025-07-28 13:36:55 -07:00
Gilles Boccon-Gibod
0665e9ca5c Merge pull request #731 from google/gbg/common-logger
use common logger
2025-07-28 10:22:30 -07:00
Gilles Boccon-Gibod
b8b78ca1ee add missing file 2025-07-27 15:02:42 -07:00
Gilles Boccon-Gibod
d611d25802 resolve merge conflicts 2025-07-26 21:20:52 -07:00
Gilles Boccon-Gibod
bf8a2cdcb5 add discrete command methods 2025-07-26 20:24:55 -07:00
Gilles Boccon-Gibod
4bf7448a01 basic AMS implementation 2025-07-22 14:57:52 -07:00
208 changed files with 7518 additions and 4048 deletions

View File

@@ -6,6 +6,8 @@ on:
branches: [ main ] branches: [ main ]
pull_request: pull_request:
branches: [ main ] branches: [ main ]
workflow_dispatch:
branches: [main]
permissions: permissions:
contents: read contents: read

View File

@@ -17,6 +17,8 @@ on:
pull_request: pull_request:
# The branches below must be a subset of the branches above # The branches below must be a subset of the branches above
branches: [ main ] branches: [ main ]
workflow_dispatch:
branches: [main]
schedule: schedule:
- cron: '39 21 * * 4' - cron: '39 21 * * 4'

View File

@@ -7,6 +7,10 @@ on:
branches: [ main ] branches: [ main ]
paths: paths:
- 'extras/android/BtBench/**' - 'extras/android/BtBench/**'
workflow_dispatch:
branches: [main]
paths:
- 'extras/android/BtBench/**'
permissions: permissions:
contents: read contents: read

View File

@@ -5,6 +5,8 @@ on:
branches: [ main ] branches: [ main ]
pull_request: pull_request:
branches: [ main ] branches: [ main ]
workflow_dispatch:
branches: [main]
permissions: permissions:
contents: read contents: read

View File

@@ -6,6 +6,8 @@ on:
branches: [ main ] branches: [ main ]
pull_request: pull_request:
branches: [ main ] branches: [ main ]
workflow_dispatch:
branches: [main]
permissions: permissions:
contents: read contents: read
@@ -47,7 +49,7 @@ jobs:
strategy: strategy:
matrix: matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
rust-version: [ "1.76.0", "stable" ] rust-version: [ "1.80.0", "stable" ]
fail-fast: false fail-fast: false
steps: steps:
- name: Check out from Git - name: Check out from Git
@@ -70,7 +72,7 @@ jobs:
- name: Check License Headers - name: Check License Headers
run: cd rust && cargo run --features dev-tools --bin file-header check-all run: cd rust && cargo run --features dev-tools --bin file-header check-all
- name: Rust Build - name: Rust Build
run: cd rust && cargo build --all-targets && cargo build-all-features --all-targets run: cd rust && cargo build --all-targets && cargo build-all-features
# Lints after build so what clippy needs is already built # Lints after build so what clippy needs is already built
- name: Rust Lints - name: Rust Lints
run: cd rust && cargo fmt --check && cargo clippy --all-targets -- --deny warnings && cargo clippy --all-features --all-targets -- --deny warnings run: cd rust && cargo fmt --check && cargo clippy --all-targets -- --deny warnings && cargo clippy --all-features --all-targets -- --deny warnings

View File

@@ -102,5 +102,10 @@
"." "."
], ],
"python.testing.unittestEnabled": false, "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"
]
} }

View File

@@ -23,14 +23,8 @@ import contextlib
import dataclasses import dataclasses
import functools import functools
import logging import logging
import os
import struct import struct
from typing import ( from typing import Any, AsyncGenerator, Coroutine, Optional
Any,
AsyncGenerator,
Coroutine,
Optional,
)
import click import click
@@ -41,19 +35,14 @@ except ImportError as e:
"Try `python -m pip install \"git+https://github.com/google/liblc3.git\"`." "Try `python -m pip install \"git+https://github.com/google/liblc3.git\"`."
) from e ) 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.device
import bumble.logging
import bumble.transport import bumble.transport
import bumble.utils 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 # Logging
@@ -870,21 +859,13 @@ async def run_transmit(
) )
broadcast_audio_announcement = bap.BroadcastAudioAnnouncement(broadcast_id) broadcast_audio_announcement = bap.BroadcastAudioAnnouncement(broadcast_id)
advertising_manufacturer_data = ( advertising_data_types: list[core.DataType] = [
b'' data_types.BroadcastName(broadcast_name)
if manufacturer_data is None ]
else bytes( if manufacturer_data is not None:
core.AdvertisingData( advertising_data_types.append(
[ data_types.ManufacturerSpecificData(*manufacturer_data)
(
core.AdvertisingData.MANUFACTURER_SPECIFIC_DATA,
struct.pack('<H', manufacturer_data[0])
+ manufacturer_data[1],
)
]
)
) )
)
advertising_set = await device.create_advertising_set( advertising_set = await device.create_advertising_set(
advertising_parameters=bumble.device.AdvertisingParameters( advertising_parameters=bumble.device.AdvertisingParameters(
@@ -896,12 +877,7 @@ async def run_transmit(
), ),
advertising_data=( advertising_data=(
broadcast_audio_announcement.get_advertising_data() broadcast_audio_announcement.get_advertising_data()
+ bytes( + bytes(core.AdvertisingData(advertising_data_types))
core.AdvertisingData(
[(core.AdvertisingData.BROADCAST_NAME, broadcast_name.encode())]
)
)
+ advertising_manufacturer_data
), ),
periodic_advertising_parameters=bumble.device.PeriodicAdvertisingParameters( periodic_advertising_parameters=bumble.device.PeriodicAdvertisingParameters(
periodic_advertising_interval_min=80, periodic_advertising_interval_min=80,
@@ -1235,7 +1211,7 @@ def transmit(
def main(): def main():
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper()) bumble.logging.setup_basic_logging()
auracast() auracast()

View File

@@ -19,7 +19,6 @@ import asyncio
import dataclasses import dataclasses
import enum import enum
import logging import logging
import os
import statistics import statistics
import struct import struct
import time import time
@@ -27,16 +26,19 @@ from typing import Optional
import click import click
import bumble.core
import bumble.logging
import bumble.rfcomm
from bumble import l2cap from bumble import l2cap
from bumble.colors import color
from bumble.core import ( from bumble.core import (
PhysicalTransport,
BT_L2CAP_PROTOCOL_ID, BT_L2CAP_PROTOCOL_ID,
BT_RFCOMM_PROTOCOL_ID, BT_RFCOMM_PROTOCOL_ID,
UUID, UUID,
CommandTimeoutError, CommandTimeoutError,
ConnectionPHY,
PhysicalTransport,
) )
from bumble.colors import color
from bumble.core import ConnectionPHY
from bumble.device import ( from bumble.device import (
CigParameters, CigParameters,
CisLink, CisLink,
@@ -50,12 +52,13 @@ from bumble.hci import (
HCI_LE_1M_PHY, HCI_LE_1M_PHY,
HCI_LE_2M_PHY, HCI_LE_2M_PHY,
HCI_LE_CODED_PHY, HCI_LE_CODED_PHY,
Role,
HCI_Constant, HCI_Constant,
HCI_Error, HCI_Error,
HCI_StatusError,
HCI_IsoDataPacket, HCI_IsoDataPacket,
HCI_StatusError,
Role,
) )
from bumble.pairing import PairingConfig
from bumble.sdp import ( from bumble.sdp import (
SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID, SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID, SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
@@ -66,11 +69,7 @@ from bumble.sdp import (
ServiceAttribute, ServiceAttribute,
) )
from bumble.transport import open_transport from bumble.transport import open_transport
import bumble.rfcomm
import bumble.core
from bumble.utils import AsyncRunner from bumble.utils import AsyncRunner
from bumble.pairing import PairingConfig
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Logging # Logging
@@ -2321,11 +2320,7 @@ def peripheral(ctx, transport):
def main(): def main():
logging.basicConfig( bumble.logging.setup_basic_logging('INFO')
level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper(),
format="[%(asctime)s.%(msecs)03d] %(levelname)s:%(name)s:%(message)s",
datefmt="%H:%M:%S",
)
bench() bench()

View File

@@ -13,6 +13,7 @@
# limitations under the License. # limitations under the License.
import click import click
from bumble.colors import color from bumble.colors import color
from bumble.hci import Address from bumble.hci import Address
from bumble.helpers import generate_irk, verify_rpa_with_irk from bumble.helpers import generate_irk, verify_rpa_with_irk

View File

@@ -23,58 +23,55 @@ import asyncio
import logging import logging
import os import os
import re import re
import humanize
from typing import Optional, Union
from collections import OrderedDict from collections import OrderedDict
from typing import Optional, Union
import click import click
import humanize
from prettytable import PrettyTable from prettytable import PrettyTable
from prompt_toolkit import Application from prompt_toolkit import Application
from prompt_toolkit.history import FileHistory
from prompt_toolkit.completion import Completer, Completion, NestedCompleter 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.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 ( from prompt_toolkit.layout import (
Layout,
HSplit,
Window,
CompletionsMenu, CompletionsMenu,
Float,
FormattedTextControl,
FloatContainer,
ConditionalContainer, ConditionalContainer,
Dimension, 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 import bumble.core
from bumble import colors from bumble import __version__, colors
from bumble.core import UUID, AdvertisingData from bumble.core import UUID, AdvertisingData
from bumble.device import ( from bumble.device import (
Connection,
ConnectionParametersPreferences, ConnectionParametersPreferences,
ConnectionPHY, ConnectionPHY,
Device, Device,
Connection,
Peer, Peer,
) )
from bumble.utils import AsyncRunner from bumble.gatt import Characteristic, CharacteristicDeclaration, Descriptor, Service
from bumble.transport import open_transport
from bumble.gatt import Characteristic, Service, CharacteristicDeclaration, Descriptor
from bumble.gatt_client import CharacteristicProxy from bumble.gatt_client import CharacteristicProxy
from bumble.hci import ( from bumble.hci import (
Address,
HCI_Constant,
HCI_LE_1M_PHY, HCI_LE_1M_PHY,
HCI_LE_2M_PHY, HCI_LE_2M_PHY,
HCI_LE_CODED_PHY, HCI_LE_CODED_PHY,
Address,
HCI_Constant,
) )
from bumble.transport import open_transport
from bumble.utils import AsyncRunner
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Constants # Constants

View File

@@ -16,46 +16,45 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
import asyncio import asyncio
import os
import logging
import time import time
import click import click
from bumble.company_ids import COMPANY_IDENTIFIERS import bumble.logging
from bumble.colors import color from bumble.colors import color
from bumble.company_ids import COMPANY_IDENTIFIERS
from bumble.core import name_or_number from bumble.core import name_or_number
from bumble.hci import ( from bumble.hci import (
map_null_terminated_utf8_string, HCI_LE_READ_BUFFER_SIZE_COMMAND,
CodecID, HCI_LE_READ_BUFFER_SIZE_V2_COMMAND,
LeFeature, 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_SUCCESS,
HCI_VERSION_NAMES, HCI_VERSION_NAMES,
LMP_VERSION_NAMES, LMP_VERSION_NAMES,
CodecID,
HCI_Command, HCI_Command,
HCI_Command_Complete_Event, HCI_Command_Complete_Event,
HCI_Command_Status_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_Buffer_Size_Command,
HCI_LE_READ_MAXIMUM_DATA_LENGTH_COMMAND, HCI_LE_Read_Buffer_Size_V2_Command,
HCI_LE_Read_Maximum_Data_Length_Command,
HCI_LE_READ_NUMBER_OF_SUPPORTED_ADVERTISING_SETS_COMMAND,
HCI_LE_Read_Number_Of_Supported_Advertising_Sets_Command,
HCI_LE_READ_MAXIMUM_ADVERTISING_DATA_LENGTH_COMMAND,
HCI_LE_Read_Maximum_Advertising_Data_Length_Command, HCI_LE_Read_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_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_Command,
HCI_Read_Local_Supported_Codecs_V2_Command, HCI_Read_Local_Supported_Codecs_V2_Command,
HCI_Read_Local_Version_Information_Command, HCI_Read_Local_Version_Information_Command,
LeFeature,
map_null_terminated_utf8_string,
) )
from bumble.host import Host from bumble.host import Host
from bumble.transport import open_transport from bumble.transport import open_transport
@@ -342,11 +341,7 @@ async def async_main(
) )
@click.argument('transport') @click.argument('transport')
def main(latency_probes, latency_probe_interval, latency_probe_command, transport): def main(latency_probes, latency_probe_interval, latency_probe_command, transport):
logging.basicConfig( bumble.logging.setup_basic_logging()
level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper(),
format="[%(asctime)s.%(msecs)03d] %(levelname)s:%(name)s:%(message)s",
datefmt="%H:%M:%S",
)
asyncio.run( asyncio.run(
async_main( async_main(
latency_probes, latency_probe_interval, latency_probe_command, transport latency_probes, latency_probe_interval, latency_probe_command, transport

View File

@@ -16,21 +16,22 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
import asyncio import asyncio
import logging
import os
import time import time
from typing import Optional from typing import Optional
import click
import bumble.logging
from bumble.colors import color from bumble.colors import color
from bumble.hci import ( from bumble.hci import (
HCI_READ_LOOPBACK_MODE_COMMAND, HCI_READ_LOOPBACK_MODE_COMMAND,
HCI_Read_Loopback_Mode_Command,
HCI_WRITE_LOOPBACK_MODE_COMMAND, HCI_WRITE_LOOPBACK_MODE_COMMAND,
HCI_Read_Loopback_Mode_Command,
HCI_Write_Loopback_Mode_Command, HCI_Write_Loopback_Mode_Command,
LoopbackMode, LoopbackMode,
) )
from bumble.host import Host from bumble.host import Host
from bumble.transport import open_transport from bumble.transport import open_transport
import click
class Loopback: class Loopback:
@@ -194,12 +195,7 @@ class Loopback:
) )
@click.argument('transport') @click.argument('transport')
def main(packet_size, packet_count, transport): def main(packet_size, packet_count, transport):
logging.basicConfig( bumble.logging.setup_basic_logging()
level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper(),
format="[%(asctime)s.%(msecs)03d] %(levelname)s:%(name)s:%(message)s",
datefmt="%H:%M:%S",
)
loopback = Loopback(packet_size, packet_count, transport) loopback = Loopback(packet_size, packet_count, transport)
asyncio.run(loopback.run()) asyncio.run(loopback.run())

View File

@@ -15,11 +15,10 @@
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
import logging
import asyncio import asyncio
import sys import sys
import os
import bumble.logging
from bumble.controller import Controller from bumble.controller import Controller
from bumble.link import LocalLink from bumble.link import LocalLink
from bumble.transport import open_transport from bumble.transport import open_transport
@@ -62,7 +61,7 @@ async def async_main():
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def main(): def main():
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper()) bumble.logging.setup_basic_logging()
asyncio.run(async_main()) asyncio.run(async_main())

View File

@@ -16,18 +16,17 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
import asyncio import asyncio
import os
import logging
from typing import Callable, Iterable, Optional from typing import Callable, Iterable, Optional
import click import click
from bumble.core import ProtocolError import bumble.logging
from bumble.colors import color from bumble.colors import color
from bumble.core import ProtocolError
from bumble.device import Device, Peer from bumble.device import Device, Peer
from bumble.gatt import Service from bumble.gatt import Service
from bumble.profiles.device_information_service import DeviceInformationServiceProxy
from bumble.profiles.battery_service import BatteryServiceProxy from bumble.profiles.battery_service import BatteryServiceProxy
from bumble.profiles.device_information_service import DeviceInformationServiceProxy
from bumble.profiles.gap import GenericAccessServiceProxy from bumble.profiles.gap import GenericAccessServiceProxy
from bumble.profiles.pacs import PublishedAudioCapabilitiesServiceProxy from bumble.profiles.pacs import PublishedAudioCapabilitiesServiceProxy
from bumble.profiles.tmap import TelephonyAndMediaAudioServiceProxy 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, Dump the GATT database on a remote device. If ADDRESS_OR_NAME is not specified,
wait for an incoming connection. 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)) asyncio.run(async_main(device_config, encrypt, transport, address_or_name))

View File

@@ -16,11 +16,11 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
import asyncio import asyncio
import os
import logging
import click import click
import bumble.core import bumble.core
import bumble.logging
from bumble.colors import color from bumble.colors import color
from bumble.device import Device, Peer from bumble.device import Device, Peer
from bumble.gatt import show_services 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, Dump the GATT database on a remote device. If ADDRESS_OR_NAME is not specified,
wait for an incoming connection. 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)) asyncio.run(async_main(device_config, encrypt, transport, address_or_name))

View File

@@ -16,20 +16,19 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
import asyncio import asyncio
import os
import struct import struct
import logging
import click import click
import bumble.logging
from bumble import l2cap from bumble import l2cap
from bumble.colors import color from bumble.colors import color
from bumble.device import Device, Peer
from bumble.core import AdvertisingData from bumble.core import AdvertisingData
from bumble.gatt import Service, Characteristic, CharacteristicValue from bumble.device import Device, Peer
from bumble.utils import AsyncRunner from bumble.gatt import Characteristic, CharacteristicValue, Service
from bumble.transport import open_transport
from bumble.hci import HCI_Constant from bumble.hci import HCI_Constant
from bumble.transport import open_transport
from bumble.utils import AsyncRunner
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Constants # Constants
@@ -383,6 +382,7 @@ def main(
receive_host, receive_host,
receive_port, receive_port,
): ):
bumble.logging.setup_basic_logging('WARNING')
asyncio.run( asyncio.run(
run( run(
hci_transport, hci_transport,
@@ -397,6 +397,5 @@ def main(
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View File

@@ -12,14 +12,15 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import asyncio
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
import logging import logging
import asyncio
import os
import sys import sys
import bumble.logging
from bumble import hci, transport from bumble import hci, transport
from bumble.bridge import HCI_Bridge from bumble.bridge import HCI_Bridge
@@ -100,7 +101,7 @@ async def async_main():
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def main(): def main():
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper()) bumble.logging.setup_basic_logging()
asyncio.run(async_main()) asyncio.run(async_main())

View File

@@ -16,16 +16,16 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
import asyncio import asyncio
import logging
import os
import click import click
import bumble.logging
from bumble import l2cap from bumble import l2cap
from bumble.colors import color from bumble.colors import color
from bumble.transport import open_transport
from bumble.device import Device from bumble.device import Device
from bumble.utils import FlowControlAsyncPipe
from bumble.hci import HCI_Constant 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__': if __name__ == '__main__':
bumble.logging.setup_basic_logging('WARNING')
cli(obj={}) # pylint: disable=no-value-for-parameter cli(obj={}) # pylint: disable=no-value-for-parameter

View File

@@ -20,31 +20,30 @@ from __future__ import annotations
import asyncio import asyncio
import datetime import datetime
import functools import functools
from importlib import resources
import json import json
import os
import logging import logging
import pathlib import pathlib
import weakref
import wave import wave
import weakref
from importlib import resources
try: try:
import lc3 # type: ignore # pylint: disable=E0401 import lc3 # type: ignore # pylint: disable=E0401
except ImportError as e: except ImportError as e:
raise ImportError("Try `python -m pip install \".[lc3]\"`.") from e raise ImportError("Try `python -m pip install \".[lc3]\"`.") from e
import click
import aiohttp.web import aiohttp.web
import click
import bumble import bumble
from bumble import utils import bumble.logging
from bumble.core import AdvertisingData from bumble import data_types, utils
from bumble.colors import color from bumble.colors import color
from bumble.device import Device, DeviceConfiguration, AdvertisingParameters, CisLink from bumble.core import AdvertisingData
from bumble.transport import open_transport from bumble.device import AdvertisingParameters, CisLink, Device, DeviceConfiguration
from bumble.profiles import ascs, bap, pacs
from bumble.hci import Address, CodecID, CodingFormat, HCI_IsoDataPacket from bumble.hci import Address, CodecID, CodingFormat, HCI_IsoDataPacket
from bumble.profiles import ascs, bap, pacs
from bumble.transport import open_transport
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Logging # Logging
@@ -331,22 +330,13 @@ class Speaker:
advertising_data = bytes( advertising_data = bytes(
AdvertisingData( AdvertisingData(
[ [
( data_types.CompleteLocalName(device_config.name),
AdvertisingData.COMPLETE_LOCAL_NAME, data_types.Flags(
bytes(device_config.name, 'utf-8'), AdvertisingData.Flags.LE_GENERAL_DISCOVERABLE_MODE
| AdvertisingData.Flags.BR_EDR_NOT_SUPPORTED
), ),
( data_types.IncompleteListOf16BitServiceUUIDs(
AdvertisingData.FLAGS, [pacs.PublishedAudioCapabilitiesService.UUID]
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),
), ),
] ]
) )
@@ -454,7 +444,7 @@ def speaker(ui_port: int, device_config: str, transport: str, lc3_file: str) ->
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def main(): def main():
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper()) bumble.logging.setup_basic_logging()
speaker() speaker()

View File

@@ -16,42 +16,44 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
import asyncio import asyncio
import os
import logging import logging
import os
import struct import struct
import click import click
from prompt_toolkit.shortcuts import PromptSession from prompt_toolkit.shortcuts import PromptSession
from bumble import data_types
from bumble.a2dp import make_audio_sink_service_sdp_records 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.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 ( from bumble.core import (
UUID,
AdvertisingData, AdvertisingData,
Appearance, Appearance,
ProtocolError, DataType,
PhysicalTransport, PhysicalTransport,
UUID, ProtocolError,
) )
from bumble.device import Device, Peer
from bumble.gatt import ( from bumble.gatt import (
GATT_DEVICE_NAME_CHARACTERISTIC, GATT_DEVICE_NAME_CHARACTERISTIC,
GATT_GENERIC_ACCESS_SERVICE, GATT_GENERIC_ACCESS_SERVICE,
GATT_HEART_RATE_SERVICE,
GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC, GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC,
Service, GATT_HEART_RATE_SERVICE,
Characteristic, Characteristic,
Service,
) )
from bumble.hci import OwnAddressType from bumble.hci import OwnAddressType
from bumble.att import ( from bumble.keys import JsonKeyStore
ATT_Error, from bumble.pairing import OobData, PairingConfig, PairingDelegate
ATT_INSUFFICIENT_AUTHENTICATION_ERROR, from bumble.smp import OobContext, OobLegacyContext
ATT_INSUFFICIENT_ENCRYPTION_ERROR, from bumble.smp import error_name as smp_error_name
) from bumble.transport import open_transport
from bumble.utils import AsyncRunner from bumble.utils import AsyncRunner
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -506,33 +508,21 @@ async def pair(
if mode == 'dual': if mode == 'dual':
flags |= AdvertisingData.Flags.SIMULTANEOUS_LE_BR_EDR_CAPABLE flags |= AdvertisingData.Flags.SIMULTANEOUS_LE_BR_EDR_CAPABLE
ad_structs = [ advertising_data_types: list[DataType] = [
( data_types.Flags(flags),
AdvertisingData.FLAGS, data_types.CompleteLocalName('Bumble'),
bytes([flags]),
),
(AdvertisingData.COMPLETE_LOCAL_NAME, 'Bumble'.encode()),
] ]
if service_uuids_16: if service_uuids_16:
ad_structs.append( advertising_data_types.append(
( data_types.IncompleteListOf16BitServiceUUIDs(service_uuids_16)
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
b"".join(bytes(uuid) for uuid in service_uuids_16),
)
) )
if service_uuids_32: if service_uuids_32:
ad_structs.append( advertising_data_types.append(
( data_types.IncompleteListOf32BitServiceUUIDs(service_uuids_32)
AdvertisingData.INCOMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS,
b"".join(bytes(uuid) for uuid in service_uuids_32),
)
) )
if service_uuids_128: if service_uuids_128:
ad_structs.append( advertising_data_types.append(
( data_types.IncompleteListOf128BitServiceUUIDs(service_uuids_128)
AdvertisingData.INCOMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS,
b"".join(bytes(uuid) for uuid in service_uuids_128),
)
) )
if advertise_appearance: if advertise_appearance:
@@ -559,13 +549,10 @@ async def pair(
advertise_appearance_int = int( advertise_appearance_int = int(
Appearance(category_enum, subcategory_enum) Appearance(category_enum, subcategory_enum)
) )
ad_structs.append( advertising_data_types.append(
( data_types.Appearance(category_enum, subcategory_enum)
AdvertisingData.APPEARANCE,
struct.pack('<H', advertise_appearance_int),
)
) )
device.advertising_data = bytes(AdvertisingData(ad_structs)) device.advertising_data = bytes(AdvertisingData(advertising_data_types))
await device.start_advertising( await device.start_advertising(
auto_restart=True, auto_restart=True,
own_address_type=( own_address_type=(

View File

@@ -1,11 +1,12 @@
import asyncio import asyncio
import click
import logging
import json import json
import logging
from bumble.pandora import PandoraDevice, Config, serve
from typing import Any from typing import Any
import click
from bumble.pandora import Config, PandoraDevice, serve
BUMBLE_SERVER_GRPC_PORT = 7999 BUMBLE_SERVER_GRPC_PORT = 7999
ROOTCANAL_PORT_CUTTLEFISH = 7300 ROOTCANAL_PORT_CUTTLEFISH = 7300

View File

@@ -16,55 +16,51 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import asyncio.subprocess
import os
import logging import logging
from typing import Optional, Union from typing import Optional, Union
import click import click
import bumble.logging
from bumble.a2dp import ( from bumble.a2dp import (
make_audio_source_service_sdp_records,
A2DP_SBC_CODEC_TYPE,
A2DP_MPEG_2_4_AAC_CODEC_TYPE, A2DP_MPEG_2_4_AAC_CODEC_TYPE,
A2DP_NON_A2DP_CODEC_TYPE, A2DP_NON_A2DP_CODEC_TYPE,
A2DP_SBC_CODEC_TYPE,
AacFrame, AacFrame,
AacParser,
AacPacketSource,
AacMediaCodecInformation, AacMediaCodecInformation,
SbcFrame, AacPacketSource,
SbcParser, AacParser,
SbcPacketSource,
SbcMediaCodecInformation,
OpusPacket,
OpusParser,
OpusPacketSource,
OpusMediaCodecInformation, 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 ( from bumble.avdtp import (
find_avdtp_service_with_connection,
AVDTP_AUDIO_MEDIA_TYPE, AVDTP_AUDIO_MEDIA_TYPE,
AVDTP_DELAY_REPORTING_SERVICE_CATEGORY, AVDTP_DELAY_REPORTING_SERVICE_CATEGORY,
MediaCodecCapabilities, MediaCodecCapabilities,
MediaPacketPump, 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.colors import color
from bumble.core import ( from bumble.core import AdvertisingData
AdvertisingData, from bumble.core import ConnectionError as BumbleConnectionError
ConnectionError as BumbleConnectionError, from bumble.core import DeviceClass, PhysicalTransport
DeviceClass,
PhysicalTransport,
)
from bumble.device import Connection, Device, DeviceConfiguration from bumble.device import Connection, Device, DeviceConfiguration
from bumble.hci import Address, HCI_CONNECTION_ALREADY_EXISTS_ERROR, HCI_Constant from bumble.hci import HCI_CONNECTION_ALREADY_EXISTS_ERROR, Address, HCI_Constant
from bumble.pairing import PairingConfig from bumble.pairing import PairingConfig
from bumble.transport import open_transport from bumble.transport import open_transport
from bumble.utils import AsyncRunner from bumble.utils import AsyncRunner
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Logging # Logging
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -599,7 +595,7 @@ def play(context, address, audio_format, audio_file):
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def main(): def main():
logging.basicConfig(level=os.environ.get("BUMBLE_LOGLEVEL", "WARNING").upper()) bumble.logging.setup_basic_logging("WARNING")
player_cli() player_cli()

View File

@@ -16,21 +16,15 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
import asyncio import asyncio
import logging
import os
import time import time
from typing import Optional from typing import Optional
import click import click
import bumble.logging
from bumble import core, hci, rfcomm, transport, utils
from bumble.colors import color from bumble.colors import color
from bumble.device import Device, DeviceConfiguration, Connection from bumble.device import Connection, Device, DeviceConfiguration
from bumble import core
from bumble import hci
from bumble import rfcomm
from bumble import transport
from bumble import utils
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Constants # 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__": if __name__ == "__main__":
bumble.logging.setup_basic_logging("WARNING")
cli(obj={}) # pylint: disable=no-value-for-parameter cli(obj={}) # pylint: disable=no-value-for-parameter

View File

@@ -16,17 +16,17 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
import asyncio import asyncio
import os
import logging
import click import click
import bumble.logging
from bumble import data_types
from bumble.colors import color from bumble.colors import color
from bumble.device import Device from bumble.device import Advertisement, Device
from bumble.transport import open_transport from bumble.hci import HCI_LE_1M_PHY, HCI_LE_CODED_PHY, Address, HCI_Constant
from bumble.keys import JsonKeyStore from bumble.keys import JsonKeyStore
from bumble.smp import AddressResolver from bumble.smp import AddressResolver
from bumble.device import Advertisement from bumble.transport import open_transport
from bumble.hci import Address, HCI_Constant, HCI_LE_1M_PHY, HCI_LE_CODED_PHY
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -95,13 +95,22 @@ class AdvertisementPrinter:
else: else:
phy_info = '' 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( print(
f'>>> {color(address, address_color)} ' f'>>> {color(address, address_color)} '
f'[{color(address_type_string, type_color)}]{address_qualifier}' f'[{color(address_type_string, type_color)}]{address_qualifier}'
f'{resolution_qualifier}:{separator}' f'{resolution_qualifier}:{separator}'
f'{phy_info}' f'{phy_info}'
f'RSSI:{advertisement.rssi:4} {rssi_bar}{separator}' f'RSSI:{advertisement.rssi:4} {rssi_bar}{separator}'
f'{advertisement.data.to_string(separator)}\n' f'{details}\n'
) )
def on_advertisement(self, advertisement): def on_advertisement(self, advertisement):
@@ -237,7 +246,7 @@ def main(
device_config, device_config,
transport, transport,
): ):
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper()) bumble.logging.setup_basic_logging('WARNING')
asyncio.run( asyncio.run(
scan( scan(
min_rssi, min_rssi,

View File

@@ -18,16 +18,15 @@
import datetime import datetime
import importlib import importlib
import logging import logging
import os
import struct import struct
import click import click
from bumble.colors import color import bumble.logging
from bumble import hci from bumble import hci
from bumble.transport.common import PacketReader from bumble.colors import color
from bumble.helpers import PacketTracer from bumble.helpers import PacketTracer
from bumble.transport.common import PacketReader
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Logging # Logging
@@ -188,5 +187,5 @@ def main(format, vendor, filename):
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
if __name__ == '__main__': 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 main() # pylint: disable=no-value-for-parameter

View File

@@ -16,49 +16,49 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import asyncio.subprocess import asyncio.subprocess
from importlib import resources
import enum import enum
import json import json
import os
import logging import logging
import pathlib import pathlib
import subprocess import subprocess
from typing import Optional
import weakref import weakref
from importlib import resources
from typing import Optional
import click
import aiohttp import aiohttp
import click
from aiohttp import web from aiohttp import web
import bumble import bumble
from bumble.colors import color import bumble.logging
from bumble.core import PhysicalTransport, CommandTimeoutError from bumble.a2dp import (
from bumble.device import Connection, Device, DeviceConfiguration A2DP_MPEG_2_4_AAC_CODEC_TYPE,
from bumble.hci import HCI_StatusError A2DP_NON_A2DP_CODEC_TYPE,
from bumble.pairing import PairingConfig A2DP_SBC_CODEC_TYPE,
from bumble.sdp import ServiceAttribute AacMediaCodecInformation,
from bumble.transport import open_transport OpusMediaCodecInformation,
SbcMediaCodecInformation,
make_audio_sink_service_sdp_records,
)
from bumble.avdtp import ( from bumble.avdtp import (
AVDTP_AUDIO_MEDIA_TYPE, AVDTP_AUDIO_MEDIA_TYPE,
Listener, Listener,
MediaCodecCapabilities, MediaCodecCapabilities,
Protocol, 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.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.rtp import MediaPacket
from bumble.sdp import ServiceAttribute
from bumble.transport import open_transport
from bumble.utils import AsyncRunner
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Logging # Logging
@@ -833,11 +833,7 @@ def speaker(
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def main(): def main():
logging.basicConfig( bumble.logging.setup_basic_logging('WARNING')
level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper(),
format="[%(asctime)s.%(msecs)03d] %(levelname)s:%(name)s:%(message)s",
datefmt="%H:%M:%S",
)
speaker() speaker()

View File

@@ -16,10 +16,10 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
import asyncio import asyncio
import os
import logging
import click import click
import bumble.logging
from bumble.device import Device from bumble.device import Device
from bumble.keys import JsonKeyStore from bumble.keys import JsonKeyStore
from bumble.transport import open_transport from bumble.transport import open_transport
@@ -68,7 +68,7 @@ def main(keystore_file, hci_transport, device_config, address):
instantiated. instantiated.
If no address is passed, the existing pairing keys for all addresses are printed. 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: if not keystore_file and not hci_transport:
print('either --keystore-file or --hci-transport must be specified.') print('either --keystore-file or --hci-transport must be specified.')

View File

@@ -26,15 +26,13 @@
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
import os
import logging
import click import click
import usb1 import usb1
import bumble.logging
from bumble.colors import color from bumble.colors import color
from bumble.transport.usb import load_libusb from bumble.transport.usb import load_libusb
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Constants # Constants
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -169,7 +167,7 @@ def is_bluetooth_hci(device):
@click.command() @click.command()
@click.option('--verbose', is_flag=True, default=False, help='Print more details') @click.option('--verbose', is_flag=True, default=False, help='Print more details')
def main(verbose): def main(verbose):
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper()) bumble.logging.setup_basic_logging('WARNING')
load_libusb() load_libusb()
with usb1.USBContext() as context: with usb1.USBContext() as context:

View File

@@ -17,37 +17,36 @@
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
from collections.abc import AsyncGenerator
import dataclasses import dataclasses
import enum import enum
import logging import logging
import struct import struct
from collections.abc import AsyncGenerator
from typing import Awaitable, Callable from typing import Awaitable, Callable
from typing_extensions import ClassVar, Self
from typing_extensions import ClassVar, Self
from bumble.codecs import AacAudioRtpPacket from bumble.codecs import AacAudioRtpPacket
from bumble.company_ids import COMPANY_IDENTIFIERS 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 ( 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_ADVANCED_AUDIO_DISTRIBUTION_SERVICE,
BT_AUDIO_SINK_SERVICE,
BT_AUDIO_SOURCE_SERVICE,
BT_AVDTP_PROTOCOL_ID,
BT_L2CAP_PROTOCOL_ID,
name_or_number, name_or_number,
) )
from bumble.rtp import MediaPacket 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 # Logging

View File

@@ -24,24 +24,26 @@
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
import dataclasses
import enum import enum
import functools import functools
import inspect import inspect
import struct import struct
from typing import ( from typing import (
TYPE_CHECKING,
Awaitable, Awaitable,
Callable, Callable,
ClassVar,
Generic, Generic,
Optional,
TypeVar, TypeVar,
Union, Union,
TYPE_CHECKING,
) )
from bumble import hci, utils
from bumble import utils
from bumble.core import UUID, name_or_number, InvalidOperationError, ProtocolError
from bumble.hci import HCI_Object, key_with_value
from bumble.colors import color from bumble.colors import color
from bumble.core import UUID, InvalidOperationError, ProtocolError
from bumble.hci import HCI_Object
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Typing # Typing
@@ -60,96 +62,66 @@ _T = TypeVar('_T')
ATT_CID = 0x04 ATT_CID = 0x04
ATT_PSM = 0x001F ATT_PSM = 0x001F
ATT_ERROR_RESPONSE = 0x01 class Opcode(hci.SpecableEnum):
ATT_EXCHANGE_MTU_REQUEST = 0x02 ATT_ERROR_RESPONSE = 0x01
ATT_EXCHANGE_MTU_RESPONSE = 0x03 ATT_EXCHANGE_MTU_REQUEST = 0x02
ATT_FIND_INFORMATION_REQUEST = 0x04 ATT_EXCHANGE_MTU_RESPONSE = 0x03
ATT_FIND_INFORMATION_RESPONSE = 0x05 ATT_FIND_INFORMATION_REQUEST = 0x04
ATT_FIND_BY_TYPE_VALUE_REQUEST = 0x06 ATT_FIND_INFORMATION_RESPONSE = 0x05
ATT_FIND_BY_TYPE_VALUE_RESPONSE = 0x07 ATT_FIND_BY_TYPE_VALUE_REQUEST = 0x06
ATT_READ_BY_TYPE_REQUEST = 0x08 ATT_FIND_BY_TYPE_VALUE_RESPONSE = 0x07
ATT_READ_BY_TYPE_RESPONSE = 0x09 ATT_READ_BY_TYPE_REQUEST = 0x08
ATT_READ_REQUEST = 0x0A ATT_READ_BY_TYPE_RESPONSE = 0x09
ATT_READ_RESPONSE = 0x0B ATT_READ_REQUEST = 0x0A
ATT_READ_BLOB_REQUEST = 0x0C ATT_READ_RESPONSE = 0x0B
ATT_READ_BLOB_RESPONSE = 0x0D ATT_READ_BLOB_REQUEST = 0x0C
ATT_READ_MULTIPLE_REQUEST = 0x0E ATT_READ_BLOB_RESPONSE = 0x0D
ATT_READ_MULTIPLE_RESPONSE = 0x0F ATT_READ_MULTIPLE_REQUEST = 0x0E
ATT_READ_BY_GROUP_TYPE_REQUEST = 0x10 ATT_READ_MULTIPLE_RESPONSE = 0x0F
ATT_READ_BY_GROUP_TYPE_RESPONSE = 0x11 ATT_READ_BY_GROUP_TYPE_REQUEST = 0x10
ATT_WRITE_REQUEST = 0x12 ATT_READ_BY_GROUP_TYPE_RESPONSE = 0x11
ATT_WRITE_RESPONSE = 0x13 ATT_WRITE_REQUEST = 0x12
ATT_WRITE_COMMAND = 0x52 ATT_WRITE_RESPONSE = 0x13
ATT_SIGNED_WRITE_COMMAND = 0xD2 ATT_WRITE_COMMAND = 0x52
ATT_PREPARE_WRITE_REQUEST = 0x16 ATT_SIGNED_WRITE_COMMAND = 0xD2
ATT_PREPARE_WRITE_RESPONSE = 0x17 ATT_PREPARE_WRITE_REQUEST = 0x16
ATT_EXECUTE_WRITE_REQUEST = 0x18 ATT_PREPARE_WRITE_RESPONSE = 0x17
ATT_EXECUTE_WRITE_RESPONSE = 0x19 ATT_EXECUTE_WRITE_REQUEST = 0x18
ATT_HANDLE_VALUE_NOTIFICATION = 0x1B ATT_EXECUTE_WRITE_RESPONSE = 0x19
ATT_HANDLE_VALUE_INDICATION = 0x1D ATT_HANDLE_VALUE_NOTIFICATION = 0x1B
ATT_HANDLE_VALUE_CONFIRMATION = 0x1E ATT_HANDLE_VALUE_INDICATION = 0x1D
ATT_HANDLE_VALUE_CONFIRMATION = 0x1E
ATT_PDU_NAMES = {
ATT_ERROR_RESPONSE: 'ATT_ERROR_RESPONSE',
ATT_EXCHANGE_MTU_REQUEST: 'ATT_EXCHANGE_MTU_REQUEST',
ATT_EXCHANGE_MTU_RESPONSE: 'ATT_EXCHANGE_MTU_RESPONSE',
ATT_FIND_INFORMATION_REQUEST: 'ATT_FIND_INFORMATION_REQUEST',
ATT_FIND_INFORMATION_RESPONSE: 'ATT_FIND_INFORMATION_RESPONSE',
ATT_FIND_BY_TYPE_VALUE_REQUEST: 'ATT_FIND_BY_TYPE_VALUE_REQUEST',
ATT_FIND_BY_TYPE_VALUE_RESPONSE: 'ATT_FIND_BY_TYPE_VALUE_RESPONSE',
ATT_READ_BY_TYPE_REQUEST: 'ATT_READ_BY_TYPE_REQUEST',
ATT_READ_BY_TYPE_RESPONSE: 'ATT_READ_BY_TYPE_RESPONSE',
ATT_READ_REQUEST: 'ATT_READ_REQUEST',
ATT_READ_RESPONSE: 'ATT_READ_RESPONSE',
ATT_READ_BLOB_REQUEST: 'ATT_READ_BLOB_REQUEST',
ATT_READ_BLOB_RESPONSE: 'ATT_READ_BLOB_RESPONSE',
ATT_READ_MULTIPLE_REQUEST: 'ATT_READ_MULTIPLE_REQUEST',
ATT_READ_MULTIPLE_RESPONSE: 'ATT_READ_MULTIPLE_RESPONSE',
ATT_READ_BY_GROUP_TYPE_REQUEST: 'ATT_READ_BY_GROUP_TYPE_REQUEST',
ATT_READ_BY_GROUP_TYPE_RESPONSE: 'ATT_READ_BY_GROUP_TYPE_RESPONSE',
ATT_WRITE_REQUEST: 'ATT_WRITE_REQUEST',
ATT_WRITE_RESPONSE: 'ATT_WRITE_RESPONSE',
ATT_WRITE_COMMAND: 'ATT_WRITE_COMMAND',
ATT_SIGNED_WRITE_COMMAND: 'ATT_SIGNED_WRITE_COMMAND',
ATT_PREPARE_WRITE_REQUEST: 'ATT_PREPARE_WRITE_REQUEST',
ATT_PREPARE_WRITE_RESPONSE: 'ATT_PREPARE_WRITE_RESPONSE',
ATT_EXECUTE_WRITE_REQUEST: 'ATT_EXECUTE_WRITE_REQUEST',
ATT_EXECUTE_WRITE_RESPONSE: 'ATT_EXECUTE_WRITE_RESPONSE',
ATT_HANDLE_VALUE_NOTIFICATION: 'ATT_HANDLE_VALUE_NOTIFICATION',
ATT_HANDLE_VALUE_INDICATION: 'ATT_HANDLE_VALUE_INDICATION',
ATT_HANDLE_VALUE_CONFIRMATION: 'ATT_HANDLE_VALUE_CONFIRMATION'
}
ATT_REQUESTS = [ ATT_REQUESTS = [
ATT_EXCHANGE_MTU_REQUEST, Opcode.ATT_EXCHANGE_MTU_REQUEST,
ATT_FIND_INFORMATION_REQUEST, Opcode.ATT_FIND_INFORMATION_REQUEST,
ATT_FIND_BY_TYPE_VALUE_REQUEST, Opcode.ATT_FIND_BY_TYPE_VALUE_REQUEST,
ATT_READ_BY_TYPE_REQUEST, Opcode.ATT_READ_BY_TYPE_REQUEST,
ATT_READ_REQUEST, Opcode.ATT_READ_REQUEST,
ATT_READ_BLOB_REQUEST, Opcode.ATT_READ_BLOB_REQUEST,
ATT_READ_MULTIPLE_REQUEST, Opcode.ATT_READ_MULTIPLE_REQUEST,
ATT_READ_BY_GROUP_TYPE_REQUEST, Opcode.ATT_READ_BY_GROUP_TYPE_REQUEST,
ATT_WRITE_REQUEST, Opcode.ATT_WRITE_REQUEST,
ATT_PREPARE_WRITE_REQUEST, Opcode.ATT_PREPARE_WRITE_REQUEST,
ATT_EXECUTE_WRITE_REQUEST Opcode.ATT_EXECUTE_WRITE_REQUEST
] ]
ATT_RESPONSES = [ ATT_RESPONSES = [
ATT_ERROR_RESPONSE, Opcode.ATT_ERROR_RESPONSE,
ATT_EXCHANGE_MTU_RESPONSE, Opcode.ATT_EXCHANGE_MTU_RESPONSE,
ATT_FIND_INFORMATION_RESPONSE, Opcode.ATT_FIND_INFORMATION_RESPONSE,
ATT_FIND_BY_TYPE_VALUE_RESPONSE, Opcode.ATT_FIND_BY_TYPE_VALUE_RESPONSE,
ATT_READ_BY_TYPE_RESPONSE, Opcode.ATT_READ_BY_TYPE_RESPONSE,
ATT_READ_RESPONSE, Opcode.ATT_READ_RESPONSE,
ATT_READ_BLOB_RESPONSE, Opcode.ATT_READ_BLOB_RESPONSE,
ATT_READ_MULTIPLE_RESPONSE, Opcode.ATT_READ_MULTIPLE_RESPONSE,
ATT_READ_BY_GROUP_TYPE_RESPONSE, Opcode.ATT_READ_BY_GROUP_TYPE_RESPONSE,
ATT_WRITE_RESPONSE, Opcode.ATT_WRITE_RESPONSE,
ATT_PREPARE_WRITE_RESPONSE, Opcode.ATT_PREPARE_WRITE_RESPONSE,
ATT_EXECUTE_WRITE_RESPONSE Opcode.ATT_EXECUTE_WRITE_RESPONSE
] ]
class ErrorCode(utils.OpenIntEnum): class ErrorCode(hci.SpecableEnum):
''' '''
See See
@@ -204,10 +176,6 @@ ATT_INSUFFICIENT_RESOURCES_ERROR = ErrorCode.INSUFFICIENT_RESOURCES
ATT_DEFAULT_MTU = 23 ATT_DEFAULT_MTU = 23
HANDLE_FIELD_SPEC = {'size': 2, 'mapper': lambda x: f'0x{x:04X}'} HANDLE_FIELD_SPEC = {'size': 2, 'mapper': lambda x: f'0x{x:04X}'}
# 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 # fmt: on
# pylint: enable=line-too-long # pylint: enable=line-too-long
@@ -227,7 +195,7 @@ class ATT_Error(ProtocolError):
super().__init__( super().__init__(
error_code, error_code,
error_namespace='att', error_namespace='att',
error_name=ATT_PDU.error_name(error_code), error_name=ErrorCode(error_code).name,
) )
self.att_handle = att_handle self.att_handle = att_handle
self.message = message self.message = message
@@ -242,61 +210,45 @@ class ATT_Error(ProtocolError):
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Attribute Protocol # Attribute Protocol
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@dataclasses.dataclass
class ATT_PDU: class ATT_PDU:
''' '''
See Bluetooth spec @ Vol 3, Part F - 3.3 ATTRIBUTE PDU See Bluetooth spec @ Vol 3, Part F - 3.3 ATTRIBUTE PDU
''' '''
pdu_classes: dict[int, type[ATT_PDU]] = {} pdu_classes: ClassVar[dict[int, type[ATT_PDU]]] = {}
op_code = 0 fields: ClassVar[hci.Fields] = ()
name: str op_code: int = dataclasses.field(init=False)
name: str = dataclasses.field(init=False)
@staticmethod _payload: Optional[bytes] = dataclasses.field(default=None, init=False)
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)
@classmethod @classmethod
def error_name(cls, error_code: int) -> str: def from_bytes(cls, pdu: bytes) -> ATT_PDU:
return ErrorCode(error_code).name op_code = pdu[0]
@staticmethod subclass = ATT_PDU.pdu_classes.get(op_code)
def subclass(fields): if subclass is None:
def inner(cls): instance = ATT_PDU()
cls.name = cls.__name__.upper() instance.op_code = op_code
cls.op_code = key_with_value(ATT_PDU_NAMES, cls.name) instance.payload = pdu[1:]
if cls.op_code is None: instance.name = Opcode(op_code).name
raise KeyError(f'PDU name {cls.name} not found in ATT_PDU_NAMES') return instance
cls.fields = fields instance = subclass(**HCI_Object.dict_from_bytes(pdu, 1, subclass.fields))
instance.payload = pdu[1:]
return instance
# Register a factory for this class _PDU = TypeVar("_PDU", bound="ATT_PDU")
ATT_PDU.pdu_classes[cls.op_code] = cls
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): return subclass
if hasattr(self, 'fields') and kwargs:
HCI_Object.init_from_fields(self, self.fields, kwargs)
if pdu is None:
pdu = bytes([self.op_code]) + HCI_Object.dict_to_bytes(kwargs, self.fields)
self.pdu = pdu
def init_from_bytes(self, pdu, offset): def init_from_bytes(self, pdu, offset):
return HCI_Object.init_from_bytes(self, pdu, offset, self.fields) return HCI_Object.init_from_bytes(self, pdu, offset, self.fields)
@@ -309,67 +261,91 @@ class ATT_PDU:
def has_authentication_signature(self): def has_authentication_signature(self):
return ((self.op_code >> 7) & 1) == 1 return ((self.op_code >> 7) & 1) == 1
def __bytes__(self): @property
return self.pdu 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): def __str__(self):
result = color(self.name, 'yellow') result = color(self.name, 'yellow')
if fields := getattr(self, 'fields', None): if fields := getattr(self, 'fields', None):
result += ':\n' + HCI_Object.format_fields(self.__dict__, fields, ' ') result += ':\n' + HCI_Object.format_fields(self.__dict__, fields, ' ')
else: else:
if len(self.pdu) > 1: if self.payload:
result += f': {self.pdu.hex()}' result += f': {self.payload.hex()}'
return result return result
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@ATT_PDU.subclass( @ATT_PDU.subclass
[ @dataclasses.dataclass
('request_opcode_in_error', {'size': 1, 'mapper': ATT_PDU.pdu_name}),
('attribute_handle_in_error', HANDLE_FIELD_SPEC),
('error_code', {'size': 1, 'mapper': ATT_PDU.error_name}),
]
)
class ATT_Error_Response(ATT_PDU): class ATT_Error_Response(ATT_PDU):
''' '''
See Bluetooth spec @ Vol 3, Part F - 3.4.1.1 Error Response 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): class ATT_Exchange_MTU_Request(ATT_PDU):
''' '''
See Bluetooth spec @ Vol 3, Part F - 3.4.2.1 Exchange MTU Request 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): class ATT_Exchange_MTU_Response(ATT_PDU):
''' '''
See Bluetooth spec @ Vol 3, Part F - 3.4.2.2 Exchange MTU Response 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( @ATT_PDU.subclass
[('starting_handle', HANDLE_FIELD_SPEC), ('ending_handle', HANDLE_FIELD_SPEC)] @dataclasses.dataclass
)
class ATT_Find_Information_Request(ATT_PDU): class ATT_Find_Information_Request(ATT_PDU):
''' '''
See Bluetooth spec @ Vol 3, Part F - 3.4.3.1 Find Information Request 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): class ATT_Find_Information_Response(ATT_PDU):
''' '''
See Bluetooth spec @ Vol 3, Part F - 3.4.3.2 Find Information Response 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 = [] self.information = []
offset = 0 offset = 0
uuid_size = 2 if self.format == 1 else 16 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)) self.information.append((handle, uuid))
offset += 2 + uuid_size 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): def __str__(self):
result = color(self.name, 'yellow') result = color(self.name, 'yellow')
result += ':\n' + HCI_Object.format_fields( result += ':\n' + HCI_Object.format_fields(
@@ -408,28 +376,31 @@ class ATT_Find_Information_Response(ATT_PDU):
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@ATT_PDU.subclass( @ATT_PDU.subclass
[ @dataclasses.dataclass
('starting_handle', HANDLE_FIELD_SPEC),
('ending_handle', HANDLE_FIELD_SPEC),
('attribute_type', UUID_2_FIELD_SPEC),
('attribute_value', '*'),
]
)
class ATT_Find_By_Type_Value_Request(ATT_PDU): 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 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): 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 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 = [] self.handles_information = []
offset = 0 offset = 0
while offset + 4 <= len(self.handles_information_list): 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)) self.handles_information.append((found_attribute_handle, group_end_handle))
offset += 4 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): def __str__(self):
result = color(self.name, 'yellow') result = color(self.name, 'yellow')
result += ':\n' + HCI_Object.format_fields( result += ':\n' + HCI_Object.format_fields(
@@ -470,27 +433,31 @@ class ATT_Find_By_Type_Value_Response(ATT_PDU):
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@ATT_PDU.subclass( @ATT_PDU.subclass
[ @dataclasses.dataclass
('starting_handle', HANDLE_FIELD_SPEC),
('ending_handle', HANDLE_FIELD_SPEC),
('attribute_type', UUID_2_16_FIELD_SPEC),
]
)
class ATT_Read_By_Type_Request(ATT_PDU): class ATT_Read_By_Type_Request(ATT_PDU):
''' '''
See Bluetooth spec @ Vol 3, Part F - 3.4.4.1 Read By Type Request See Bluetooth spec @ Vol 3, Part F - 3.4.4.1 Read By Type Request
''' '''
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): class ATT_Read_By_Type_Response(ATT_PDU):
''' '''
See Bluetooth spec @ Vol 3, Part F - 3.4.4.2 Read By Type Response 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 = [] self.attributes = []
offset = 0 offset = 0
while self.length != 0 and offset + self.length <= len( 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)) self.attributes.append((attribute_handle, attribute_value))
offset += self.length 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): def __str__(self):
result = color(self.name, 'yellow') result = color(self.name, 'yellow')
result += ':\n' + HCI_Object.format_fields( 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): class ATT_Read_Request(ATT_PDU):
''' '''
See Bluetooth spec @ Vol 3, Part F - 3.4.4.3 Read Request See Bluetooth spec @ Vol 3, Part F - 3.4.4.3 Read Request
''' '''
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): class ATT_Read_Response(ATT_PDU):
''' '''
See Bluetooth spec @ Vol 3, Part F - 3.4.4.4 Read Response 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): class ATT_Read_Blob_Request(ATT_PDU):
''' '''
See Bluetooth spec @ Vol 3, Part F - 3.4.4.5 Read Blob Request See Bluetooth spec @ Vol 3, Part F - 3.4.4.5 Read Blob Request
''' '''
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): class ATT_Read_Blob_Response(ATT_PDU):
''' '''
See Bluetooth spec @ Vol 3, Part F - 3.4.4.6 Read Blob Response 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): class ATT_Read_Multiple_Request(ATT_PDU):
''' '''
See Bluetooth spec @ Vol 3, Part F - 3.4.4.7 Read Multiple Request 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): class ATT_Read_Multiple_Response(ATT_PDU):
''' '''
See Bluetooth spec @ Vol 3, Part F - 3.4.4.8 Read Multiple Response 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( @ATT_PDU.subclass
[ @dataclasses.dataclass
('starting_handle', HANDLE_FIELD_SPEC),
('ending_handle', HANDLE_FIELD_SPEC),
('attribute_group_type', UUID_2_16_FIELD_SPEC),
]
)
class ATT_Read_By_Group_Type_Request(ATT_PDU): 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 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): 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 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 = [] self.attributes = []
offset = 0 offset = 0
while self.length != 0 and offset + self.length <= len( 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 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): def __str__(self):
result = color(self.name, 'yellow') result = color(self.name, 'yellow')
result += ':\n' + HCI_Object.format_fields( 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): class ATT_Write_Request(ATT_PDU):
''' '''
See Bluetooth spec @ Vol 3, Part F - 3.4.5.1 Write Request See Bluetooth spec @ Vol 3, Part F - 3.4.5.1 Write Request
''' '''
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): class ATT_Write_Response(ATT_PDU):
''' '''
See Bluetooth spec @ Vol 3, Part F - 3.4.5.2 Write Response 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): class ATT_Write_Command(ATT_PDU):
''' '''
See Bluetooth spec @ Vol 3, Part F - 3.4.5.3 Write Command See Bluetooth spec @ Vol 3, Part F - 3.4.5.3 Write Command
''' '''
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
('attribute_handle', HANDLE_FIELD_SPEC),
('attribute_value', '*'),
# ('authentication_signature', 'TODO')
]
)
class ATT_Signed_Write_Command(ATT_PDU): class ATT_Signed_Write_Command(ATT_PDU):
''' '''
See Bluetooth spec @ Vol 3, Part F - 3.4.5.4 Signed Write Command 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( @ATT_PDU.subclass
[ @dataclasses.dataclass
('attribute_handle', HANDLE_FIELD_SPEC),
('value_offset', 2),
('part_attribute_value', '*'),
]
)
class ATT_Prepare_Write_Request(ATT_PDU): class ATT_Prepare_Write_Request(ATT_PDU):
''' '''
See Bluetooth spec @ Vol 3, Part F - 3.4.6.1 Prepare Write Request 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( @ATT_PDU.subclass
[ @dataclasses.dataclass
('attribute_handle', HANDLE_FIELD_SPEC),
('value_offset', 2),
('part_attribute_value', '*'),
]
)
class ATT_Prepare_Write_Response(ATT_PDU): class ATT_Prepare_Write_Response(ATT_PDU):
''' '''
See Bluetooth spec @ Vol 3, Part F - 3.4.6.2 Prepare Write Response 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): class ATT_Execute_Write_Request(ATT_PDU):
''' '''
See Bluetooth spec @ Vol 3, Part F - 3.4.6.3 Execute Write Request 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): class ATT_Execute_Write_Response(ATT_PDU):
''' '''
See Bluetooth spec @ Vol 3, Part F - 3.4.6.4 Execute Write Response 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): class ATT_Handle_Value_Notification(ATT_PDU):
''' '''
See Bluetooth spec @ Vol 3, Part F - 3.4.7.1 Handle Value Notification 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): class ATT_Handle_Value_Indication(ATT_PDU):
''' '''
See Bluetooth spec @ Vol 3, Part F - 3.4.7.2 Handle Value Indication 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): class ATT_Handle_Value_Confirmation(ATT_PDU):
''' '''
See Bluetooth spec @ Vol 3, Part F - 3.4.7.3 Handle Value Confirmation See Bluetooth spec @ Vol 3, Part F - 3.4.7.3 Handle Value Confirmation

View File

@@ -17,20 +17,16 @@
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
import asyncio
import abc import abc
from concurrent.futures import ThreadPoolExecutor import asyncio
import dataclasses import dataclasses
import enum import enum
import logging import logging
import pathlib import pathlib
from typing import (
AsyncGenerator,
BinaryIO,
TYPE_CHECKING,
)
import sys import sys
import wave import wave
from concurrent.futures import ThreadPoolExecutor
from typing import TYPE_CHECKING, AsyncGenerator, BinaryIO
from bumble.colors import color from bumble.colors import color
@@ -230,8 +226,8 @@ class SoundDeviceAudioOutput(ThreadedAudioOutput):
try: try:
self._stream.write(pcm_samples) self._stream.write(pcm_samples)
except Exception as error: except Exception:
print(f'Sound device error: {error}') logger.exception('Sound device error')
raise raise
def _close(self): def _close(self):

View File

@@ -16,12 +16,12 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
import enum import enum
import struct import struct
from typing import Union from typing import Union
from bumble import core from bumble import core, utils
from bumble import utils
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------

View File

@@ -16,15 +16,14 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
from enum import IntEnum
import logging import logging
import struct 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.colors import color
from bumble import avc
from bumble import core
from bumble import l2cap
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Logging # Logging
@@ -137,8 +136,8 @@ class MessageAssembler:
self.pid, self.pid,
self.payload, self.payload,
) )
except Exception as error: except Exception:
logger.exception(color(f"!!! exception in callback: {error}", "red")) logger.exception(color("!!! exception in callback", "red"))
self.reset() self.reset()

View File

@@ -16,31 +16,25 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import time
import logging
import enum import enum
import logging
import time
import warnings import warnings
from typing import ( from typing import (
Any, Any,
Awaitable,
Optional,
Callable,
AsyncGenerator, AsyncGenerator,
Awaitable,
Callable,
Iterable, Iterable,
Union, Optional,
SupportsBytes, SupportsBytes,
Union,
cast, cast,
) )
from bumble import device, l2cap, sdp, utils
from bumble.core import (
BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE,
InvalidStateError,
ProtocolError,
InvalidArgumentError,
name_or_number,
)
from bumble.a2dp import ( from bumble.a2dp import (
A2DP_CODEC_TYPE_NAMES, A2DP_CODEC_TYPE_NAMES,
A2DP_MPEG_2_4_AAC_CODEC_TYPE, A2DP_MPEG_2_4_AAC_CODEC_TYPE,
@@ -51,10 +45,15 @@ from bumble.a2dp import (
SbcMediaCodecInformation, SbcMediaCodecInformation,
VendorSpecificMediaCodecInformation, VendorSpecificMediaCodecInformation,
) )
from bumble.rtp import MediaPacket
from bumble import sdp, device, l2cap, utils
from bumble.colors import color 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 # Logging
@@ -434,8 +433,8 @@ class MessageAssembler:
) )
try: try:
self.callback(self.transaction_label, message) self.callback(self.transaction_label, message)
except Exception as error: except Exception:
logger.exception(color(f'!!! exception in callback: {error}', 'red')) logger.exception(color('!!! exception in callback', 'red'))
self.reset() self.reset()
@@ -1400,10 +1399,8 @@ class Protocol(utils.EventEmitter):
try: try:
response = handler(message) response = handler(message)
self.send_message(transaction_label, response) self.send_message(transaction_label, response)
except Exception as error: except Exception:
logger.warning( logger.exception(color("!!! Exception in handler:", "red"))
f'{color("!!! Exception in handler:", "red")} {error}'
)
else: else:
logger.warning('unhandled command') logger.warning('unhandled command')
else: else:

File diff suppressed because it is too large Load Diff

View File

@@ -16,7 +16,9 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from typing_extensions import Self from typing_extensions import Self
from bumble import core from bumble import core

View File

@@ -17,34 +17,32 @@
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
import logging
import asyncio import asyncio
import dataclasses import dataclasses
import itertools import itertools
import logging
import random import random
import struct import struct
from bumble.colors import color from typing import TYPE_CHECKING, Any, Optional, Union
from bumble.core import (
PhysicalTransport,
)
from bumble import hci from bumble import hci
from bumble.colors import color
from bumble.core import PhysicalTransport
from bumble.hci import ( from bumble.hci import (
HCI_ACL_DATA_PACKET, HCI_ACL_DATA_PACKET,
HCI_COMMAND_DISALLOWED_ERROR, HCI_COMMAND_DISALLOWED_ERROR,
HCI_COMMAND_PACKET, HCI_COMMAND_PACKET,
HCI_COMMAND_STATUS_PENDING, HCI_COMMAND_STATUS_PENDING,
HCI_CONNECTION_TIMEOUT_ERROR,
HCI_CONTROLLER_BUSY_ERROR, HCI_CONTROLLER_BUSY_ERROR,
HCI_EVENT_PACKET, HCI_EVENT_PACKET,
HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR, HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR,
HCI_LE_1M_PHY, HCI_LE_1M_PHY,
HCI_SUCCESS,
HCI_UNKNOWN_HCI_COMMAND_ERROR,
HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR,
HCI_REMOTE_USER_TERMINATED_CONNECTION_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, HCI_VERSION_BLUETOOTH_CORE_5_0,
Address, Address,
Role,
HCI_AclDataPacket, HCI_AclDataPacket,
HCI_AclDataPacketAssembler, HCI_AclDataPacketAssembler,
HCI_Command_Complete_Event, HCI_Command_Complete_Event,
@@ -53,7 +51,6 @@ from bumble.hci import (
HCI_Connection_Request_Event, HCI_Connection_Request_Event,
HCI_Disconnection_Complete_Event, HCI_Disconnection_Complete_Event,
HCI_Encryption_Change_Event, HCI_Encryption_Change_Event,
HCI_Synchronous_Connection_Complete_Event,
HCI_LE_Advertising_Report_Event, HCI_LE_Advertising_Report_Event,
HCI_LE_CIS_Established_Event, HCI_LE_CIS_Established_Event,
HCI_LE_CIS_Request_Event, HCI_LE_CIS_Request_Event,
@@ -62,8 +59,9 @@ from bumble.hci import (
HCI_Number_Of_Completed_Packets_Event, HCI_Number_Of_Completed_Packets_Event,
HCI_Packet, HCI_Packet,
HCI_Role_Change_Event, HCI_Role_Change_Event,
HCI_Synchronous_Connection_Complete_Event,
Role,
) )
from typing import Optional, Union, Any, TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from bumble.link import LocalLink from bumble.link import LocalLink
@@ -89,6 +87,7 @@ class CisLink:
cis_id: int cis_id: int
cig_id: int cig_id: int
acl_connection: Optional[Connection] = None acl_connection: Optional[Connection] = None
data_paths: set[int] = dataclasses.field(default_factory=set)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -370,12 +369,23 @@ class Controller:
return connection return connection
return None 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): def find_classic_connection_by_handle(self, handle):
for connection in self.classic_connections.values(): for connection in self.classic_connections.values():
if connection.handle == handle: if connection.handle == handle:
return connection return connection
return None return None
def find_iso_link_by_handle(self, handle: int) -> Optional[CisLink]:
return self.central_cis_links.get(handle) or self.peripheral_cis_links.get(
handle
)
def on_link_central_connected(self, central_address): def on_link_central_connected(self, central_address):
''' '''
Called when an incoming connection occurs from a central on the link Called when an incoming connection occurs from a central on the link
@@ -414,7 +424,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 Called when an active disconnection occurs from a peer
''' '''
@@ -431,6 +441,17 @@ class Controller:
# Remove the connection # Remove the connection
del self.peripheral_connections[peer_address] 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: else:
logger.warning(f'!!! No peripheral connection found for {peer_address}') logger.warning(f'!!! No peripheral connection found for {peer_address}')
@@ -479,7 +500,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 Called when a disconnection has been completed
''' '''
@@ -499,26 +520,11 @@ class Controller:
): ):
logger.debug(f'CENTRAL Connection removed: {connection}') logger.debug(f'CENTRAL Connection removed: {connection}')
del self.central_connections[connection.peer_address] del self.central_connections[connection.peer_address]
elif connection := self.find_peripheral_connection_by_handle(
def on_link_peripheral_disconnected(self, peer_address): disconnection_command.connection_handle
''' ):
Called when a connection to a peripheral is broken logger.debug(f'PERIPHERAL Connection removed: {connection}')
''' del self.peripheral_connections[connection.peer_address]
# 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}')
def on_link_encrypted(self, peer_address, _rand, _ediv, _ltk): def on_link_encrypted(self, peer_address, _rand, _ediv, _ltk):
# For now, just setup the encryption without asking the host # For now, just setup the encryption without asking the host
@@ -877,6 +883,14 @@ class Controller:
else: else:
# Remove the connection # Remove the connection
del self.central_connections[connection.peer_address] 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): elif connection := self.find_classic_connection_by_handle(handle):
if self.link: if self.link:
self.link.classic_disconnect( self.link.classic_disconnect(
@@ -1259,6 +1273,56 @@ class Controller:
) )
return bytes([HCI_SUCCESS]) + bd_addr 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): 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 See Bluetooth spec Vol 4, Part E - 7.8.1 LE Set Event Mask Command
@@ -1794,14 +1858,57 @@ class Controller:
) )
) )
def on_hci_le_setup_iso_data_path_command(self, command): def on_hci_le_setup_iso_data_path_command(
self, command: hci.HCI_LE_Setup_ISO_Data_Path_Command
) -> bytes:
''' '''
See Bluetooth spec Vol 4, Part E - 7.8.109 LE Setup ISO Data Path Command See Bluetooth spec Vol 4, Part E - 7.8.109 LE Setup ISO Data Path Command
''' '''
if not (iso_link := self.find_iso_link_by_handle(command.connection_handle)):
return struct.pack(
'<BH',
HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR,
command.connection_handle,
)
if command.data_path_direction in iso_link.data_paths:
return struct.pack(
'<BH',
HCI_COMMAND_DISALLOWED_ERROR,
command.connection_handle,
)
iso_link.data_paths.add(command.data_path_direction)
return struct.pack('<BH', HCI_SUCCESS, command.connection_handle) return struct.pack('<BH', HCI_SUCCESS, command.connection_handle)
def on_hci_le_remove_iso_data_path_command(self, command): def on_hci_le_remove_iso_data_path_command(
self, command: hci.HCI_LE_Remove_ISO_Data_Path_Command
) -> bytes:
''' '''
See Bluetooth spec Vol 4, Part E - 7.8.110 LE Remove ISO Data Path Command See Bluetooth spec Vol 4, Part E - 7.8.110 LE Remove ISO Data Path Command
''' '''
if not (iso_link := self.find_iso_link_by_handle(command.connection_handle)):
return struct.pack(
'<BH',
HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR,
command.connection_handle,
)
data_paths: set[int] = set(
direction
for direction in hci.HCI_LE_Setup_ISO_Data_Path_Command.Direction
if (1 << direction) & command.data_path_direction
)
if not data_paths.issubset(iso_link.data_paths):
return struct.pack(
'<BH',
HCI_COMMAND_DISALLOWED_ERROR,
command.connection_handle,
)
iso_link.data_paths.difference_update(data_paths)
return struct.pack('<BH', HCI_SUCCESS, command.connection_handle) 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])

File diff suppressed because it is too large Load Diff

View File

@@ -22,12 +22,12 @@ import operator
import secrets import secrets
try: try:
from bumble.crypto.cryptography import EccKey, e, aes_cmac from bumble.crypto.cryptography import EccKey, aes_cmac, e
except ImportError: except ImportError:
logging.getLogger(__name__).debug( logging.getLogger(__name__).debug(
"Unable to import cryptography, use built-in primitives." "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]
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------

View File

@@ -24,9 +24,9 @@
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
import copy
import dataclasses import dataclasses
import functools import functools
import copy
import secrets import secrets
import struct import struct
from typing import Optional from typing import Optional

View File

@@ -16,11 +16,9 @@ from __future__ import annotations
import functools import functools
from cryptography.hazmat.primitives import ciphers from cryptography.hazmat.primitives import ciphers, cmac
from cryptography.hazmat.primitives.ciphers import algorithms
from cryptography.hazmat.primitives.ciphers import modes
from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import cmac from cryptography.hazmat.primitives.ciphers import algorithms, modes
def e(key: bytes, data: bytes) -> bytes: def e(key: bytes, data: bytes) -> bytes:

1025
bumble/data_types.py Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -20,12 +20,13 @@ like loading firmware after a cold start.
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
import logging import logging
import pathlib import pathlib
import platform 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 from bumble.drivers.common import Driver
if TYPE_CHECKING: if TYPE_CHECKING:

View File

@@ -20,6 +20,7 @@ Loosely based on the Fuchsia OS implementation.
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import collections import collections
import dataclasses import dataclasses
@@ -28,12 +29,10 @@ import os
import pathlib import pathlib
import platform import platform
import struct 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.drivers import common
from bumble import hci
from bumble import utils
if TYPE_CHECKING: if TYPE_CHECKING:
from bumble.host import Host from bumble.host import Host

View File

@@ -17,10 +17,6 @@ Based on various online bits of information, including the Linux kernel.
(see `drivers/bluetooth/btrtl.c`) (see `drivers/bluetooth/btrtl.c`)
""" """
# -----------------------------------------------------------------------------
# Imports
# -----------------------------------------------------------------------------
from dataclasses import dataclass, field
import asyncio import asyncio
import enum import enum
import logging import logging
@@ -31,9 +27,12 @@ import platform
import struct import struct
import weakref import weakref
# -----------------------------------------------------------------------------
# Imports
# -----------------------------------------------------------------------------
from dataclasses import dataclass, field
from bumble import core from bumble import core, hci
from bumble import hci
from bumble.drivers import common from bumble.drivers import common
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -489,6 +488,21 @@ class Driver(common.Driver):
return True 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 @classmethod
async def driver_info_for_host(cls, host): async def driver_info_for_host(cls, host):
try: try:
@@ -592,7 +606,7 @@ class Driver(common.Driver):
) )
if response.return_parameters.status != hci.HCI_SUCCESS: if response.return_parameters.status != hci.HCI_SUCCESS:
logger.warning("can't get ROM version") logger.warning("can't get ROM version")
return return None
rom_version = response.return_parameters.version rom_version = response.return_parameters.version
logger.debug(f"ROM version before download: {rom_version:04X}") logger.debug(f"ROM version before download: {rom_version:04X}")
else: else:
@@ -600,13 +614,14 @@ class Driver(common.Driver):
firmware = Firmware(self.firmware) firmware = Firmware(self.firmware)
logger.debug(f"firmware: project_id=0x{firmware.project_id:04X}") 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: for patch in firmware.patches:
if patch[0] == rom_version + 1: if patch[0] == rom_version + 1:
logger.debug(f"using patch {patch[0]}") logger.debug(f"using patch {patch[0]}")
break break
else: else:
logger.warning("no valid patch found for rom version {rom_version}") logger.warning("no valid patch found for rom version {rom_version}")
return return None
# Append the config if there is one. # Append the config if there is one.
if self.config: if self.config:
@@ -642,7 +657,9 @@ class Driver(common.Driver):
logger.warning("can't get ROM version") logger.warning("can't get ROM version")
else: else:
rom_version = response.return_parameters.version 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): async def download_firmware(self):
if self.driver_info.rom == RTK_ROM_LMP_8723A: if self.driver_info.rom == RTK_ROM_LMP_8723A:

View File

@@ -19,11 +19,11 @@ import logging
import struct import struct
from bumble.gatt import ( from bumble.gatt import (
Service,
Characteristic,
GATT_GENERIC_ACCESS_SERVICE,
GATT_DEVICE_NAME_CHARACTERISTIC,
GATT_APPEARANCE_CHARACTERISTIC, GATT_APPEARANCE_CHARACTERISTIC,
GATT_DEVICE_NAME_CHARACTERISTIC,
GATT_GENERIC_ACCESS_SERVICE,
Characteristic,
Service,
) )
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------

View File

@@ -23,15 +23,16 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
import enum import enum
import functools import functools
import logging import logging
import struct import struct
from typing import Iterable, Optional, Sequence, TypeVar, Union 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.att import Attribute, AttributeValue
from bumble.colors import color
from bumble.core import UUID, BaseBumbleError
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Typing # Typing

View File

@@ -20,22 +20,14 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations 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.core import InvalidOperationError
from bumble.gatt import Characteristic from bumble.gatt import Characteristic
from bumble.gatt_client import CharacteristicProxy from bumble.gatt_client import CharacteristicProxy
from bumble import utils
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Typing # Typing

View File

@@ -24,60 +24,38 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import logging import logging
import struct import struct
from datetime import datetime from datetime import datetime
from typing import ( from typing import (
TYPE_CHECKING,
Any, Any,
Callable, Callable,
Generic, Generic,
Iterable, Iterable,
Optional, Optional,
Union,
TypeVar, TypeVar,
TYPE_CHECKING, Union,
) )
from bumble import att, core, utils
from bumble.colors import color 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.core import UUID, InvalidStateError
from bumble.gatt import ( from bumble.gatt import (
GATT_CHARACTERISTIC_ATTRIBUTE_TYPE, GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR, GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
GATT_INCLUDE_ATTRIBUTE_TYPE,
GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE, GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
GATT_REQUEST_TIMEOUT, GATT_REQUEST_TIMEOUT,
GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE, GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE,
GATT_INCLUDE_ATTRIBUTE_TYPE,
Characteristic, Characteristic,
ClientCharacteristicConfigurationBits, ClientCharacteristicConfigurationBits,
InvalidServiceError, InvalidServiceError,
TemplateService, TemplateService,
) )
from bumble.hci import HCI_Constant
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Typing # Typing
@@ -291,8 +269,8 @@ class Client:
indication_subscribers: dict[ indication_subscribers: dict[
int, set[Union[CharacteristicProxy, Callable[[bytes], Any]]] int, set[Union[CharacteristicProxy, Callable[[bytes], Any]]]
] ]
pending_response: Optional[asyncio.futures.Future[ATT_PDU]] pending_response: Optional[asyncio.futures.Future[att.ATT_PDU]]
pending_request: Optional[ATT_PDU] pending_request: Optional[att.ATT_PDU]
def __init__(self, connection: Connection) -> None: def __init__(self, connection: Connection) -> None:
self.connection = connection self.connection = connection
@@ -308,15 +286,15 @@ class Client:
connection.on(connection.EVENT_DISCONNECTION, self.on_disconnection) connection.on(connection.EVENT_DISCONNECTION, self.on_disconnection)
def send_gatt_pdu(self, pdu: bytes) -> None: def send_gatt_pdu(self, pdu: bytes) -> None:
self.connection.send_l2cap_pdu(ATT_CID, pdu) self.connection.send_l2cap_pdu(att.ATT_CID, pdu)
async def send_command(self, command: ATT_PDU) -> None: async def send_command(self, command: att.ATT_PDU) -> None:
logger.debug( logger.debug(
f'GATT Command from client: [0x{self.connection.handle:04X}] {command}' f'GATT Command from client: [0x{self.connection.handle:04X}] {command}'
) )
self.send_gatt_pdu(bytes(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( logger.debug(
f'GATT Request from client: [0x{self.connection.handle:04X}] {request}' f'GATT Request from client: [0x{self.connection.handle:04X}] {request}'
) )
@@ -345,7 +323,9 @@ class Client:
return response 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( logger.debug(
f'GATT Confirmation from client: [0x{self.connection.handle:04X}] ' f'GATT Confirmation from client: [0x{self.connection.handle:04X}] '
f'{confirmation}' f'{confirmation}'
@@ -354,8 +334,8 @@ class Client:
async def request_mtu(self, mtu: int) -> int: async def request_mtu(self, mtu: int) -> int:
# Check the range # Check the range
if mtu < ATT_DEFAULT_MTU: if mtu < att.ATT_DEFAULT_MTU:
raise core.InvalidArgumentError(f'MTU must be >= {ATT_DEFAULT_MTU}') raise core.InvalidArgumentError(f'MTU must be >= {att.ATT_DEFAULT_MTU}')
if mtu > 0xFFFF: if mtu > 0xFFFF:
raise core.InvalidArgumentError('MTU must be <= 0xFFFF') raise core.InvalidArgumentError('MTU must be <= 0xFFFF')
@@ -365,9 +345,11 @@ class Client:
# Send the request # Send the request
self.mtu_exchange_done = True self.mtu_exchange_done = True
response = await self.send_request(ATT_Exchange_MTU_Request(client_rx_mtu=mtu)) response = await self.send_request(
if response.op_code == ATT_ERROR_RESPONSE: att.ATT_Exchange_MTU_Request(client_rx_mtu=mtu)
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)
# Compute the final MTU # Compute the final MTU
self.connection.att_mtu = min(mtu, response.server_rx_mtu) self.connection.att_mtu = min(mtu, response.server_rx_mtu)
@@ -432,7 +414,7 @@ class Client:
services = [] services = []
while starting_handle < 0xFFFF: while starting_handle < 0xFFFF:
response = await self.send_request( response = await self.send_request(
ATT_Read_By_Group_Type_Request( att.ATT_Read_By_Group_Type_Request(
starting_handle=starting_handle, starting_handle=starting_handle,
ending_handle=0xFFFF, ending_handle=0xFFFF,
attribute_group_type=GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE, attribute_group_type=GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
@@ -443,14 +425,14 @@ class Client:
return [] return []
# Check if we reached the end of the iteration # Check if we reached the end of the iteration
if response.op_code == ATT_ERROR_RESPONSE: if response.op_code == att.Opcode.ATT_ERROR_RESPONSE:
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR: if response.error_code != att.ATT_ATTRIBUTE_NOT_FOUND_ERROR:
# Unexpected end # Unexpected end
logger.warning( logger.warning(
'!!! unexpected error while discovering services: ' '!!! unexpected error while discovering services: '
f'{HCI_Constant.error_name(response.error_code)}' f'{HCI_Constant.error_name(response.error_code)}'
) )
raise ATT_Error( raise att.ATT_Error(
error_code=response.error_code, error_code=response.error_code,
message='Unexpected error while discovering services', message='Unexpected error while discovering services',
) )
@@ -509,7 +491,7 @@ class Client:
services = [] services = []
while starting_handle < 0xFFFF: while starting_handle < 0xFFFF:
response = await self.send_request( response = await self.send_request(
ATT_Find_By_Type_Value_Request( att.ATT_Find_By_Type_Value_Request(
starting_handle=starting_handle, starting_handle=starting_handle,
ending_handle=0xFFFF, ending_handle=0xFFFF,
attribute_type=GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE, attribute_type=GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
@@ -521,8 +503,8 @@ class Client:
return [] return []
# Check if we reached the end of the iteration # Check if we reached the end of the iteration
if response.op_code == ATT_ERROR_RESPONSE: if response.op_code == att.Opcode.ATT_ERROR_RESPONSE:
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR: if response.error_code != att.ATT_ATTRIBUTE_NOT_FOUND_ERROR:
# Unexpected end # Unexpected end
logger.warning( logger.warning(
'!!! unexpected error while discovering services: ' '!!! unexpected error while discovering services: '
@@ -578,7 +560,7 @@ class Client:
included_services: list[ServiceProxy] = [] included_services: list[ServiceProxy] = []
while starting_handle <= ending_handle: while starting_handle <= ending_handle:
response = await self.send_request( response = await self.send_request(
ATT_Read_By_Type_Request( att.ATT_Read_By_Type_Request(
starting_handle=starting_handle, starting_handle=starting_handle,
ending_handle=ending_handle, ending_handle=ending_handle,
attribute_type=GATT_INCLUDE_ATTRIBUTE_TYPE, attribute_type=GATT_INCLUDE_ATTRIBUTE_TYPE,
@@ -589,14 +571,14 @@ class Client:
return [] return []
# Check if we reached the end of the iteration # Check if we reached the end of the iteration
if response.op_code == ATT_ERROR_RESPONSE: if response.op_code == att.Opcode.ATT_ERROR_RESPONSE:
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR: if response.error_code != att.ATT_ATTRIBUTE_NOT_FOUND_ERROR:
# Unexpected end # Unexpected end
logger.warning( logger.warning(
'!!! unexpected error while discovering included services: ' '!!! unexpected error while discovering included services: '
f'{HCI_Constant.error_name(response.error_code)}' f'{HCI_Constant.error_name(response.error_code)}'
) )
raise ATT_Error( raise att.ATT_Error(
error_code=response.error_code, error_code=response.error_code,
message='Unexpected error while discovering included services', message='Unexpected error while discovering included services',
) )
@@ -652,7 +634,7 @@ class Client:
characteristics: list[CharacteristicProxy[bytes]] = [] characteristics: list[CharacteristicProxy[bytes]] = []
while starting_handle <= ending_handle: while starting_handle <= ending_handle:
response = await self.send_request( response = await self.send_request(
ATT_Read_By_Type_Request( att.ATT_Read_By_Type_Request(
starting_handle=starting_handle, starting_handle=starting_handle,
ending_handle=ending_handle, ending_handle=ending_handle,
attribute_type=GATT_CHARACTERISTIC_ATTRIBUTE_TYPE, attribute_type=GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
@@ -663,14 +645,14 @@ class Client:
return [] return []
# Check if we reached the end of the iteration # Check if we reached the end of the iteration
if response.op_code == ATT_ERROR_RESPONSE: if response.op_code == att.Opcode.ATT_ERROR_RESPONSE:
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR: if response.error_code != att.ATT_ATTRIBUTE_NOT_FOUND_ERROR:
# Unexpected end # Unexpected end
logger.warning( logger.warning(
'!!! unexpected error while discovering characteristics: ' '!!! unexpected error while discovering characteristics: '
f'{HCI_Constant.error_name(response.error_code)}' f'{HCI_Constant.error_name(response.error_code)}'
) )
raise ATT_Error( raise att.ATT_Error(
error_code=response.error_code, error_code=response.error_code,
message='Unexpected error while discovering characteristics', message='Unexpected error while discovering characteristics',
) )
@@ -736,7 +718,7 @@ class Client:
descriptors: list[DescriptorProxy] = [] descriptors: list[DescriptorProxy] = []
while starting_handle <= ending_handle: while starting_handle <= ending_handle:
response = await self.send_request( response = await self.send_request(
ATT_Find_Information_Request( att.ATT_Find_Information_Request(
starting_handle=starting_handle, ending_handle=ending_handle starting_handle=starting_handle, ending_handle=ending_handle
) )
) )
@@ -745,8 +727,8 @@ class Client:
return [] return []
# Check if we reached the end of the iteration # Check if we reached the end of the iteration
if response.op_code == ATT_ERROR_RESPONSE: if response.op_code == att.Opcode.ATT_ERROR_RESPONSE:
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR: if response.error_code != att.ATT_ATTRIBUTE_NOT_FOUND_ERROR:
# Unexpected end # Unexpected end
logger.warning( logger.warning(
'!!! unexpected error while discovering descriptors: ' '!!! unexpected error while discovering descriptors: '
@@ -791,7 +773,7 @@ class Client:
attributes = [] attributes = []
while True: while True:
response = await self.send_request( response = await self.send_request(
ATT_Find_Information_Request( att.ATT_Find_Information_Request(
starting_handle=starting_handle, ending_handle=ending_handle starting_handle=starting_handle, ending_handle=ending_handle
) )
) )
@@ -799,8 +781,8 @@ class Client:
return [] return []
# Check if we reached the end of the iteration # Check if we reached the end of the iteration
if response.op_code == ATT_ERROR_RESPONSE: if response.op_code == att.Opcode.ATT_ERROR_RESPONSE:
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR: if response.error_code != att.ATT_ATTRIBUTE_NOT_FOUND_ERROR:
# Unexpected end # Unexpected end
logger.warning( logger.warning(
'!!! unexpected error while discovering attributes: ' '!!! unexpected error while discovering attributes: '
@@ -954,12 +936,12 @@ class Client:
# Send a request to read # Send a request to read
attribute_handle = attribute if isinstance(attribute, int) else attribute.handle attribute_handle = attribute if isinstance(attribute, int) else attribute.handle
response = await self.send_request( response = await self.send_request(
ATT_Read_Request(attribute_handle=attribute_handle) att.ATT_Read_Request(attribute_handle=attribute_handle)
) )
if response is None: if response is None:
raise TimeoutError('read timeout') raise TimeoutError('read timeout')
if response.op_code == ATT_ERROR_RESPONSE: if response.op_code == att.Opcode.ATT_ERROR_RESPONSE:
raise ATT_Error(error_code=response.error_code, message=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 # If the value is the max size for the MTU, try to read more unless the caller
# specifically asked not to do that # specifically asked not to do that
@@ -969,19 +951,21 @@ class Client:
offset = len(attribute_value) offset = len(attribute_value)
while True: while True:
response = await self.send_request( response = await self.send_request(
ATT_Read_Blob_Request( att.ATT_Read_Blob_Request(
attribute_handle=attribute_handle, value_offset=offset attribute_handle=attribute_handle, value_offset=offset
) )
) )
if response is None: if response is None:
raise TimeoutError('read timeout') raise TimeoutError('read timeout')
if response.op_code == ATT_ERROR_RESPONSE: if response.op_code == att.Opcode.ATT_ERROR_RESPONSE:
if response.error_code in ( if response.error_code in (
ATT_ATTRIBUTE_NOT_LONG_ERROR, att.ATT_ATTRIBUTE_NOT_LONG_ERROR,
ATT_INVALID_OFFSET_ERROR, att.ATT_INVALID_OFFSET_ERROR,
): ):
break 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 part = response.part_attribute_value
attribute_value += part attribute_value += part
@@ -1012,7 +996,7 @@ class Client:
characteristics_values = [] characteristics_values = []
while starting_handle <= ending_handle: while starting_handle <= ending_handle:
response = await self.send_request( response = await self.send_request(
ATT_Read_By_Type_Request( att.ATT_Read_By_Type_Request(
starting_handle=starting_handle, starting_handle=starting_handle,
ending_handle=ending_handle, ending_handle=ending_handle,
attribute_type=uuid, attribute_type=uuid,
@@ -1023,8 +1007,8 @@ class Client:
return [] return []
# Check if we reached the end of the iteration # Check if we reached the end of the iteration
if response.op_code == ATT_ERROR_RESPONSE: if response.op_code == att.Opcode.ATT_ERROR_RESPONSE:
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR: if response.error_code != att.ATT_ATTRIBUTE_NOT_FOUND_ERROR:
# Unexpected end # Unexpected end
logger.warning( logger.warning(
'!!! unexpected error while reading characteristics: ' '!!! unexpected error while reading characteristics: '
@@ -1069,15 +1053,15 @@ class Client:
attribute_handle = attribute if isinstance(attribute, int) else attribute.handle attribute_handle = attribute if isinstance(attribute, int) else attribute.handle
if with_response: if with_response:
response = await self.send_request( response = await self.send_request(
ATT_Write_Request( att.ATT_Write_Request(
attribute_handle=attribute_handle, attribute_value=value attribute_handle=attribute_handle, attribute_value=value
) )
) )
if response.op_code == ATT_ERROR_RESPONSE: if response.op_code == att.Opcode.ATT_ERROR_RESPONSE:
raise ATT_Error(error_code=response.error_code, message=response) raise att.ATT_Error(error_code=response.error_code, message=response)
else: else:
await self.send_command( await self.send_command(
ATT_Write_Command( att.ATT_Write_Command(
attribute_handle=attribute_handle, attribute_value=value attribute_handle=attribute_handle, attribute_value=value
) )
) )
@@ -1086,11 +1070,11 @@ class Client:
if self.pending_response and not self.pending_response.done(): if self.pending_response and not self.pending_response.done():
self.pending_response.cancel() 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( logger.debug(
f'GATT Response to client: [0x{self.connection.handle:04X}] {att_pdu}' 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: if self.pending_request is None:
# Not expected! # Not expected!
logger.warning('!!! unexpected response, there is no pending request') 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 # The response should match the pending request unless it is
# an error response # 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( expected_response_name = self.pending_request.name.replace(
'_REQUEST', '_RESPONSE' '_REQUEST', '_RESPONSE'
) )
@@ -1126,7 +1110,9 @@ class Client:
+ str(att_pdu) + 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 # Call all subscribers
subscribers = self.notification_subscribers.get( subscribers = self.notification_subscribers.get(
notification.attribute_handle, set() notification.attribute_handle, set()
@@ -1141,7 +1127,9 @@ class Client:
else: else:
subscriber.emit(subscriber.EVENT_UPDATE, notification.attribute_value) subscriber.emit(subscriber.EVENT_UPDATE, notification.attribute_value)
def on_att_handle_value_indication(self, indication): def on_att_handle_value_indication(
self, indication: att.ATT_Handle_Value_Indication
):
# Call all subscribers # Call all subscribers
subscribers = self.indication_subscribers.get( subscribers = self.indication_subscribers.get(
indication.attribute_handle, set() indication.attribute_handle, set()
@@ -1157,7 +1145,7 @@ class Client:
subscriber.emit(subscriber.EVENT_UPDATE, indication.attribute_value) subscriber.emit(subscriber.EVENT_UPDATE, indication.attribute_value)
# Confirm that we received the indication # Confirm that we received the indication
self.send_confirmation(ATT_Handle_Value_Confirmation()) self.send_confirmation(att.ATT_Handle_Value_Confirmation())
def cache_value(self, attribute_handle: int, value: bytes) -> None: def cache_value(self, attribute_handle: int, value: bytes) -> None:
self.cached_values[attribute_handle] = ( self.cached_values[attribute_handle] = (

View File

@@ -13,7 +13,7 @@
# limitations under the License. # limitations under the License.
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# GATT - Generic Attribute Profile # GATT - Generic att.Attribute Profile
# Server # Server
# #
# See Bluetooth spec @ Vol 3, Part G # See Bluetooth spec @ Vol 3, Part G
@@ -24,46 +24,16 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import logging import logging
from collections import defaultdict
import struct import struct
from typing import ( from collections import defaultdict
Iterable, from typing import TYPE_CHECKING, Iterable, Optional, TypeVar
Optional,
TypeVar,
TYPE_CHECKING,
)
from bumble import att, utils
from bumble.colors import color from bumble.colors import color
from bumble.core import UUID 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 ( from bumble.gatt import (
GATT_CHARACTERISTIC_ATTRIBUTE_TYPE, GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR, GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
@@ -74,14 +44,13 @@ from bumble.gatt import (
Characteristic, Characteristic,
CharacteristicDeclaration, CharacteristicDeclaration,
CharacteristicValue, CharacteristicValue,
IncludedServiceDeclaration,
Descriptor, Descriptor,
IncludedServiceDeclaration,
Service, Service,
) )
from bumble import utils
if TYPE_CHECKING: if TYPE_CHECKING:
from bumble.device import Device, Connection from bumble.device import Connection, Device
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Logging # Logging
@@ -99,9 +68,9 @@ GATT_SERVER_DEFAULT_MAX_MTU = 517
# GATT Server # GATT Server
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
class Server(utils.EventEmitter): class Server(utils.EventEmitter):
attributes: list[Attribute] attributes: list[att.Attribute]
services: list[Service] services: list[Service]
attributes_by_handle: dict[int, Attribute] attributes_by_handle: dict[int, att.Attribute]
subscribers: dict[int, dict[int, bytes]] subscribers: dict[int, dict[int, bytes]]
indication_semaphores: defaultdict[int, asyncio.Semaphore] indication_semaphores: defaultdict[int, asyncio.Semaphore]
pending_confirmations: defaultdict[int, Optional[asyncio.futures.Future]] pending_confirmations: defaultdict[int, Optional[asyncio.futures.Future]]
@@ -112,7 +81,7 @@ class Server(utils.EventEmitter):
super().__init__() super().__init__()
self.device = device self.device = device
self.services = [] 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.attributes_by_handle = {} # Map for fast attribute access by handle
self.max_mtu = ( self.max_mtu = (
GATT_SERVER_DEFAULT_MAX_MTU # The max MTU we're willing to negotiate 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)) return "\n".join(map(str, self.attributes))
def send_gatt_pdu(self, connection_handle: int, pdu: bytes) -> None: 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: def next_handle(self) -> int:
return 1 + len(self.attributes) 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 { return {
attribute: data attribute: data
for attribute in self.attributes for attribute in self.attributes
@@ -140,7 +109,7 @@ class Server(utils.EventEmitter):
and (data := attribute.get_advertising_data()) 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) attribute = self.attributes_by_handle.get(handle)
if attribute: if attribute:
return attribute return attribute
@@ -231,7 +200,7 @@ class Server(utils.EventEmitter):
None, None,
) )
def add_attribute(self, attribute: Attribute) -> None: def add_attribute(self, attribute: att.Attribute) -> None:
# Assign a handle to this attribute # Assign a handle to this attribute
attribute.handle = self.next_handle() attribute.handle = self.next_handle()
attribute.end_group_handle = ( attribute.end_group_handle = (
@@ -286,7 +255,7 @@ class Server(utils.EventEmitter):
# pylint: disable=line-too-long # pylint: disable=line-too-long
Descriptor( Descriptor(
GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR, GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
Attribute.READABLE | Attribute.WRITEABLE, att.Attribute.READABLE | att.Attribute.WRITEABLE,
CharacteristicValue( CharacteristicValue(
read=lambda connection, characteristic=characteristic: self.read_cccd( read=lambda connection, characteristic=characteristic: self.read_cccd(
connection, characteristic connection, characteristic
@@ -355,7 +324,7 @@ class Server(utils.EventEmitter):
indicate_enabled, 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( logger.debug(
f'GATT Response from server: [0x{connection.handle:04X}] {response}' f'GATT Response from server: [0x{connection.handle:04X}] {response}'
) )
@@ -364,7 +333,7 @@ class Server(utils.EventEmitter):
async def notify_subscriber( async def notify_subscriber(
self, self,
connection: Connection, connection: Connection,
attribute: Attribute, attribute: att.Attribute,
value: Optional[bytes] = None, value: Optional[bytes] = None,
force: bool = False, force: bool = False,
) -> None: ) -> None:
@@ -396,7 +365,7 @@ class Server(utils.EventEmitter):
value = value[: connection.att_mtu - 3] value = value[: connection.att_mtu - 3]
# Notify # Notify
notification = ATT_Handle_Value_Notification( notification = att.ATT_Handle_Value_Notification(
attribute_handle=attribute.handle, attribute_value=value attribute_handle=attribute.handle, attribute_value=value
) )
logger.debug( logger.debug(
@@ -407,7 +376,7 @@ class Server(utils.EventEmitter):
async def indicate_subscriber( async def indicate_subscriber(
self, self,
connection: Connection, connection: Connection,
attribute: Attribute, attribute: att.Attribute,
value: Optional[bytes] = None, value: Optional[bytes] = None,
force: bool = False, force: bool = False,
) -> None: ) -> None:
@@ -439,7 +408,7 @@ class Server(utils.EventEmitter):
value = value[: connection.att_mtu - 3] value = value[: connection.att_mtu - 3]
# Indicate # Indicate
indication = ATT_Handle_Value_Indication( indication = att.ATT_Handle_Value_Indication(
attribute_handle=attribute.handle, attribute_value=value attribute_handle=attribute.handle, attribute_value=value
) )
logger.debug( logger.debug(
@@ -467,7 +436,7 @@ class Server(utils.EventEmitter):
async def _notify_or_indicate_subscribers( async def _notify_or_indicate_subscribers(
self, self,
indicate: bool, indicate: bool,
attribute: Attribute, attribute: att.Attribute,
value: Optional[bytes] = None, value: Optional[bytes] = None,
force: bool = False, force: bool = False,
) -> None: ) -> None:
@@ -494,7 +463,7 @@ class Server(utils.EventEmitter):
async def notify_subscribers( async def notify_subscribers(
self, self,
attribute: Attribute, attribute: att.Attribute,
value: Optional[bytes] = None, value: Optional[bytes] = None,
force: bool = False, force: bool = False,
): ):
@@ -504,7 +473,7 @@ class Server(utils.EventEmitter):
async def indicate_subscribers( async def indicate_subscribers(
self, self,
attribute: Attribute, attribute: att.Attribute,
value: Optional[bytes] = None, value: Optional[bytes] = None,
force: bool = False, force: bool = False,
): ):
@@ -518,33 +487,33 @@ class Server(utils.EventEmitter):
if connection.handle in self.pending_confirmations: if connection.handle in self.pending_confirmations:
del self.pending_confirmations[connection.handle] 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}') logger.debug(f'GATT Request to server: [0x{connection.handle:04X}] {att_pdu}')
handler_name = f'on_{att_pdu.name.lower()}' handler_name = f'on_{att_pdu.name.lower()}'
handler = getattr(self, handler_name, None) handler = getattr(self, handler_name, None)
if handler is not None: if handler is not None:
try: try:
handler(connection, att_pdu) handler(connection, att_pdu)
except ATT_Error as error: except att.ATT_Error as error:
logger.debug(f'normal exception returned by handler: {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, request_opcode_in_error=att_pdu.op_code,
attribute_handle_in_error=error.att_handle, attribute_handle_in_error=error.att_handle,
error_code=error.error_code, error_code=error.error_code,
) )
self.send_response(connection, response) self.send_response(connection, response)
except Exception as error: except Exception:
logger.warning(f'{color("!!! Exception in handler:", "red")} {error}') logger.exception(color("!!! Exception in handler:", "red"))
response = ATT_Error_Response( response = att.ATT_Error_Response(
request_opcode_in_error=att_pdu.op_code, request_opcode_in_error=att_pdu.op_code,
attribute_handle_in_error=0x0000, attribute_handle_in_error=0x0000,
error_code=ATT_UNLIKELY_ERROR_ERROR, error_code=att.ATT_UNLIKELY_ERROR_ERROR,
) )
self.send_response(connection, response) self.send_response(connection, response)
raise error raise
else: else:
# No specific handler registered # 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 # Invoke the generic handler
self.on_att_request(connection, att_pdu) self.on_att_request(connection, att_pdu)
else: else:
@@ -560,7 +529,7 @@ class Server(utils.EventEmitter):
####################################################### #######################################################
# ATT handlers # 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 Handler for requests without a more specific handler
''' '''
@@ -570,23 +539,25 @@ class Server(utils.EventEmitter):
) )
+ str(pdu) + str(pdu)
) )
response = ATT_Error_Response( response = att.ATT_Error_Response(
request_opcode_in_error=pdu.op_code, request_opcode_in_error=pdu.op_code,
attribute_handle_in_error=0x0000, 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) 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 See Bluetooth spec Vol 3, Part F - 3.4.2.1 Exchange MTU Request
''' '''
self.send_response( 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 # 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) mtu = min(self.max_mtu, request.client_rx_mtu)
# Notify the device # Notify the device
@@ -594,11 +565,14 @@ class Server(utils.EventEmitter):
else: else:
logger.warning('invalid client_rx_mtu received, MTU not changed') 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 See Bluetooth spec Vol 3, Part F - 3.4.3.1 Find Information Request
''' '''
response: att.ATT_PDU
# Check the request parameters # Check the request parameters
if ( if (
request.starting_handle == 0 request.starting_handle == 0
@@ -606,17 +580,17 @@ class Server(utils.EventEmitter):
): ):
self.send_response( self.send_response(
connection, connection,
ATT_Error_Response( att.ATT_Error_Response(
request_opcode_in_error=request.op_code, request_opcode_in_error=request.op_code,
attribute_handle_in_error=request.starting_handle, attribute_handle_in_error=request.starting_handle,
error_code=ATT_INVALID_HANDLE_ERROR, error_code=att.ATT_INVALID_HANDLE_ERROR,
), ),
) )
return return
# Build list of returned attributes # Build list of returned attributes
pdu_space_available = connection.att_mtu - 2 pdu_space_available = connection.att_mtu - 2
attributes = [] attributes: list[att.Attribute] = []
uuid_size = 0 uuid_size = 0
for attribute in ( for attribute in (
attribute attribute
@@ -646,21 +620,23 @@ class Server(utils.EventEmitter):
struct.pack('<H', attribute.handle) + attribute.type.to_pdu_bytes() struct.pack('<H', attribute.handle) + attribute.type.to_pdu_bytes()
for attribute in attributes 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, format=1 if len(attributes[0].type.to_pdu_bytes()) == 2 else 2,
information_data=b''.join(information_data_list), information_data=b''.join(information_data_list),
) )
else: else:
response = ATT_Error_Response( response = att.ATT_Error_Response(
request_opcode_in_error=request.op_code, request_opcode_in_error=request.op_code,
attribute_handle_in_error=request.starting_handle, attribute_handle_in_error=request.starting_handle,
error_code=ATT_ATTRIBUTE_NOT_FOUND_ERROR, error_code=att.ATT_ATTRIBUTE_NOT_FOUND_ERROR,
) )
self.send_response(connection, response) self.send_response(connection, response)
@utils.AsyncRunner.run_in_task() @utils.AsyncRunner.run_in_task()
async def on_att_find_by_type_value_request(self, connection, request): async def on_att_find_by_type_value_request(
self, connection: 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 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 # Build list of returned attributes
pdu_space_available = connection.att_mtu - 2 pdu_space_available = connection.att_mtu - 2
attributes = [] attributes = []
response: att.ATT_PDU
async for attribute in ( async for attribute in (
attribute attribute
for attribute in self.attributes for attribute in self.attributes
@@ -700,33 +677,35 @@ class Server(utils.EventEmitter):
handles_information_list.append( handles_information_list.append(
struct.pack('<HH', attribute.handle, group_end_handle) 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) handles_information_list=b''.join(handles_information_list)
) )
else: else:
response = ATT_Error_Response( response = att.ATT_Error_Response(
request_opcode_in_error=request.op_code, request_opcode_in_error=request.op_code,
attribute_handle_in_error=request.starting_handle, attribute_handle_in_error=request.starting_handle,
error_code=ATT_ATTRIBUTE_NOT_FOUND_ERROR, error_code=att.ATT_ATTRIBUTE_NOT_FOUND_ERROR,
) )
self.send_response(connection, response) self.send_response(connection, response)
@utils.AsyncRunner.run_in_task() @utils.AsyncRunner.run_in_task()
async def on_att_read_by_type_request(self, connection, request): async def on_att_read_by_type_request(
self, connection: Connection, request: att.ATT_Read_By_Type_Request
):
''' '''
See Bluetooth spec Vol 3, Part F - 3.4.4.1 Read By Type Request See Bluetooth spec Vol 3, Part F - 3.4.4.1 Read By Type Request
''' '''
pdu_space_available = connection.att_mtu - 2 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, request_opcode_in_error=request.op_code,
attribute_handle_in_error=request.starting_handle, 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 ( for attribute in (
attribute attribute
for attribute in self.attributes for attribute in self.attributes
@@ -737,11 +716,11 @@ class Server(utils.EventEmitter):
): ):
try: try:
attribute_value = await attribute.read_value(connection) 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 # If the first attribute is unreadable, return an error
# Otherwise return attributes up to this point # Otherwise return attributes up to this point
if not attributes: if not attributes:
response = ATT_Error_Response( response = att.ATT_Error_Response(
request_opcode_in_error=request.op_code, request_opcode_in_error=request.op_code,
attribute_handle_in_error=attribute.handle, attribute_handle_in_error=attribute.handle,
error_code=error.error_code, error_code=error.error_code,
@@ -770,7 +749,7 @@ class Server(utils.EventEmitter):
attribute_data_list = [ attribute_data_list = [
struct.pack('<H', handle) + value for handle, value in attributes 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) length=entry_size, attribute_data_list=b''.join(attribute_data_list)
) )
else: else:
@@ -779,95 +758,104 @@ class Server(utils.EventEmitter):
self.send_response(connection, response) self.send_response(connection, response)
@utils.AsyncRunner.run_in_task() @utils.AsyncRunner.run_in_task()
async def on_att_read_request(self, connection, request): async def on_att_read_request(
self, connection: Connection, request: att.ATT_Read_Request
):
''' '''
See Bluetooth spec Vol 3, Part F - 3.4.4.3 Read Request See Bluetooth spec Vol 3, Part F - 3.4.4.3 Read Request
''' '''
response: att.ATT_PDU
if attribute := self.get_attribute(request.attribute_handle): if attribute := self.get_attribute(request.attribute_handle):
try: try:
value = await attribute.read_value(connection) value = await attribute.read_value(connection)
except ATT_Error as error: except att.ATT_Error as error:
response = ATT_Error_Response( response = att.ATT_Error_Response(
request_opcode_in_error=request.op_code, request_opcode_in_error=request.op_code,
attribute_handle_in_error=request.attribute_handle, attribute_handle_in_error=request.attribute_handle,
error_code=error.error_code, error_code=error.error_code,
) )
else: else:
value_size = min(connection.att_mtu - 1, len(value)) 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: else:
response = ATT_Error_Response( response = att.ATT_Error_Response(
request_opcode_in_error=request.op_code, request_opcode_in_error=request.op_code,
attribute_handle_in_error=request.attribute_handle, 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) self.send_response(connection, response)
@utils.AsyncRunner.run_in_task() @utils.AsyncRunner.run_in_task()
async def on_att_read_blob_request(self, connection, request): async def on_att_read_blob_request(
self, connection: Connection, request: att.ATT_Read_Blob_Request
):
''' '''
See Bluetooth spec Vol 3, Part F - 3.4.4.5 Read Blob Request See Bluetooth spec Vol 3, Part F - 3.4.4.5 Read Blob Request
''' '''
response: att.ATT_PDU
if attribute := self.get_attribute(request.attribute_handle): if attribute := self.get_attribute(request.attribute_handle):
try: try:
value = await attribute.read_value(connection) value = await attribute.read_value(connection)
except ATT_Error as error: except att.ATT_Error as error:
response = ATT_Error_Response( response = att.ATT_Error_Response(
request_opcode_in_error=request.op_code, request_opcode_in_error=request.op_code,
attribute_handle_in_error=request.attribute_handle, attribute_handle_in_error=request.attribute_handle,
error_code=error.error_code, error_code=error.error_code,
) )
else: else:
if request.value_offset > len(value): if request.value_offset > len(value):
response = ATT_Error_Response( response = att.ATT_Error_Response(
request_opcode_in_error=request.op_code, request_opcode_in_error=request.op_code,
attribute_handle_in_error=request.attribute_handle, 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: elif len(value) <= connection.att_mtu - 1:
response = ATT_Error_Response( response = att.ATT_Error_Response(
request_opcode_in_error=request.op_code, request_opcode_in_error=request.op_code,
attribute_handle_in_error=request.attribute_handle, attribute_handle_in_error=request.attribute_handle,
error_code=ATT_ATTRIBUTE_NOT_LONG_ERROR, error_code=att.ATT_ATTRIBUTE_NOT_LONG_ERROR,
) )
else: else:
part_size = min( part_size = min(
connection.att_mtu - 1, len(value) - request.value_offset connection.att_mtu - 1, len(value) - request.value_offset
) )
response = ATT_Read_Blob_Response( response = att.ATT_Read_Blob_Response(
part_attribute_value=value[ part_attribute_value=value[
request.value_offset : request.value_offset + part_size request.value_offset : request.value_offset + part_size
] ]
) )
else: else:
response = ATT_Error_Response( response = att.ATT_Error_Response(
request_opcode_in_error=request.op_code, request_opcode_in_error=request.op_code,
attribute_handle_in_error=request.attribute_handle, 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) self.send_response(connection, response)
@utils.AsyncRunner.run_in_task() @utils.AsyncRunner.run_in_task()
async def on_att_read_by_group_type_request(self, connection, request): async def on_att_read_by_group_type_request(
self, connection: 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 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 ( if request.attribute_group_type not in (
GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE, GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE, GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE,
): ):
response = ATT_Error_Response( response = att.ATT_Error_Response(
request_opcode_in_error=request.op_code, request_opcode_in_error=request.op_code,
attribute_handle_in_error=request.starting_handle, attribute_handle_in_error=request.starting_handle,
error_code=ATT_UNSUPPORTED_GROUP_TYPE_ERROR, error_code=att.ATT_UNSUPPORTED_GROUP_TYPE_ERROR,
) )
self.send_response(connection, response) self.send_response(connection, response)
return return
pdu_space_available = connection.att_mtu - 2 pdu_space_available = connection.att_mtu - 2
attributes = [] attributes: list[tuple[int, int, bytes]] = []
for attribute in ( for attribute in (
attribute attribute
for attribute in self.attributes for attribute in self.attributes
@@ -904,21 +892,23 @@ class Server(utils.EventEmitter):
struct.pack('<HH', handle, end_group_handle) + value struct.pack('<HH', handle, end_group_handle) + value
for handle, end_group_handle, value in attributes 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]), length=len(attribute_data_list[0]),
attribute_data_list=b''.join(attribute_data_list), attribute_data_list=b''.join(attribute_data_list),
) )
else: else:
response = ATT_Error_Response( response = att.ATT_Error_Response(
request_opcode_in_error=request.op_code, request_opcode_in_error=request.op_code,
attribute_handle_in_error=request.starting_handle, attribute_handle_in_error=request.starting_handle,
error_code=ATT_ATTRIBUTE_NOT_FOUND_ERROR, error_code=att.ATT_ATTRIBUTE_NOT_FOUND_ERROR,
) )
self.send_response(connection, response) self.send_response(connection, response)
@utils.AsyncRunner.run_in_task() @utils.AsyncRunner.run_in_task()
async def on_att_write_request(self, connection, request): async def on_att_write_request(
self, connection: Connection, request: att.ATT_Write_Request
):
''' '''
See Bluetooth spec Vol 3, Part F - 3.4.5.1 Write Request See Bluetooth spec Vol 3, Part F - 3.4.5.1 Write Request
''' '''
@@ -928,10 +918,10 @@ class Server(utils.EventEmitter):
if attribute is None: if attribute is None:
self.send_response( self.send_response(
connection, connection,
ATT_Error_Response( att.ATT_Error_Response(
request_opcode_in_error=request.op_code, request_opcode_in_error=request.op_code,
attribute_handle_in_error=request.attribute_handle, attribute_handle_in_error=request.attribute_handle,
error_code=ATT_INVALID_HANDLE_ERROR, error_code=att.ATT_INVALID_HANDLE_ERROR,
), ),
) )
return return
@@ -942,30 +932,33 @@ class Server(utils.EventEmitter):
if len(request.attribute_value) > GATT_MAX_ATTRIBUTE_VALUE_SIZE: if len(request.attribute_value) > GATT_MAX_ATTRIBUTE_VALUE_SIZE:
self.send_response( self.send_response(
connection, connection,
ATT_Error_Response( att.ATT_Error_Response(
request_opcode_in_error=request.op_code, request_opcode_in_error=request.op_code,
attribute_handle_in_error=request.attribute_handle, attribute_handle_in_error=request.attribute_handle,
error_code=ATT_INVALID_ATTRIBUTE_LENGTH_ERROR, error_code=att.ATT_INVALID_ATTRIBUTE_LENGTH_ERROR,
), ),
) )
return return
response: att.ATT_PDU
try: try:
# Accept the value # Accept the value
await attribute.write_value(connection, request.attribute_value) await attribute.write_value(connection, request.attribute_value)
except ATT_Error as error: except att.ATT_Error as error:
response = ATT_Error_Response( response = att.ATT_Error_Response(
request_opcode_in_error=request.op_code, request_opcode_in_error=request.op_code,
attribute_handle_in_error=request.attribute_handle, attribute_handle_in_error=request.attribute_handle,
error_code=error.error_code, error_code=error.error_code,
) )
else: else:
# Done # Done
response = ATT_Write_Response() response = att.ATT_Write_Response()
self.send_response(connection, response) self.send_response(connection, response)
@utils.AsyncRunner.run_in_task() @utils.AsyncRunner.run_in_task()
async def on_att_write_command(self, connection, request): async def on_att_write_command(
self, connection: Connection, request: att.ATT_Write_Command
):
''' '''
See Bluetooth spec Vol 3, Part F - 3.4.5.3 Write Command See Bluetooth spec Vol 3, Part F - 3.4.5.3 Write Command
''' '''
@@ -984,18 +977,25 @@ class Server(utils.EventEmitter):
# Accept the value # Accept the value
try: try:
await attribute.write_value(connection, request.attribute_value) await attribute.write_value(connection, request.attribute_value)
except Exception as error: except Exception:
logger.exception(f'!!! ignoring exception: {error}') 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 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! # Not expected!
logger.warning( logger.warning(
'!!! unexpected confirmation, there is no pending indication' '!!! unexpected confirmation, there is no pending indication'
) )
return return
self.pending_confirmations[connection.handle].set_result(None) pending_confirmation.set_result(None)

View File

@@ -16,19 +16,31 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
import collections import collections
import dataclasses import dataclasses
from dataclasses import field
import enum import enum
import functools import functools
import logging import logging
import secrets import secrets
import struct import struct
from collections.abc import Sequence 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 typing_extensions import Self
from bumble import crypto from bumble import crypto, utils
from bumble.colors import color from bumble.colors import color
from bumble.core import ( from bumble.core import (
DeviceClass, DeviceClass,
@@ -40,8 +52,6 @@ from bumble.core import (
name_or_number, name_or_number,
padded_bytes, padded_bytes,
) )
from bumble import utils
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Logging # Logging
@@ -111,23 +121,57 @@ def phy_list_to_bits(phys: Optional[Iterable[Phy]]) -> int:
class SpecableEnum(utils.OpenIntEnum): class SpecableEnum(utils.OpenIntEnum):
@classmethod @classmethod
def type_spec(cls, size: int): def type_spec(cls, size: int, byteorder: Literal['little', 'big'] = 'little'):
return {'size': size, 'mapper': lambda x: cls(x).name} 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 @classmethod
def type_metadata(cls, size: int, list_begin: bool = False, list_end: bool = False): def type_metadata(
return metadata(cls.type_spec(size), list_begin=list_begin, list_end=list_end) 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): class SpecableFlag(enum.IntFlag):
@classmethod @classmethod
def type_spec(cls, size: int): def type_spec(cls, size: int, byteorder: Literal['little', 'big'] = 'little'):
return {'size': size, 'mapper': lambda x: cls(x).name} 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 @classmethod
def type_metadata(cls, size: int, list_begin: bool = False, list_end: bool = False): def type_metadata(
return metadata(cls.type_spec(size), list_begin=list_begin, list_end=list_end) 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 MONITORING_ADVERTISERS = 64
FRAME_SPACE_UPDATE = 65 FRAME_SPACE_UPDATE = 65
class LeFeatureMask(enum.IntFlag): class LeFeatureMask(utils.CompatibleIntFlag):
LE_ENCRYPTION = 1 << LeFeature.LE_ENCRYPTION LE_ENCRYPTION = 1 << LeFeature.LE_ENCRYPTION
CONNECTION_PARAMETERS_REQUEST_PROCEDURE = 1 << LeFeature.CONNECTION_PARAMETERS_REQUEST_PROCEDURE CONNECTION_PARAMETERS_REQUEST_PROCEDURE = 1 << LeFeature.CONNECTION_PARAMETERS_REQUEST_PROCEDURE
EXTENDED_REJECT_INDICATION = 1 << LeFeature.EXTENDED_REJECT_INDICATION EXTENDED_REJECT_INDICATION = 1 << LeFeature.EXTENDED_REJECT_INDICATION
@@ -1463,7 +1507,7 @@ class LmpFeature(SpecableEnum):
SLOT_AVAILABILITY_MASK = 138 SLOT_AVAILABILITY_MASK = 138
TRAIN_NUDGING = 139 TRAIN_NUDGING = 139
class LmpFeatureMask(enum.IntFlag): class LmpFeatureMask(utils.CompatibleIntFlag):
# Page 0 (Legacy LMP features) # Page 0 (Legacy LMP features)
LMP_3_SLOT_PACKETS = (1 << LmpFeature.LMP_3_SLOT_PACKETS) LMP_3_SLOT_PACKETS = (1 << LmpFeature.LMP_3_SLOT_PACKETS)
LMP_5_SLOT_PACKETS = (1 << LmpFeature.LMP_5_SLOT_PACKETS) LMP_5_SLOT_PACKETS = (1 << LmpFeature.LMP_5_SLOT_PACKETS)
@@ -2135,6 +2179,7 @@ class Address:
if len(address) == 12 + 5: if len(address) == 12 + 5:
# Form with ':' separators # Form with ':' separators
address = address.replace(':', '') address = address.replace(':', '')
self.address_bytes = bytes(reversed(bytes.fromhex(address))) self.address_bytes = bytes(reversed(bytes.fromhex(address)))
if len(self.address_bytes) != 6: if len(self.address_bytes) != 6:
@@ -5257,7 +5302,7 @@ class HCI_LE_BIG_Terminate_Sync_Command(HCI_Command):
return_parameters_fields = [ return_parameters_fields = [
('status', STATUS_SPEC), ('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)) 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 @HCI_Command.command
@dataclasses.dataclass @dataclasses.dataclass
@@ -6390,7 +6466,9 @@ class HCI_LE_Create_BIG_Complete_Event(HCI_LE_Meta_Event):
irc: int = field(metadata=metadata(1)) irc: int = field(metadata=metadata(1))
max_pdu: int = field(metadata=metadata(2)) max_pdu: int = field(metadata=metadata(2))
iso_interval: 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)) irc: int = field(metadata=metadata(1))
max_pdu: int = field(metadata=metadata(2)) max_pdu: int = field(metadata=metadata(2))
iso_interval: 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)) 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 @HCI_LE_Meta_Event.event
@dataclasses.dataclass @dataclasses.dataclass

View File

@@ -17,43 +17,36 @@
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable, MutableMapping
import datetime import datetime
from typing import cast, Any, Optional
import logging import logging
from collections.abc import Callable, MutableMapping
from typing import Any, Optional, cast
from bumble import avc from bumble import avc, avctp, avdtp, avrcp, crypto, rfcomm, sdp
from bumble import avctp
from bumble import avdtp
from bumble import avrcp
from bumble import crypto
from bumble import rfcomm
from bumble import sdp
from bumble.colors import color
from bumble.att import ATT_CID, ATT_PDU from bumble.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.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 ( from bumble.hci import (
Address,
HCI_EVENT_PACKET,
HCI_ACL_DATA_PACKET, HCI_ACL_DATA_PACKET,
HCI_DISCONNECTION_COMPLETE_EVENT, HCI_DISCONNECTION_COMPLETE_EVENT,
HCI_AclDataPacketAssembler, HCI_EVENT_PACKET,
HCI_Packet, Address,
HCI_Event,
HCI_AclDataPacket, HCI_AclDataPacket,
HCI_AclDataPacketAssembler,
HCI_Disconnection_Complete_Event, 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 # Logging

View File

@@ -17,45 +17,34 @@
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
import asyncio
import collections import collections
import collections.abc import collections.abc
import logging
import asyncio
import dataclasses import dataclasses
import enum import enum
import traceback import logging
import re import re
from typing import ( import traceback
Union, from typing import TYPE_CHECKING, Any, ClassVar, Iterable, Optional, Union
Any,
Optional,
ClassVar,
Iterable,
TYPE_CHECKING,
)
from typing_extensions import Self from typing_extensions import Self
from bumble import at from bumble import at, device, rfcomm, sdp, utils
from bumble import device
from bumble import rfcomm
from bumble import sdp
from bumble import utils
from bumble.colors import color from bumble.colors import color
from bumble.core import ( from bumble.core import (
ProtocolError,
BT_GENERIC_AUDIO_SERVICE, BT_GENERIC_AUDIO_SERVICE,
BT_HANDSFREE_SERVICE,
BT_HANDSFREE_AUDIO_GATEWAY_SERVICE, BT_HANDSFREE_AUDIO_GATEWAY_SERVICE,
BT_HANDSFREE_SERVICE,
BT_L2CAP_PROTOCOL_ID, BT_L2CAP_PROTOCOL_ID,
BT_RFCOMM_PROTOCOL_ID, BT_RFCOMM_PROTOCOL_ID,
ProtocolError,
) )
from bumble.hci import ( from bumble.hci import (
HCI_Enhanced_Setup_Synchronous_Connection_Command,
CodingFormat,
CodecID, CodecID,
CodingFormat,
HCI_Enhanced_Setup_Synchronous_Connection_Command,
) )
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Logging # Logging
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------

View File

@@ -16,22 +16,20 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations 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 abc import ABC, abstractmethod
from typing import Optional, Callable from dataclasses import dataclass
from typing import Callable, Optional
from typing_extensions import override from typing_extensions import override
from bumble import l2cap from bumble import device, l2cap, utils
from bumble import device
from bumble import utils
from bumble.core import InvalidStateError, ProtocolError from bumble.core import InvalidStateError, ProtocolError
from bumble.hci import Address from bumble.hci import Address
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Logging # Logging
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -219,33 +217,41 @@ class HID(ABC, utils.EventEmitter):
self.role = role self.role = role
# Register ourselves with the L2CAP channel manager # Register ourselves with the L2CAP channel manager
device.register_l2cap_server(HID_CONTROL_PSM, self.on_l2cap_connection) device.create_l2cap_server(
device.register_l2cap_server(HID_INTERRUPT_PSM, self.on_l2cap_connection) 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) device.on(device.EVENT_CONNECTION, self.on_device_connection)
async def connect_control_channel(self) -> None: async def connect_control_channel(self) -> None:
if not self.connection:
raise InvalidStateError("Connection is not established!")
# Create a new L2CAP connection - control channel # Create a new L2CAP connection - control channel
try: try:
channel = await self.device.l2cap_channel_manager.connect( channel = await self.connection.create_l2cap_channel(
self.connection, HID_CONTROL_PSM l2cap.ClassicChannelSpec(HID_CONTROL_PSM)
) )
channel.sink = self.on_ctrl_pdu channel.sink = self.on_ctrl_pdu
self.l2cap_ctrl_channel = channel self.l2cap_ctrl_channel = channel
except ProtocolError: except ProtocolError:
logging.exception(f'L2CAP connection failed.') logging.exception('L2CAP connection failed.')
raise raise
async def connect_interrupt_channel(self) -> None: async def connect_interrupt_channel(self) -> None:
if not self.connection:
raise InvalidStateError("Connection is not established!")
# Create a new L2CAP connection - interrupt channel # Create a new L2CAP connection - interrupt channel
try: try:
channel = await self.device.l2cap_channel_manager.connect( channel = await self.connection.create_l2cap_channel(
self.connection, HID_INTERRUPT_PSM l2cap.ClassicChannelSpec(HID_CONTROL_PSM)
) )
channel.sink = self.on_intr_pdu channel.sink = self.on_intr_pdu
self.l2cap_intr_channel = channel self.l2cap_intr_channel = channel
except ProtocolError: except ProtocolError:
logging.exception(f'L2CAP connection failed.') logging.exception('L2CAP connection failed.')
raise raise
async def disconnect_interrupt_channel(self) -> None: async def disconnect_interrupt_channel(self) -> None:

View File

@@ -16,33 +16,19 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import collections import collections
import dataclasses import dataclasses
import logging import logging
import struct import struct
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Optional, Union, cast
from typing import ( from bumble import drivers, hci, utils
Any,
Awaitable,
Callable,
Optional,
cast,
TYPE_CHECKING,
)
from bumble.colors import color from bumble.colors import color
from bumble.core import ConnectionPHY, InvalidStateError, PhysicalTransport
from bumble.l2cap import L2CAP_PDU from bumble.l2cap import L2CAP_PDU
from bumble.snoop import Snooper 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 from bumble.transport.common import TransportLostError
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -707,11 +693,9 @@ class Host(utils.EventEmitter):
raise hci.HCI_Error(status) raise hci.HCI_Error(status)
return response return response
except Exception as error: except Exception:
logger.exception( logger.exception(color("!!! Exception while sending command:", "red"))
f'{color("!!! Exception while sending command:", "red")} {error}' raise
)
raise error
finally: finally:
self.pending_command = None self.pending_command = None
self.pending_response = None self.pending_response = None
@@ -918,10 +902,14 @@ class Host(utils.EventEmitter):
def on_l2cap_pdu(self, connection: Connection, cid: int, pdu: bytes) -> None: def on_l2cap_pdu(self, connection: Connection, cid: int, pdu: bytes) -> None:
self.emit('l2cap_pdu', connection.handle, cid, pdu) 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: if self.pending_response:
# Check that it is what we were expecting # Check that it is what we were expecting
if self.pending_command.op_code != event.command_opcode: if self.pending_command is None:
logger.warning('!!! pending_command is None ')
elif self.pending_command.op_code != event.command_opcode:
logger.warning( logger.warning(
'!!! command result mismatch, expected ' '!!! command result mismatch, expected '
f'0x{self.pending_command.op_code:X} but got ' f'0x{self.pending_command.op_code:X} but got '
@@ -935,10 +923,10 @@ class Host(utils.EventEmitter):
############################################################ ############################################################
# HCI handlers # 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")}') 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: if event.command_opcode == 0:
# This is used just for the Num_HCI_Command_Packets field, not related to # This is used just for the Num_HCI_Command_Packets field, not related to
# an actual command # an actual command
@@ -947,7 +935,7 @@ class Host(utils.EventEmitter):
return self.on_command_processed(event) return self.on_command_processed(event)
def on_hci_command_status_event(self, event): def on_hci_command_status_event(self, event: hci.HCI_Command_Status_Event):
return self.on_command_processed(event) return self.on_command_processed(event)
def on_hci_number_of_completed_packets_event( def on_hci_number_of_completed_packets_event(
@@ -967,7 +955,7 @@ class Host(utils.EventEmitter):
) )
# Classic only # 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 # Notify the listeners
self.emit( self.emit(
'connection_request', 'connection_request',
@@ -976,7 +964,14 @@ class Host(utils.EventEmitter):
event.link_type, 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 # Check if this is a cancellation
if event.status == hci.HCI_SUCCESS: if event.status == hci.HCI_SUCCESS:
# Create/update the connection # Create/update the connection
@@ -996,20 +991,16 @@ class Host(utils.EventEmitter):
self.connections[event.connection_handle] = connection self.connections[event.connection_handle] = connection
# Notify the client # Notify the client
connection_parameters = ConnectionParameters(
event.connection_interval,
event.peripheral_latency,
event.supervision_timeout,
)
self.emit( self.emit(
'connection', 'le_connection',
event.connection_handle, event.connection_handle,
PhysicalTransport.LE,
event.peer_address, event.peer_address,
getattr(event, 'local_resolvable_private_address', None), getattr(event, 'local_resolvable_private_address', None),
getattr(event, 'peer_resolvable_private_address', None), getattr(event, 'peer_resolvable_private_address', None),
hci.Role(event.role), hci.Role(event.role),
connection_parameters, event.connection_interval,
event.peripheral_latency,
event.supervision_timeout,
) )
else: else:
logger.debug(f'### CONNECTION FAILED: {event.status}') logger.debug(f'### CONNECTION FAILED: {event.status}')
@@ -1022,15 +1013,25 @@ class Host(utils.EventEmitter):
event.status, 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 # Just use the same implementation as for the non-enhanced event for now
self.on_hci_le_connection_complete_event(event) 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 # Just use the same implementation as for the v1 event for now
self.on_hci_le_enhanced_connection_complete_event(event) 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: if event.status == hci.HCI_SUCCESS:
# Create/update the connection # Create/update the connection
logger.debug( logger.debug(
@@ -1050,14 +1051,9 @@ class Host(utils.EventEmitter):
# Notify the client # Notify the client
self.emit( self.emit(
'connection', 'classic_connection',
event.connection_handle, event.connection_handle,
PhysicalTransport.BR_EDR,
event.bd_addr, event.bd_addr,
None,
None,
None,
None,
) )
else: else:
logger.debug(f'### BR/EDR CONNECTION FAILED: {event.status}') logger.debug(f'### BR/EDR CONNECTION FAILED: {event.status}')
@@ -1070,7 +1066,9 @@ class Host(utils.EventEmitter):
event.status, 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 # Find the connection
handle = event.connection_handle handle = event.connection_handle
if ( if (
@@ -1109,27 +1107,30 @@ class Host(utils.EventEmitter):
# Notify the listeners # Notify the listeners
self.emit('disconnection_failure', handle, event.status) 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: if (connection := self.connections.get(event.connection_handle)) is None:
logger.warning('!!! CONNECTION PARAMETERS UPDATE COMPLETE: unknown handle') logger.warning('!!! CONNECTION PARAMETERS UPDATE COMPLETE: unknown handle')
return return
# Notify the client # Notify the client
if event.status == hci.HCI_SUCCESS: if event.status == hci.HCI_SUCCESS:
connection_parameters = ConnectionParameters( self.emit(
'connection_parameters_update',
connection.handle,
event.connection_interval, event.connection_interval,
event.peripheral_latency, event.peripheral_latency,
event.supervision_timeout, event.supervision_timeout,
) )
self.emit(
'connection_parameters_update', connection.handle, connection_parameters
)
else: else:
self.emit( self.emit(
'connection_parameters_update_failure', connection.handle, event.status '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: if (connection := self.connections.get(event.connection_handle)) is None:
logger.warning('!!! CONNECTION PHY UPDATE COMPLETE: unknown handle') logger.warning('!!! CONNECTION PHY UPDATE COMPLETE: unknown handle')
return return
@@ -1159,7 +1160,9 @@ class Host(utils.EventEmitter):
): ):
self.on_hci_le_advertising_report_event(event) 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( self.emit(
'advertising_set_termination', 'advertising_set_termination',
event.status, event.status,
@@ -1168,7 +1171,9 @@ class Host(utils.EventEmitter):
event.num_completed_extended_advertising_events, 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( self.emit(
'periodic_advertising_sync_establishment', 'periodic_advertising_sync_establishment',
event.status, event.status,
@@ -1180,16 +1185,22 @@ class Host(utils.EventEmitter):
event.advertiser_clock_accuracy, 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) 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) 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) 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( self.emit(
'cis_request', 'cis_request',
event.acl_connection_handle, event.acl_connection_handle,
@@ -1198,10 +1209,12 @@ class Host(utils.EventEmitter):
event.cis_id, 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) self.bigs[event.big_handle] = set(event.connection_handle)
if self.iso_packet_queue is None: 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: for connection_handle in event.connection_handle:
self.bis_links[connection_handle] = IsoLink( self.bis_links[connection_handle] = IsoLink(
@@ -1224,8 +1237,13 @@ class Host(utils.EventEmitter):
event.iso_interval, 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) 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: for connection_handle in event.connection_handle:
self.bis_links[connection_handle] = IsoLink( self.bis_links[connection_handle] = IsoLink(
connection_handle, self.iso_packet_queue connection_handle, self.iso_packet_queue
@@ -1245,15 +1263,19 @@ class Host(utils.EventEmitter):
event.connection_handle, 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.remove_big(event.big_handle)
self.emit('big_sync_lost', event.big_handle, event.reason) 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.remove_big(event.big_handle)
self.emit('big_termination', event.reason, 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( self.emit(
'periodic_advertising_sync_transfer', 'periodic_advertising_sync_transfer',
event.status, event.status,
@@ -1266,7 +1288,9 @@ class Host(utils.EventEmitter):
event.advertiser_clock_accuracy, 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( self.emit(
'periodic_advertising_sync_transfer', 'periodic_advertising_sync_transfer',
event.status, event.status,
@@ -1279,11 +1303,11 @@ class Host(utils.EventEmitter):
event.advertiser_clock_accuracy, 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. # The remaining parameters are unused for now.
if event.status == hci.HCI_SUCCESS: if event.status == hci.HCI_SUCCESS:
if self.iso_packet_queue is None: 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( self.cis_links[event.connection_handle] = IsoLink(
handle=event.connection_handle, packet_queue=self.iso_packet_queue handle=event.connection_handle, packet_queue=self.iso_packet_queue
) )
@@ -1310,7 +1334,9 @@ class Host(utils.EventEmitter):
'cis_establishment_failure', event.connection_handle, event.status '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: if event.connection_handle not in self.connections:
logger.warning('!!! REMOTE CONNECTION PARAMETER REQUEST: unknown handle') logger.warning('!!! REMOTE CONNECTION PARAMETER REQUEST: unknown handle')
return return
@@ -1329,7 +1355,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: if (connection := self.connections.get(event.connection_handle)) is None:
logger.warning('!!! LE LONG TERM KEY REQUEST: unknown handle') logger.warning('!!! LE LONG TERM KEY REQUEST: unknown handle')
return return
@@ -1363,7 +1391,9 @@ class Host(utils.EventEmitter):
asyncio.create_task(send_long_term_key()) 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: if event.status == hci.HCI_SUCCESS:
# Create/update the connection # Create/update the connection
logger.debug( logger.debug(
@@ -1389,7 +1419,9 @@ class Host(utils.EventEmitter):
# Notify the client # Notify the client
self.emit('sco_connection_failure', event.bd_addr, event.status) 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 pass
def on_hci_mode_change_event(self, event: hci.HCI_Mode_Change_Event): def on_hci_mode_change_event(self, event: hci.HCI_Mode_Change_Event):
@@ -1401,7 +1433,7 @@ class Host(utils.EventEmitter):
event.interval, 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: if event.status == hci.HCI_SUCCESS:
logger.debug( logger.debug(
f'role change for {event.bd_addr}: ' f'role change for {event.bd_addr}: '
@@ -1415,7 +1447,9 @@ class Host(utils.EventEmitter):
) )
self.emit('role_change_failure', event.bd_addr, event.status) 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: if (connection := self.connections.get(event.connection_handle)) is None:
logger.warning('!!! DATA LENGTH CHANGE: unknown handle') logger.warning('!!! DATA LENGTH CHANGE: unknown handle')
return return
@@ -1429,7 +1463,9 @@ class Host(utils.EventEmitter):
event.max_rx_time, 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 # Notify the client
if event.status == hci.HCI_SUCCESS: if event.status == hci.HCI_SUCCESS:
self.emit('connection_authentication', event.connection_handle) self.emit('connection_authentication', event.connection_handle)
@@ -1470,7 +1506,9 @@ class Host(utils.EventEmitter):
'connection_encryption_failure', event.connection_handle, event.status '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 # Notify the client
if event.status == hci.HCI_SUCCESS: if event.status == hci.HCI_SUCCESS:
self.emit('connection_encryption_key_refresh', event.connection_handle) self.emit('connection_encryption_key_refresh', event.connection_handle)
@@ -1481,7 +1519,7 @@ class Host(utils.EventEmitter):
event.status, 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: if event.status == hci.HCI_SUCCESS:
self.emit( self.emit(
'connection_qos_setup', event.connection_handle, event.service_type 'connection_qos_setup', event.connection_handle, event.service_type
@@ -1493,23 +1531,31 @@ class Host(utils.EventEmitter):
event.status, 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 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 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 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( logger.debug(
f'link key for {event.bd_addr}: {event.link_key.hex()}, ' f'link key for {event.bd_addr}: {event.link_key.hex()}, '
f'type={hci.HCI_Constant.link_key_type_name(event.key_type)}' 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) 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( logger.debug(
f'simple pairing complete for {event.bd_addr}: ' f'simple pairing complete for {event.bd_addr}: '
f'status={hci.HCI_Constant.status_name(event.status)}' f'status={hci.HCI_Constant.status_name(event.status)}'
@@ -1519,10 +1565,10 @@ class Host(utils.EventEmitter):
else: else:
self.emit('classic_pairing_failure', event.bd_addr, event.status) 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) 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(): async def send_link_key():
if self.link_key_provider is None: if self.link_key_provider is None:
logger.debug('no link key provider') logger.debug('no link key provider')
@@ -1547,10 +1593,14 @@ class Host(utils.EventEmitter):
asyncio.create_task(send_link_key()) 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) 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( self.emit(
'authentication_io_capability_response', 'authentication_io_capability_response',
event.bd_addr, event.bd_addr,
@@ -1558,25 +1608,33 @@ class Host(utils.EventEmitter):
event.authentication_requirements, 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( self.emit(
'authentication_user_confirmation_request', 'authentication_user_confirmation_request',
event.bd_addr, event.bd_addr,
event.numeric_value, 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) 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( self.emit(
'authentication_user_passkey_notification', event.bd_addr, event.passkey 'authentication_user_passkey_notification', event.bd_addr, event.passkey
) )
def on_hci_inquiry_complete_event(self, _event): def on_hci_inquiry_complete_event(self, _event: hci.HCI_Inquiry_Complete_Event):
self.emit('inquiry_complete') self.emit('inquiry_complete')
def on_hci_inquiry_result_with_rssi_event(self, event): def on_hci_inquiry_result_with_rssi_event(
self, event: hci.HCI_Inquiry_Result_With_RSSI_Event
):
for bd_addr, class_of_device, rssi in zip( for bd_addr, class_of_device, rssi in zip(
event.bd_addr, event.class_of_device, event.rssi event.bd_addr, event.class_of_device, event.rssi
): ):
@@ -1588,7 +1646,9 @@ class Host(utils.EventEmitter):
rssi, 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( self.emit(
'inquiry_result', 'inquiry_result',
event.bd_addr, event.bd_addr,
@@ -1597,7 +1657,9 @@ class Host(utils.EventEmitter):
event.rssi, 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: if event.status != hci.HCI_SUCCESS:
self.emit('remote_name_failure', event.bd_addr, event.status) self.emit('remote_name_failure', event.bd_addr, event.status)
else: else:
@@ -1608,14 +1670,18 @@ class Host(utils.EventEmitter):
self.emit('remote_name', event.bd_addr, utf8_name) 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( self.emit(
'remote_host_supported_features', 'remote_host_supported_features',
event.bd_addr, event.bd_addr,
event.host_supported_features, 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: if event.status != hci.HCI_SUCCESS:
self.emit( self.emit(
'le_remote_features_failure', event.connection_handle, event.status 'le_remote_features_failure', event.connection_handle, event.status
@@ -1627,23 +1693,45 @@ class Host(utils.EventEmitter):
int.from_bytes(event.le_features, 'little'), 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) 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) 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) 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) 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) 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) 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) self.emit('vendor_event', event)

View File

@@ -21,16 +21,18 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import dataclasses import dataclasses
import json
import logging import logging
import os import os
import json from typing import TYPE_CHECKING, Any, Optional
from typing import TYPE_CHECKING, Optional, Any
from typing_extensions import Self from typing_extensions import Self
from bumble.colors import color
from bumble import hci from bumble import hci
from bumble.colors import color
if TYPE_CHECKING: if TYPE_CHECKING:
from bumble.device import Device from bumble.device import Device

View File

@@ -16,32 +16,32 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import dataclasses import dataclasses
import enum import enum
import logging import logging
import struct import struct
from collections import deque from collections import deque
from collections.abc import Sequence
from typing import ( from typing import (
Optional, TYPE_CHECKING,
Callable,
Any, Any,
Union, Callable,
ClassVar,
Iterable, Iterable,
Optional,
SupportsBytes, SupportsBytes,
TypeVar, TypeVar,
ClassVar, Union,
TYPE_CHECKING,
) )
from bumble import utils from bumble import hci, utils
from bumble import hci
from bumble.colors import color from bumble.colors import color
from bumble.core import ( from bumble.core import (
InvalidStateError,
InvalidArgumentError, InvalidArgumentError,
InvalidPacketError, InvalidPacketError,
InvalidStateError,
OutOfResourcesError, OutOfResourcesError,
ProtocolError, ProtocolError,
) )
@@ -112,6 +112,10 @@ class CommandCode(hci.SpecableEnum):
L2CAP_LE_CREDIT_BASED_CONNECTION_REQUEST = 0x14 L2CAP_LE_CREDIT_BASED_CONNECTION_REQUEST = 0x14
L2CAP_LE_CREDIT_BASED_CONNECTION_RESPONSE = 0x15 L2CAP_LE_CREDIT_BASED_CONNECTION_RESPONSE = 0x15
L2CAP_LE_FLOW_CONTROL_CREDIT = 0x16 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_ACCEPTED_RESULT = 0x0000
L2CAP_CONNECTION_PARAMETERS_REJECTED_RESULT = 0x0001 L2CAP_CONNECTION_PARAMETERS_REJECTED_RESULT = 0x0001
@@ -213,7 +217,7 @@ class L2CAP_Control_Frame:
fields: ClassVar[hci.Fields] = () fields: ClassVar[hci.Fields] = ()
code: int = dataclasses.field(default=0, init=False) code: int = dataclasses.field(default=0, init=False)
name: str = dataclasses.field(default='', 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 identifier: int
@@ -223,7 +227,8 @@ class L2CAP_Control_Frame:
subclass = L2CAP_Control_Frame.classes.get(code) subclass = L2CAP_Control_Frame.classes.get(code)
if subclass is None: if subclass is None:
instance = L2CAP_Control_Frame(pdu) instance = L2CAP_Control_Frame(identifier=identifier)
instance.payload = pdu[4:]
instance.code = CommandCode(code) instance.code = CommandCode(code)
instance.name = instance.code.name instance.name = instance.code.name
return instance return instance
@@ -232,11 +237,11 @@ class L2CAP_Control_Frame:
identifier=identifier, identifier=identifier,
) )
frame.identifier = identifier frame.identifier = identifier
frame.data = pdu[4:] frame.payload = pdu[4:]
if length != len(pdu): if length != len(frame.payload):
logger.warning( logger.warning(
color( color(
f'!!! length mismatch: expected {len(pdu) - 4} but got {length}', f'!!! length mismatch: expected {length} but got {len(frame.payload)}',
'red', 'red',
) )
) )
@@ -273,34 +278,20 @@ class L2CAP_Control_Frame:
return subclass 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 @property
def data(self) -> bytes: def payload(self) -> bytes:
if self._data is None: if self._payload is None:
self._data = hci.HCI_Object.dict_to_bytes(self.__dict__, self.fields) self._payload = hci.HCI_Object.dict_to_bytes(self.__dict__, self.fields)
return self._data return self._payload
@data.setter @payload.setter
def data(self, parameters: bytes) -> None: def payload(self, payload: bytes) -> None:
self._data = parameters self._payload = payload
def __bytes__(self) -> bytes: def __bytes__(self) -> bytes:
return ( return (
struct.pack('<BBH', self.code, self.identifier, len(self.data) + 4) struct.pack('<BBH', self.code, self.identifier, len(self.payload))
+ self.data + self.payload
) )
def __str__(self) -> str: def __str__(self) -> str:
@@ -308,8 +299,8 @@ class L2CAP_Control_Frame:
if fields := getattr(self, 'fields', None): if fields := getattr(self, 'fields', None):
result += ':\n' + hci.HCI_Object.format_fields(self.__dict__, fields, ' ') result += ':\n' + hci.HCI_Object.format_fields(self.__dict__, fields, ' ')
else: else:
if len(self.data) > 1: if len(self.payload) > 1:
result += f': {self.data.hex()}' result += f': {self.payload.hex()}'
return result return result
@@ -608,6 +599,109 @@ class L2CAP_LE_Flow_Control_Credit(L2CAP_Control_Frame):
credits: int = dataclasses.field(metadata=hci.metadata(2)) 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 ClassicChannel(utils.EventEmitter):
class State(enum.IntEnum): class State(enum.IntEnum):
@@ -1437,16 +1531,6 @@ class ChannelManager:
if cid in self.fixed_channels: if cid in self.fixed_channels:
del self.fixed_channels[cid] 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( def create_classic_server(
self, self,
spec: ClassicChannelSpec, spec: ClassicChannelSpec,
@@ -1483,22 +1567,6 @@ class ChannelManager:
return self.servers[spec.psm] 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( def create_le_credit_based_server(
self, self,
spec: LeCreditBasedChannelSpec, spec: LeCreditBasedChannelSpec,
@@ -1600,8 +1668,8 @@ class ChannelManager:
if handler: if handler:
try: try:
handler(connection, cid, control_frame) handler(connection, cid, control_frame)
except Exception as error: except Exception:
logger.warning(f'{color("!!! Exception in handler:", "red")} {error}') logger.exception(color("!!! Exception in handler:", "red"))
self.send_control_frame( self.send_control_frame(
connection, connection,
cid, cid,
@@ -1611,7 +1679,7 @@ class ChannelManager:
data=b'', data=b'',
), ),
) )
raise error raise
else: else:
logger.error(color('Channel Manager command not handled???', 'red')) logger.error(color('Channel Manager command not handled???', 'red'))
self.send_control_frame( self.send_control_frame(
@@ -2051,17 +2119,6 @@ class ChannelManager:
if channel.source_cid in connection_channels: if channel.source_cid in connection_channels:
del connection_channels[channel.source_cid] del connection_channels[channel.source_cid]
@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( async def create_le_credit_based_channel(
self, self,
connection: Connection, connection: Connection,
@@ -2097,8 +2154,8 @@ class ChannelManager:
# Connect # Connect
try: try:
await channel.connect() await channel.connect()
except Exception as error: except Exception:
logger.warning(f'connection failed: {error}') logger.exception('connection failed')
del connection_channels[source_cid] del connection_channels[source_cid]
raise raise
@@ -2108,12 +2165,6 @@ class ChannelManager:
return channel 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( async def create_classic_channel(
self, connection: Connection, spec: ClassicChannelSpec self, connection: Connection, spec: ClassicChannelSpec
) -> ClassicChannel: ) -> ClassicChannel:
@@ -2150,20 +2201,3 @@ class ChannelManager:
raise e raise e
return channel 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)

View File

@@ -12,26 +12,25 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import asyncio
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
import logging 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 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 # Logging
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -159,29 +158,29 @@ class LocalLink:
asyncio.get_running_loop().call_soon(self.on_connection_complete) asyncio.get_running_loop().call_soon(self.on_connection_complete)
def on_disconnection_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 # 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') logger.warning('!!! Initiating controller not found')
return return
# Disconnect from the first controller with a matching address # Disconnect from the first controller with a matching address
if peripheral_controller := self.find_controller(peripheral_address): if target_controller := self.find_controller(target_address):
peripheral_controller.on_link_central_disconnected( target_controller.on_link_disconnected(
central_address, disconnect_command.reason initiating_address, disconnect_command.reason
) )
central_controller.on_link_peripheral_disconnection_complete( initiating_controller.on_link_disconnection_complete(
disconnect_command, HCI_SUCCESS disconnect_command, HCI_SUCCESS
) )
def disconnect(self, central_address, peripheral_address, disconnect_command): def disconnect(self, initiating_address, target_address, disconnect_command):
logger.debug( logger.debug(
f'$$$ DISCONNECTION {central_address} -> ' f'$$$ DISCONNECTION {initiating_address} -> '
f'{peripheral_address}: reason = {disconnect_command.reason}' 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) asyncio.get_running_loop().call_soon(self.on_disconnection_complete, *args)
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments

65
bumble/logging.py Normal file
View 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],
)

View File

@@ -16,27 +16,28 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
import enum import enum
from dataclasses import dataclass
import secrets import secrets
from dataclasses import dataclass
from typing import Optional from typing import Optional
from bumble import hci from bumble import hci
from bumble.core import AdvertisingData, LeRole
from bumble.smp import ( from bumble.smp import (
SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
SMP_KEYBOARD_ONLY_IO_CAPABILITY,
SMP_DISPLAY_ONLY_IO_CAPABILITY, SMP_DISPLAY_ONLY_IO_CAPABILITY,
SMP_DISPLAY_YES_NO_IO_CAPABILITY, SMP_DISPLAY_YES_NO_IO_CAPABILITY,
SMP_KEYBOARD_DISPLAY_IO_CAPABILITY,
SMP_ENC_KEY_DISTRIBUTION_FLAG, SMP_ENC_KEY_DISTRIBUTION_FLAG,
SMP_ID_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_LINK_KEY_DISTRIBUTION_FLAG,
SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
SMP_SIGN_KEY_DISTRIBUTION_FLAG,
OobContext, OobContext,
OobLegacyContext, OobLegacyContext,
OobSharedData, OobSharedData,
) )
from bumble.core import AdvertisingData, LeRole
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------

View File

@@ -19,21 +19,22 @@ This module implement the Pandora Bluetooth test APIs for the Bumble stack.
__version__ = "0.0.1" __version__ = "0.0.1"
from typing import Callable, List, Optional
import grpc import grpc
import grpc.aio 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.host_grpc_aio import add_HostServicer_to_server
from pandora.l2cap_grpc_aio import add_L2CAPServicer_to_server from pandora.l2cap_grpc_aio import add_L2CAPServicer_to_server
from pandora.security_grpc_aio import ( from pandora.security_grpc_aio import (
add_SecurityServicer_to_server, add_SecurityServicer_to_server,
add_SecurityStorageServicer_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 # public symbols
__all__ = [ __all__ = [
@@ -49,7 +50,7 @@ _SERVICERS_HOOKS: list[Callable[[PandoraDevice, Config, grpc.aio.Server], None]]
def register_servicer_hook( def register_servicer_hook(
hook: Callable[[PandoraDevice, Config, grpc.aio.Server], None] hook: Callable[[PandoraDevice, Config, grpc.aio.Server], None],
) -> None: ) -> None:
_SERVICERS_HOOKS.append(hook) _SERVICERS_HOOKS.append(hook)

View File

@@ -13,10 +13,12 @@
# limitations under the License. # limitations under the License.
from __future__ import annotations from __future__ import annotations
from bumble.pairing import PairingConfig, PairingDelegate
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any from typing import Any
from bumble.pairing import PairingConfig, PairingDelegate
@dataclass @dataclass
class Config: class Config:

View File

@@ -15,6 +15,9 @@
"""Generic & dependency free Bumble (reference) device.""" """Generic & dependency free Bumble (reference) device."""
from __future__ import annotations from __future__ import annotations
from typing import Any, Optional
from bumble import transport from bumble import transport
from bumble.core import ( from bumble.core import (
BT_GENERIC_AUDIO_SERVICE, BT_GENERIC_AUDIO_SERVICE,
@@ -32,8 +35,6 @@ from bumble.sdp import (
DataElement, DataElement,
ServiceAttribute, ServiceAttribute,
) )
from typing import Any, Optional
# Default rootcanal HCI TCP address # Default rootcanal HCI TCP address
ROOTCANAL_HCI_ADDRESS = "localhost:6402" ROOTCANAL_HCI_ADDRESS = "localhost:6402"

View File

@@ -13,51 +13,23 @@
# limitations under the License. # limitations under the License.
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import bumble.device
import grpc
import grpc.aio
import logging import logging
import struct import struct
from typing import AsyncGenerator, Optional, cast
import bumble.utils import grpc
from bumble.pandora import utils import grpc.aio
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,
)
from google.protobuf import any_pb2 # pytype: disable=pyi-error from google.protobuf import any_pb2 # pytype: disable=pyi-error
from google.protobuf import empty_pb2 # pytype: disable=pyi-error from google.protobuf import empty_pb2 # pytype: disable=pyi-error
from pandora.host_grpc_aio import HostServicer
from pandora import host_pb2 from pandora import host_pb2
from pandora.host_grpc_aio import HostServicer
from pandora.host_pb2 import ( from pandora.host_pb2 import (
DISCOVERABLE_GENERAL,
DISCOVERABLE_LIMITED,
NOT_CONNECTABLE, NOT_CONNECTABLE,
NOT_DISCOVERABLE, NOT_DISCOVERABLE,
DISCOVERABLE_LIMITED,
DISCOVERABLE_GENERAL,
PRIMARY_1M, PRIMARY_1M,
PRIMARY_CODED, PRIMARY_CODED,
SECONDARY_1M, SECONDARY_1M,
@@ -85,7 +57,37 @@ from pandora.host_pb2 import (
WaitConnectionResponse, WaitConnectionResponse,
WaitDisconnectionRequest, 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] = { PRIMARY_PHY_MAP: dict[int, PrimaryPhy] = {
# Default value reported by Bumble for legacy Advertising reports. # Default value reported by Bumble for legacy Advertising reports.

View File

@@ -12,31 +12,21 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import grpc
import json import json
import logging 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 import grpc
from bumble.pandora import utils
from bumble.pandora.config import Config
from bumble.core import OutOfResourcesError, InvalidArgumentError
from bumble.device import Device
from bumble.l2cap import (
ClassicChannel,
ClassicChannelServer,
ClassicChannelSpec,
LeCreditBasedChannel,
LeCreditBasedChannelServer,
LeCreditBasedChannelSpec,
)
from google.protobuf import any_pb2, empty_pb2 # pytype: disable=pyi-error from 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_grpc_aio import L2CAPServicer # pytype: disable=pyi-error
from pandora.l2cap_pb2 import ( # pytype: disable=pyi-error from pandora.l2cap_pb2 import COMMAND_NOT_UNDERSTOOD, INVALID_CID_IN_REQUEST
COMMAND_NOT_UNDERSTOOD, from pandora.l2cap_pb2 import Channel as PandoraChannel # pytype: disable=pyi-error
INVALID_CID_IN_REQUEST, from pandora.l2cap_pb2 import (
Channel as PandoraChannel,
ConnectRequest, ConnectRequest,
ConnectResponse, ConnectResponse,
CreditBasedChannelRequest, CreditBasedChannelRequest,
@@ -51,8 +41,19 @@ from pandora.l2cap_pb2 import ( # pytype: disable=pyi-error
WaitDisconnectionRequest, WaitDisconnectionRequest,
WaitDisconnectionResponse, 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] L2capChannel = Union[ClassicChannel, LeCreditBasedChannel]

View File

@@ -13,24 +13,14 @@
# limitations under the License. # limitations under the License.
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import contextlib import contextlib
from collections.abc import Awaitable
import grpc
import logging import logging
from collections.abc import Awaitable
from typing import Any, AsyncGenerator, AsyncIterator, Callable, Optional, Union
from bumble.pandora import utils import grpc
from bumble.pandora.config import Config
from bumble import hci
from bumble.core import (
PhysicalTransport,
ProtocolError,
InvalidArgumentError,
)
import bumble.utils
from bumble.device import Connection as BumbleConnection, Device
from bumble.hci import HCI_Error, Role
from bumble.pairing import PairingConfig, PairingDelegate as BasePairingDelegate
from google.protobuf import any_pb2 # pytype: disable=pyi-error from google.protobuf import any_pb2 # pytype: disable=pyi-error
from google.protobuf import empty_pb2 # pytype: disable=pyi-error from google.protobuf import empty_pb2 # pytype: disable=pyi-error
from google.protobuf import wrappers_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, WaitSecurityRequest,
WaitSecurityResponse, 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): class PairingDelegate(BasePairingDelegate):

View File

@@ -13,16 +13,18 @@
# limitations under the License. # limitations under the License.
from __future__ import annotations from __future__ import annotations
import contextlib import contextlib
import functools import functools
import grpc
import inspect import inspect
import logging 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.device import Device
from bumble.hci import Address, AddressType 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] = { ADDRESS_TYPES: dict[str, AddressType] = {
"public": Address.PUBLIC_DEVICE_ADDRESS, "public": Address.PUBLIC_DEVICE_ADDRESS,

View File

@@ -18,26 +18,27 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
import logging import logging
import struct import struct
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional from typing import Optional
from bumble.device import Connection from bumble import utils
from bumble.att import ATT_Error from bumble.att import ATT_Error
from bumble.device import Connection
from bumble.gatt import ( 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, Attribute,
Characteristic, Characteristic,
TemplateService,
CharacteristicValue, CharacteristicValue,
GATT_AUDIO_INPUT_CONTROL_SERVICE, TemplateService,
GATT_AUDIO_INPUT_STATE_CHARACTERISTIC,
GATT_GAIN_SETTINGS_ATTRIBUTE_CHARACTERISTIC,
GATT_AUDIO_INPUT_TYPE_CHARACTERISTIC,
GATT_AUDIO_INPUT_STATUS_CHARACTERISTIC,
GATT_AUDIO_INPUT_CONTROL_POINT_CHARACTERISTIC,
GATT_AUDIO_INPUT_DESCRIPTION_CHARACTERISTIC,
) )
from bumble.gatt_adapters import ( from bumble.gatt_adapters import (
CharacteristicProxy, CharacteristicProxy,
@@ -48,7 +49,6 @@ from bumble.gatt_adapters import (
UTF8CharacteristicProxyAdapter, UTF8CharacteristicProxyAdapter,
) )
from bumble.gatt_client import ProfileServiceProxy, ServiceProxy from bumble.gatt_client import ProfileServiceProxy, ServiceProxy
from bumble import utils
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Logging # Logging

403
bumble/profiles/ams.py Normal file
View 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]}")

View File

@@ -20,6 +20,7 @@ Apple Notification Center Service (ANCS).
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import dataclasses import dataclasses
import datetime import datetime
@@ -28,21 +29,19 @@ import logging
import struct import struct
from typing import Optional, Sequence, Union from typing import Optional, Sequence, Union
from bumble import utils
from bumble.att import ATT_Error from bumble.att import ATT_Error
from bumble.device import Peer from bumble.device import Peer
from bumble.gatt import ( from bumble.gatt import (
Characteristic,
GATT_ANCS_SERVICE,
GATT_ANCS_NOTIFICATION_SOURCE_CHARACTERISTIC,
GATT_ANCS_CONTROL_POINT_CHARACTERISTIC, GATT_ANCS_CONTROL_POINT_CHARACTERISTIC,
GATT_ANCS_DATA_SOURCE_CHARACTERISTIC, GATT_ANCS_DATA_SOURCE_CHARACTERISTIC,
GATT_ANCS_NOTIFICATION_SOURCE_CHARACTERISTIC,
GATT_ANCS_SERVICE,
Characteristic,
TemplateService, TemplateService,
) )
from bumble.gatt_client import CharacteristicProxy, ProfileServiceProxy, ServiceProxy
from bumble.gatt_adapters import SerializableCharacteristicProxyAdapter from bumble.gatt_adapters import SerializableCharacteristicProxyAdapter
from bumble import utils from bumble.gatt_client import CharacteristicProxy, ProfileServiceProxy, ServiceProxy
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Constants # Constants

View File

@@ -18,22 +18,17 @@
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass, field
import enum import enum
import functools import functools
import logging import logging
import struct import struct
from typing import Any, Optional, Union, TypeVar
from collections.abc import Sequence 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, device, gatt, gatt_client, hci, utils
from bumble import colors
from bumble.profiles.bap import CodecSpecificConfiguration
from bumble.profiles import le_audio from bumble.profiles import le_audio
from bumble import device from bumble.profiles.bap import CodecSpecificConfiguration
from bumble import gatt
from bumble import gatt_client
from bumble import hci
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Logging # Logging
@@ -452,6 +447,16 @@ class AseStateMachine(gatt.Characteristic):
self.metadata = le_audio.Metadata.from_bytes(metadata) self.metadata = le_audio.Metadata.from_bytes(metadata)
self.state = self.State.ENABLING 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) return (AseResponseCode.SUCCESS, AseReasonCode.NONE)

View File

@@ -17,16 +17,13 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
import enum import enum
import struct
import logging 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 data_types, gatt, gatt_client, l2cap, utils
from bumble import utils
from bumble import gatt
from bumble import gatt_client
from bumble.core import AdvertisingData from bumble.core import AdvertisingData
from bumble.device import Device, Connection from bumble.device import Connection, Device
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Logging # Logging
@@ -188,12 +185,11 @@ class AshaService(gatt.TemplateService):
return bytes( return bytes(
AdvertisingData( AdvertisingData(
[ [
( data_types.ServiceData16BitUUID(
AdvertisingData.SERVICE_DATA_16_BIT_UUID, gatt.GATT_ASHA_SERVICE,
bytes(gatt.GATT_ASHA_SERVICE) bytes([self.protocol_version, self.capability])
+ bytes([self.protocol_version, self.capability])
+ self.hisyncid[:4], + self.hisyncid[:4],
), )
] ]
) )
) )

View File

@@ -18,21 +18,18 @@
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
from collections.abc import Sequence
import dataclasses import dataclasses
import enum import enum
import struct
import functools import functools
import logging import logging
import struct
from collections.abc import Sequence
from typing_extensions import Self from typing_extensions import Self
from bumble import core from bumble import core, data_types, gatt, hci, utils
from bumble import hci
from bumble import gatt
from bumble import utils
from bumble.profiles import le_audio from bumble.profiles import le_audio
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Logging # Logging
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -260,11 +257,10 @@ class UnicastServerAdvertisingData:
return bytes( return bytes(
core.AdvertisingData( core.AdvertisingData(
[ [
( data_types.ServiceData16BitUUID(
core.AdvertisingData.SERVICE_DATA_16_BIT_UUID, gatt.GATT_AUDIO_STREAM_CONTROL_SERVICE,
struct.pack( struct.pack(
'<2sBIB', '<BIB',
bytes(gatt.GATT_AUDIO_STREAM_CONTROL_SERVICE),
self.announcement_type, self.announcement_type,
self.available_audio_contexts, self.available_audio_contexts,
len(self.metadata), len(self.metadata),
@@ -493,12 +489,8 @@ class BroadcastAudioAnnouncement:
return bytes( return bytes(
core.AdvertisingData( core.AdvertisingData(
[ [
( data_types.ServiceData16BitUUID(
core.AdvertisingData.SERVICE_DATA_16_BIT_UUID, gatt.GATT_BROADCAST_AUDIO_ANNOUNCEMENT_SERVICE, bytes(self)
(
bytes(gatt.GATT_BROADCAST_AUDIO_ANNOUNCEMENT_SERVICE)
+ bytes(self)
),
) )
] ]
) )
@@ -610,12 +602,8 @@ class BasicAudioAnnouncement:
return bytes( return bytes(
core.AdvertisingData( core.AdvertisingData(
[ [
( data_types.ServiceData16BitUUID(
core.AdvertisingData.SERVICE_DATA_16_BIT_UUID, gatt.GATT_BASIC_AUDIO_ANNOUNCEMENT_SERVICE, bytes(self)
(
bytes(gatt.GATT_BASIC_AUDIO_ANNOUNCEMENT_SERVICE)
+ bytes(self)
),
) )
] ]
) )

View File

@@ -17,18 +17,13 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
import dataclasses import dataclasses
import logging import logging
import struct import struct
from typing import ClassVar, Optional, Sequence from typing import ClassVar, Optional, Sequence
from bumble import core from bumble import core, device, gatt, gatt_adapters, gatt_client, hci, utils
from bumble import device
from bumble import gatt
from bumble import gatt_adapters
from bumble import gatt_client
from bumble import hci
from bumble import utils
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Logging # Logging

View File

@@ -18,19 +18,18 @@
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from typing import Optional from typing import Optional
from bumble.gatt_client import ProfileServiceProxy
from bumble.gatt import ( from bumble.gatt import (
GATT_BATTERY_SERVICE,
GATT_BATTERY_LEVEL_CHARACTERISTIC, GATT_BATTERY_LEVEL_CHARACTERISTIC,
TemplateService, GATT_BATTERY_SERVICE,
Characteristic, Characteristic,
CharacteristicValue, CharacteristicValue,
TemplateService,
) )
from bumble.gatt_client import CharacteristicProxy
from bumble.gatt_adapters import ( from bumble.gatt_adapters import (
PackedCharacteristicAdapter, PackedCharacteristicAdapter,
PackedCharacteristicProxyAdapter, PackedCharacteristicProxyAdapter,
) )
from bumble.gatt_client import CharacteristicProxy, ProfileServiceProxy
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------

View File

@@ -18,8 +18,7 @@
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
from bumble import gatt from bumble import gatt, gatt_client
from bumble import gatt_client
from bumble.profiles import csip from bumble.profiles import csip

View File

@@ -17,16 +17,12 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
import enum import enum
import struct import struct
from typing import Optional from typing import Optional
from bumble import core from bumble import core, crypto, device, gatt, gatt_client
from bumble import crypto
from bumble import device
from bumble import gatt
from bumble import gatt_client
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Constants # Constants

View File

@@ -25,12 +25,12 @@ from bumble.gatt import (
GATT_HARDWARE_REVISION_STRING_CHARACTERISTIC, GATT_HARDWARE_REVISION_STRING_CHARACTERISTIC,
GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC, GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC,
GATT_MODEL_NUMBER_STRING_CHARACTERISTIC, GATT_MODEL_NUMBER_STRING_CHARACTERISTIC,
GATT_REGULATORY_CERTIFICATION_DATA_LIST_CHARACTERISTIC,
GATT_SERIAL_NUMBER_STRING_CHARACTERISTIC, GATT_SERIAL_NUMBER_STRING_CHARACTERISTIC,
GATT_SOFTWARE_REVISION_STRING_CHARACTERISTIC, GATT_SOFTWARE_REVISION_STRING_CHARACTERISTIC,
GATT_SYSTEM_ID_CHARACTERISTIC, GATT_SYSTEM_ID_CHARACTERISTIC,
GATT_REGULATORY_CERTIFICATION_DATA_LIST_CHARACTERISTIC,
TemplateService,
Characteristic, Characteristic,
TemplateService,
) )
from bumble.gatt_adapters import ( from bumble.gatt_adapters import (
DelegatedCharacteristicProxyAdapter, DelegatedCharacteristicProxyAdapter,

View File

@@ -23,11 +23,11 @@ from typing import Optional, Union
from bumble.core import Appearance from bumble.core import Appearance
from bumble.gatt import ( from bumble.gatt import (
TemplateService,
Characteristic,
GATT_GENERIC_ACCESS_SERVICE,
GATT_DEVICE_NAME_CHARACTERISTIC,
GATT_APPEARANCE_CHARACTERISTIC, GATT_APPEARANCE_CHARACTERISTIC,
GATT_DEVICE_NAME_CHARACTERISTIC,
GATT_GENERIC_ACCESS_SERVICE,
Characteristic,
TemplateService,
) )
from bumble.gatt_adapters import ( from bumble.gatt_adapters import (
DelegatedCharacteristicProxyAdapter, DelegatedCharacteristicProxyAdapter,

View File

@@ -17,10 +17,7 @@ from __future__ import annotations
import struct import struct
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from bumble import att from bumble import att, crypto, gatt, gatt_client
from bumble import gatt
from bumble import gatt_client
from bumble import crypto
if TYPE_CHECKING: if TYPE_CHECKING:
from bumble import device from bumble import device

View File

@@ -18,21 +18,21 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
import struct import struct
from enum import IntFlag
from typing import Optional from typing import Optional
from bumble.gatt import ( from bumble.gatt import (
TemplateService, GATT_BGR_FEATURES_CHARACTERISTIC,
Characteristic, GATT_BGS_FEATURES_CHARACTERISTIC,
GATT_GAMING_AUDIO_SERVICE, GATT_GAMING_AUDIO_SERVICE,
GATT_GMAP_ROLE_CHARACTERISTIC, GATT_GMAP_ROLE_CHARACTERISTIC,
GATT_UGG_FEATURES_CHARACTERISTIC, GATT_UGG_FEATURES_CHARACTERISTIC,
GATT_UGT_FEATURES_CHARACTERISTIC, GATT_UGT_FEATURES_CHARACTERISTIC,
GATT_BGS_FEATURES_CHARACTERISTIC, Characteristic,
GATT_BGR_FEATURES_CHARACTERISTIC, TemplateService,
) )
from bumble.gatt_adapters import DelegatedCharacteristicProxyAdapter from bumble.gatt_adapters import DelegatedCharacteristicProxyAdapter
from bumble.gatt_client import CharacteristicProxy, ProfileServiceProxy, ServiceProxy from bumble.gatt_client import CharacteristicProxy, ProfileServiceProxy, ServiceProxy
from enum import IntFlag
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------

View File

@@ -16,16 +16,15 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import functools
from dataclasses import dataclass, field
import logging import logging
from dataclasses import dataclass, field
from typing import Any, Optional, Union 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.core import InvalidArgumentError, InvalidStateError
from bumble.device import Device, Connection from bumble.device import Connection, Device
from bumble import utils
from bumble.hci import Address from bumble.hci import Address
@@ -272,7 +271,7 @@ class HearingAccessService(gatt.TemplateService):
def on_connection(connection: Connection) -> None: def on_connection(connection: Connection) -> None:
@connection.on(connection.EVENT_DISCONNECTION) @connection.on(connection.EVENT_DISCONNECTION)
def on_disconnection(_reason) -> None: def on_disconnection(_reason) -> None:
self.currently_connected_clients.remove(connection) self.currently_connected_clients.discard(connection)
@connection.on(connection.EVENT_PAIRING) @connection.on(connection.EVENT_PAIRING)
def on_pairing(*_: Any) -> None: def on_pairing(*_: Any) -> None:
@@ -373,8 +372,7 @@ class HearingAccessService(gatt.TemplateService):
self.preset_records[key] self.preset_records[key]
for key in sorted(self.preset_records.keys()) for key in sorted(self.preset_records.keys())
if self.preset_records[key].index >= start_index if self.preset_records[key].index >= start_index
] ][:num_presets]
del presets[num_presets:]
if len(presets) == 0: if len(presets) == 0:
raise att.ATT_Error(att.ErrorCode.OUT_OF_RANGE) raise att.ATT_Error(att.ErrorCode.OUT_OF_RANGE)
@@ -383,7 +381,10 @@ class HearingAccessService(gatt.TemplateService):
async def _read_preset_response( async def _read_preset_response(
self, connection: Connection, presets: list[PresetRecord] self, connection: Connection, presets: list[PresetRecord]
): ):
# If the ATT bearer is terminated before all notifications or indications are sent, then the server shall consider the Read Presets Request operation aborted and shall not either continue or restart the operation when the client reconnects. # If the ATT bearer is terminated before all notifications or indications are
# sent, then the server shall consider the Read Presets Request operation
# aborted and shall not either continue or restart the operation when the client
# reconnects.
try: try:
for i, preset in enumerate(presets): for i, preset in enumerate(presets):
await connection.device.indicate_subscriber( await connection.device.indicate_subscriber(
@@ -404,7 +405,7 @@ class HearingAccessService(gatt.TemplateService):
async def generic_update(self, op: PresetChangedOperation) -> None: 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''' '''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: async def delete_preset(self, index: int) -> None:
'''Server API to delete a preset. It should not be the current active preset''' '''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') raise InvalidStateError('Cannot delete active preset')
del self.preset_records[index] 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: async def available_preset(self, index: int) -> None:
'''Server API to make a preset available''' '''Server API to make a preset available'''
preset = self.preset_records[index] preset = self.preset_records[index]
preset.properties.is_available = PresetRecord.Property.IsAvailable.IS_AVAILABLE 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: async def unavailable_preset(self, index: int) -> None:
'''Server API to make a preset unavailable. It should not be the current active preset''' '''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 = ( preset.properties.is_available = (
PresetRecord.Property.IsAvailable.IS_UNAVAILABLE 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: async def _preset_changed_operation(self, connection: Connection) -> None:
'''Send all PresetChangedOperation saved for a given connection''' '''Send all PresetChangedOperation saved for a given connection'''
@@ -447,8 +448,10 @@ class HearingAccessService(gatt.TemplateService):
return op.additional_parameters return op.additional_parameters
op_list.sort(key=get_op_index) 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. # If the ATT bearer is terminated before all notifications or indications are
while len(op_list) > 0: # sent, then the server shall consider the Preset Changed operation aborted and
# shall continue the operation when the client reconnects.
while op_list:
try: try:
await connection.device.indicate_subscriber( await connection.device.indicate_subscriber(
connection, connection,
@@ -460,14 +463,15 @@ class HearingAccessService(gatt.TemplateService):
except TimeoutError: except TimeoutError:
break break
async def _notifyPresetOperations(self, op: PresetChangedOperation) -> None: async def _notify_preset_operations(self, op: PresetChangedOperation) -> None:
for historyList in self.preset_changed_operations_history_per_device.values(): for history_list in self.preset_changed_operations_history_per_device.values():
historyList.append(op) history_list.append(op)
for connection in self.currently_connected_clients: for connection in self.currently_connected_clients:
await self._preset_changed_operation(connection) await self._preset_changed_operation(connection)
async def _on_write_preset_name(self, connection: Connection, value: bytes): async def _on_write_preset_name(self, connection: Connection, value: bytes):
del connection # Unused
if self.read_presets_request_in_progress: if self.read_presets_request_in_progress:
raise att.ATT_Error(att.ErrorCode.PROCEDURE_ALREADY_IN_PROGRESS) raise att.ATT_Error(att.ErrorCode.PROCEDURE_ALREADY_IN_PROGRESS)
@@ -532,48 +536,51 @@ class HearingAccessService(gatt.TemplateService):
self.active_preset_index = index self.active_preset_index = index
await self.notify_active_preset() await self.notify_active_preset()
async def _on_set_active_preset(self, _: Connection, value: bytes): async def _on_set_active_preset(self, connection: Connection, value: bytes):
del connection # Unused
await self.set_active_preset(value) 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''' '''Set the next or the previous preset as active'''
if self.active_preset_index == 0x00: if self.active_preset_index == 0x00:
raise att.ATT_Error(ErrorCode.PRESET_OPERATION_NOT_POSSIBLE) raise att.ATT_Error(ErrorCode.PRESET_OPERATION_NOT_POSSIBLE)
first_preset: Optional[PresetRecord] = None # To loop to first preset presets = sorted(
next_preset: Optional[PresetRecord] = None [
for index, record in sorted(self.preset_records.items(), reverse=is_previous): record
if not record.is_available(): for record in self.preset_records.values()
continue if record.is_available()
if first_preset == None: ],
first_preset = record key=lambda record: record.index,
if is_previous: )
if index >= self.active_preset_index: current_preset = self.preset_records[self.active_preset_index]
continue current_preset_pos = presets.index(current_preset)
elif index <= self.active_preset_index: if is_previous:
continue new_preset = presets[(current_preset_pos - 1) % len(presets)]
next_preset = record else:
break 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) raise att.ATT_Error(ErrorCode.PRESET_OPERATION_NOT_POSSIBLE)
if next_preset: self.active_preset_index = new_preset.index
self.active_preset_index = next_preset.index
else:
self.active_preset_index = first_preset.index
await self.notify_active_preset() 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) 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) await self.set_next_or_previous_preset(True)
async def _on_set_active_preset_synchronized_locally( async def _on_set_active_preset_synchronized_locally(
self, _: Connection, value: bytes self, connection: Connection, value: bytes
): ):
del connection # Unused.
if ( if (
self.server_features.preset_synchronization_support self.server_features.preset_synchronization_support
== PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_NOT_SUPPORTED == 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) await self.other_server_in_binaural_set.set_active_preset(value)
async def _on_set_next_preset_synchronized_locally( async def _on_set_next_preset_synchronized_locally(
self, _: Connection, __value__: bytes self, connection: Connection, value: bytes
): ):
del connection, value # Unused.
if ( if (
self.server_features.preset_synchronization_support self.server_features.preset_synchronization_support
== PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_NOT_SUPPORTED == 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) await self.other_server_in_binaural_set.set_next_or_previous_preset(False)
async def _on_set_previous_preset_synchronized_locally( async def _on_set_previous_preset_synchronized_locally(
self, _: Connection, __value__: bytes self, connection: Connection, value: bytes
): ):
del connection, value # Unused.
if ( if (
self.server_features.preset_synchronization_support self.server_features.preset_synchronization_support
== PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_NOT_SUPPORTED == PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_NOT_SUPPORTED
@@ -615,11 +624,13 @@ class HearingAccessServiceProxy(gatt_client.ProfileServiceProxy):
SERVICE_CLASS = HearingAccessService SERVICE_CLASS = HearingAccessService
hearing_aid_preset_control_point: gatt_client.CharacteristicProxy hearing_aid_preset_control_point: gatt_client.CharacteristicProxy
preset_control_point_indications: asyncio.Queue preset_control_point_indications: asyncio.Queue[bytes]
active_preset_index_notification: asyncio.Queue active_preset_index_notification: asyncio.Queue[bytes]
def __init__(self, service_proxy: gatt_client.ServiceProxy) -> None: def __init__(self, service_proxy: gatt_client.ServiceProxy) -> None:
self.service_proxy = service_proxy self.service_proxy = service_proxy
self.preset_control_point_indications = asyncio.Queue()
self.active_preset_index_notification = asyncio.Queue()
self.server_features = gatt_adapters.PackedCharacteristicProxyAdapter( self.server_features = gatt_adapters.PackedCharacteristicProxyAdapter(
service_proxy.get_characteristics_by_uuid( service_proxy.get_characteristics_by_uuid(
@@ -641,20 +652,12 @@ class HearingAccessServiceProxy(gatt_client.ProfileServiceProxy):
'B', 'B',
) )
async def setup_subscription(self): async def setup_subscription(self) -> None:
self.preset_control_point_indications = asyncio.Queue()
self.active_preset_index_notification = asyncio.Queue()
def on_active_preset_index_notification(data: bytes):
self.active_preset_index_notification.put_nowait(data)
def on_preset_control_point_indication(data: bytes):
self.preset_control_point_indications.put_nowait(data)
await self.hearing_aid_preset_control_point.subscribe( 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( await self.active_preset_index.subscribe(
functools.partial(on_active_preset_index_notification) self.active_preset_index_notification.put_nowait
) )

View File

@@ -17,20 +17,21 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
from enum import IntEnum
import struct import struct
from enum import IntEnum
from typing import Optional from typing import Optional
from bumble import core from bumble import core
from bumble.att import ATT_Error from bumble.att import ATT_Error
from bumble.gatt import ( from bumble.gatt import (
GATT_HEART_RATE_SERVICE,
GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC,
GATT_BODY_SENSOR_LOCATION_CHARACTERISTIC, GATT_BODY_SENSOR_LOCATION_CHARACTERISTIC,
GATT_HEART_RATE_CONTROL_POINT_CHARACTERISTIC, GATT_HEART_RATE_CONTROL_POINT_CHARACTERISTIC,
TemplateService, GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC,
GATT_HEART_RATE_SERVICE,
Characteristic, Characteristic,
CharacteristicValue, CharacteristicValue,
TemplateService,
) )
from bumble.gatt_adapters import ( from bumble.gatt_adapters import (
DelegatedCharacteristicAdapter, DelegatedCharacteristicAdapter,

View File

@@ -16,14 +16,16 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
import dataclasses import dataclasses
import enum import enum
import struct import struct
from typing import Any from typing import Any
from typing_extensions import Self from typing_extensions import Self
from bumble.profiles import bap
from bumble import utils from bumble import utils
from bumble.profiles import bap
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------

View File

@@ -22,16 +22,12 @@ import asyncio
import dataclasses import dataclasses
import enum import enum
import struct 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 typing_extensions import Self
from bumble import core, device, gatt, gatt_client, utils
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Constants # Constants
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------

View File

@@ -17,18 +17,15 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
import dataclasses import dataclasses
import logging import logging
import struct import struct
from typing import Optional, Sequence, Union 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.profiles import le_audio
from bumble import gatt from bumble.profiles.bap import AudioLocation, CodecSpecificCapabilities, ContextType
from bumble import gatt_adapters
from bumble import gatt_client
from bumble import hci
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Logging # Logging

View File

@@ -16,8 +16,10 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
import dataclasses import dataclasses
import enum import enum
from typing_extensions import Self from typing_extensions import Self
from bumble.profiles import le_audio from bumble.profiles import le_audio

View File

@@ -22,15 +22,14 @@ import logging
import struct import struct
from bumble.gatt import ( from bumble.gatt import (
TemplateService,
Characteristic,
GATT_TELEPHONY_AND_MEDIA_AUDIO_SERVICE, GATT_TELEPHONY_AND_MEDIA_AUDIO_SERVICE,
GATT_TMAP_ROLE_CHARACTERISTIC, GATT_TMAP_ROLE_CHARACTERISTIC,
Characteristic,
TemplateService,
) )
from bumble.gatt_adapters import DelegatedCharacteristicProxyAdapter from bumble.gatt_adapters import DelegatedCharacteristicProxyAdapter
from bumble.gatt_client import CharacteristicProxy, ProfileServiceProxy, ServiceProxy from bumble.gatt_client import CharacteristicProxy, ProfileServiceProxy, ServiceProxy
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Logging # Logging
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------

View File

@@ -17,18 +17,12 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
import dataclasses import dataclasses
import enum import enum
from typing import Sequence from typing import Sequence
from bumble import att from bumble import att, device, gatt, gatt_adapters, gatt_client, utils
from bumble import utils
from bumble import device
from bumble import gatt
from bumble import gatt_adapters
from bumble import gatt_client
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Constants # Constants

View File

@@ -20,17 +20,18 @@ import struct
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional from typing import Optional
from bumble.device import Connection from bumble import utils
from bumble.att import ATT_Error from bumble.att import ATT_Error
from bumble.device import Connection
from bumble.gatt import ( from bumble.gatt import (
Characteristic, GATT_AUDIO_LOCATION_CHARACTERISTIC,
TemplateService, GATT_AUDIO_OUTPUT_DESCRIPTION_CHARACTERISTIC,
CharacteristicValue, GATT_VOLUME_OFFSET_CONTROL_POINT_CHARACTERISTIC,
GATT_VOLUME_OFFSET_CONTROL_SERVICE, GATT_VOLUME_OFFSET_CONTROL_SERVICE,
GATT_VOLUME_OFFSET_STATE_CHARACTERISTIC, GATT_VOLUME_OFFSET_STATE_CHARACTERISTIC,
GATT_AUDIO_LOCATION_CHARACTERISTIC, Characteristic,
GATT_VOLUME_OFFSET_CONTROL_POINT_CHARACTERISTIC, CharacteristicValue,
GATT_AUDIO_OUTPUT_DESCRIPTION_CHARACTERISTIC, TemplateService,
) )
from bumble.gatt_adapters import ( from bumble.gatt_adapters import (
DelegatedCharacteristicProxyAdapter, DelegatedCharacteristicProxyAdapter,
@@ -38,7 +39,6 @@ from bumble.gatt_adapters import (
UTF8CharacteristicProxyAdapter, UTF8CharacteristicProxyAdapter,
) )
from bumble.gatt_client import ProfileServiceProxy, ServiceProxy from bumble.gatt_client import ProfileServiceProxy, ServiceProxy
from bumble import utils
from bumble.profiles.bap import AudioLocation from bumble.profiles.bap import AudioLocation
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------

View File

@@ -17,33 +17,30 @@
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
import logging
import asyncio import asyncio
import collections import collections
import dataclasses import dataclasses
import enum 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 typing_extensions import Self
from bumble import core, l2cap, sdp, utils
from bumble import core
from bumble import l2cap
from bumble import sdp
from bumble import utils
from bumble.colors import color from bumble.colors import color
from bumble.core import ( from bumble.core import (
UUID,
BT_RFCOMM_PROTOCOL_ID,
PhysicalTransport,
BT_L2CAP_PROTOCOL_ID, BT_L2CAP_PROTOCOL_ID,
BT_RFCOMM_PROTOCOL_ID,
UUID,
InvalidArgumentError, InvalidArgumentError,
InvalidStateError,
InvalidPacketError, InvalidPacketError,
InvalidStateError,
PhysicalTransport,
ProtocolError, ProtocolError,
) )
if TYPE_CHECKING: if TYPE_CHECKING:
from bumble.device import Device, Connection from bumble.device import Connection, Device
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Logging # Logging
@@ -1047,8 +1044,8 @@ class Client:
self.l2cap_channel = await self.connection.create_l2cap_channel( self.l2cap_channel = await self.connection.create_l2cap_channel(
spec=l2cap.ClassicChannelSpec(psm=RFCOMM_PSM, mtu=self.l2cap_mtu) spec=l2cap.ClassicChannelSpec(psm=RFCOMM_PSM, mtu=self.l2cap_mtu)
) )
except ProtocolError as error: except ProtocolError:
logger.warning(f'L2CAP connection failed: {error}') logger.exception('L2CAP connection failed')
raise raise
assert self.l2cap_channel is not None assert self.l2cap_channel is not None

View File

@@ -16,6 +16,7 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
import struct import struct

View File

@@ -16,24 +16,26 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import logging import logging
import struct 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 typing_extensions import Self
from bumble import core, l2cap from bumble import core, l2cap
from bumble.colors import color from bumble.colors import color
from bumble.core import ( from bumble.core import (
InvalidStateError,
InvalidArgumentError, InvalidArgumentError,
InvalidPacketError, InvalidPacketError,
InvalidStateError,
ProtocolError, 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: if TYPE_CHECKING:
from bumble.device import Device, Connection from bumble.device import Connection, Device
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Logging # Logging
@@ -1084,8 +1086,8 @@ class Server:
def on_pdu(self, pdu): def on_pdu(self, pdu):
try: try:
sdp_pdu = SDP_PDU.from_bytes(pdu) sdp_pdu = SDP_PDU.from_bytes(pdu)
except Exception as error: except Exception:
logger.warning(color(f'failed to parse SDP Request PDU: {error}', 'red')) logger.exception(color('failed to parse SDP Request PDU', 'red'))
self.send_response( self.send_response(
SDP_ErrorResponse( SDP_ErrorResponse(
transaction_id=0, error_code=SDP_INVALID_REQUEST_SYNTAX_ERROR transaction_id=0, error_code=SDP_INVALID_REQUEST_SYNTAX_ERROR
@@ -1100,8 +1102,8 @@ class Server:
if handler: if handler:
try: try:
handler(sdp_pdu) handler(sdp_pdu)
except Exception as error: except Exception:
logger.exception(f'{color("!!! Exception in handler:", "red")} {error}') logger.exception(color("!!! Exception in handler:", "red"))
self.send_response( self.send_response(
SDP_ErrorResponse( SDP_ErrorResponse(
transaction_id=sdp_pdu.transaction_id, transaction_id=sdp_pdu.transaction_id,

View File

@@ -23,38 +23,41 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
import logging
import asyncio import asyncio
import enum import enum
from dataclasses import dataclass import logging
from dataclasses import dataclass, field
from typing import ( from typing import (
TYPE_CHECKING, TYPE_CHECKING,
Any, Any,
Awaitable, Awaitable,
Callable, Callable,
ClassVar,
Optional, Optional,
TypeVar,
cast, cast,
) )
from bumble import crypto, utils
from bumble.colors import color 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 ( from bumble.core import (
PhysicalTransport,
AdvertisingData, AdvertisingData,
InvalidArgumentError, InvalidArgumentError,
PhysicalTransport,
ProtocolError, ProtocolError,
name_or_number, 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.keys import PairingKeys
from bumble import crypto
from bumble import utils
if TYPE_CHECKING: if TYPE_CHECKING:
from bumble.device import Connection, Device from bumble.device import Connection, Device
@@ -200,31 +203,32 @@ def error_name(error_code: int) -> str:
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Classes # Classes
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@dataclass
class SMP_Command: class SMP_Command:
''' '''
See Bluetooth spec @ Vol 3, Part H - 3 SECURITY MANAGER PROTOCOL See Bluetooth spec @ Vol 3, Part H - 3 SECURITY MANAGER PROTOCOL
''' '''
smp_classes: dict[int, type[SMP_Command]] = {} smp_classes: ClassVar[dict[int, type[SMP_Command]]] = {}
fields: Any fields: ClassVar[Fields]
code = 0 code: int = field(default=0, init=False)
name = '' name: str = field(default='', init=False)
_payload: Optional[bytes] = field(default=None, init=False)
@staticmethod @classmethod
def from_bytes(pdu: bytes) -> "SMP_Command": def from_bytes(cls, pdu: bytes) -> "SMP_Command":
code = pdu[0] code = pdu[0]
cls = SMP_Command.smp_classes.get(code) subclass = SMP_Command.smp_classes.get(code)
if cls is None: if subclass is None:
instance = SMP_Command(pdu) instance = SMP_Command()
instance.name = SMP_Command.command_name(code) instance.name = SMP_Command.command_name(code)
instance.code = code instance.code = code
instance.payload = pdu
return instance return instance
self = cls.__new__(cls) instance = subclass(**HCI_Object.dict_from_bytes(pdu, 1, subclass.fields))
SMP_Command.__init__(self, pdu) instance.payload = pdu[1:]
if hasattr(self, 'fields'): return instance
self.init_from_bytes(pdu, 1)
return self
@staticmethod @staticmethod
def command_name(code: int) -> str: def command_name(code: int) -> str:
@@ -264,36 +268,35 @@ class SMP_Command:
def keypress_notification_type_name(notification_type: int) -> str: def keypress_notification_type_name(notification_type: int) -> str:
return name_or_number(SMP_KEYPRESS_NOTIFICATION_TYPE_NAMES, notification_type) return name_or_number(SMP_KEYPRESS_NOTIFICATION_TYPE_NAMES, notification_type)
@staticmethod _Command = TypeVar("_Command", bound="SMP_Command")
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
# Register a factory for this class @classmethod
SMP_Command.smp_classes[cls.code] = cls 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: @property
if hasattr(self, 'fields') and kwargs: def payload(self) -> bytes:
HCI_Object.init_from_fields(self, self.fields, kwargs) if self._payload is None:
if pdu is None: self._payload = HCI_Object.dict_to_bytes(self.__dict__, self.fields)
pdu = bytes([self.code]) + HCI_Object.dict_to_bytes(kwargs, self.fields) return self._payload
self.pdu = pdu
def init_from_bytes(self, pdu: bytes, offset: int) -> None: @payload.setter
return HCI_Object.init_from_bytes(self, pdu, offset, self.fields) def payload(self, value: bytes) -> None:
self._payload = value
def __bytes__(self): def __bytes__(self):
return self.pdu return bytes([self.code]) + self.payload
def __str__(self): def __str__(self):
result = color(self.name, 'yellow') result = color(self.name, 'yellow')
@@ -306,206 +309,192 @@ class SMP_Command:
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@SMP_Command.subclass( @SMP_Command.subclass
[ @dataclass
('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},
),
]
)
class SMP_Pairing_Request_Command(SMP_Command): class SMP_Pairing_Request_Command(SMP_Command):
''' '''
See Bluetooth spec @ Vol 3, Part H - 3.5.1 Pairing Request See Bluetooth spec @ Vol 3, Part H - 3.5.1 Pairing Request
''' '''
io_capability: int io_capability: int = field(
oob_data_flag: int metadata=metadata({'size': 1, 'mapper': SMP_Command.io_capability_name})
auth_req: int )
maximum_encryption_key_size: int oob_data_flag: int = field(metadata=metadata(1))
initiator_key_distribution: int auth_req: int = field(
responder_key_distribution: int 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( @SMP_Command.subclass
[ @dataclass
('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},
),
]
)
class SMP_Pairing_Response_Command(SMP_Command): class SMP_Pairing_Response_Command(SMP_Command):
''' '''
See Bluetooth spec @ Vol 3, Part H - 3.5.2 Pairing Response See Bluetooth spec @ Vol 3, Part H - 3.5.2 Pairing Response
''' '''
io_capability: int io_capability: int = field(
oob_data_flag: int metadata=metadata({'size': 1, 'mapper': SMP_Command.io_capability_name})
auth_req: int )
maximum_encryption_key_size: int oob_data_flag: int = field(metadata=metadata(1))
initiator_key_distribution: int auth_req: int = field(
responder_key_distribution: int 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): class SMP_Pairing_Confirm_Command(SMP_Command):
''' '''
See Bluetooth spec @ Vol 3, Part H - 3.5.3 Pairing Confirm 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): class SMP_Pairing_Random_Command(SMP_Command):
''' '''
See Bluetooth spec @ Vol 3, Part H - 3.5.4 Pairing Random 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): class SMP_Pairing_Failed_Command(SMP_Command):
''' '''
See Bluetooth spec @ Vol 3, Part H - 3.5.5 Pairing Failed 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): class SMP_Pairing_Public_Key_Command(SMP_Command):
''' '''
See Bluetooth spec @ Vol 3, Part H - 3.5.6 Pairing Public Key See Bluetooth spec @ Vol 3, Part H - 3.5.6 Pairing Public Key
''' '''
public_key_x: bytes public_key_x: bytes = field(metadata=metadata(32))
public_key_y: bytes public_key_y: bytes = field(metadata=metadata(32))
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@SMP_Command.subclass( @SMP_Command.subclass
[ @dataclass
('dhkey_check', 16),
]
)
class SMP_Pairing_DHKey_Check_Command(SMP_Command): class SMP_Pairing_DHKey_Check_Command(SMP_Command):
''' '''
See Bluetooth spec @ Vol 3, Part H - 3.5.7 Pairing DHKey Check 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( @SMP_Command.subclass
[ @dataclass
(
'notification_type',
{'size': 1, 'mapper': SMP_Command.keypress_notification_type_name},
),
]
)
class SMP_Pairing_Keypress_Notification_Command(SMP_Command): class SMP_Pairing_Keypress_Notification_Command(SMP_Command):
''' '''
See Bluetooth spec @ Vol 3, Part H - 3.5.8 Keypress Notification 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): class SMP_Encryption_Information_Command(SMP_Command):
''' '''
See Bluetooth spec @ Vol 3, Part H - 3.6.2 Encryption Information 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): class SMP_Master_Identification_Command(SMP_Command):
''' '''
See Bluetooth spec @ Vol 3, Part H - 3.6.3 Master Identification See Bluetooth spec @ Vol 3, Part H - 3.6.3 Master Identification
''' '''
ediv: int ediv: int = field(metadata=metadata(2))
rand: bytes rand: bytes = field(metadata=metadata(8))
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@SMP_Command.subclass([('identity_resolving_key', 16)]) @SMP_Command.subclass
@dataclass
class SMP_Identity_Information_Command(SMP_Command): class SMP_Identity_Information_Command(SMP_Command):
''' '''
See Bluetooth spec @ Vol 3, Part H - 3.6.4 Identity Information 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( @SMP_Command.subclass
[ @dataclass
('addr_type', Address.ADDRESS_TYPE_SPEC),
('bd_addr', Address.parse_address_preceded_by_type),
]
)
class SMP_Identity_Address_Information_Command(SMP_Command): class SMP_Identity_Address_Information_Command(SMP_Command):
''' '''
See Bluetooth spec @ Vol 3, Part H - 3.6.5 Identity Address Information See Bluetooth spec @ Vol 3, Part H - 3.6.5 Identity Address Information
''' '''
addr_type: int addr_type: int = field(metadata=metadata(Address.ADDRESS_TYPE_SPEC))
bd_addr: Address 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): class SMP_Signing_Information_Command(SMP_Command):
''' '''
See Bluetooth spec @ Vol 3, Part H - 3.6.6 Signing Information 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( @SMP_Command.subclass
[ @dataclass
('auth_req', {'size': 1, 'mapper': SMP_Command.auth_req_str}),
]
)
class SMP_Security_Request_Command(SMP_Command): class SMP_Security_Request_Command(SMP_Command):
''' '''
See Bluetooth spec @ Vol 3, Part H - 3.6.7 Security Request 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: if response:
next_steps() next_steps()
return return
except Exception as error: except Exception:
logger.warning(f'exception while confirm: {error}') logger.exception('exception while confirm')
self.send_pairing_failed(SMP_CONFIRM_VALUE_FAILED_ERROR) self.send_pairing_failed(SMP_CONFIRM_VALUE_FAILED_ERROR)
@@ -911,8 +900,8 @@ class Session:
if response: if response:
next_steps() next_steps()
return return
except Exception as error: except Exception:
logger.warning(f'exception while prompting: {error}') logger.exception('exception while prompting')
self.send_pairing_failed(SMP_CONFIRM_VALUE_FAILED_ERROR) self.send_pairing_failed(SMP_CONFIRM_VALUE_FAILED_ERROR)
@@ -929,8 +918,8 @@ class Session:
return return
logger.debug(f'user input: {passkey}') logger.debug(f'user input: {passkey}')
next_steps(passkey) next_steps(passkey)
except Exception as error: except Exception:
logger.warning(f'exception while prompting: {error}') logger.exception('exception while prompting')
self.send_pairing_failed(SMP_PASSKEY_ENTRY_FAILED_ERROR) self.send_pairing_failed(SMP_PASSKEY_ENTRY_FAILED_ERROR)
self.connection.cancel_on_disconnection(prompt()) self.connection.cancel_on_disconnection(prompt())
@@ -946,7 +935,9 @@ class Session:
self.tk = self.passkey.to_bytes(16, byteorder='little') self.tk = self.passkey.to_bytes(16, byteorder='little')
logger.debug(f'TK from passkey = {self.tk.hex()}') logger.debug(f'TK from passkey = {self.tk.hex()}')
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: def input_passkey(self, next_steps: Optional[Callable[[], None]] = None) -> None:
# Prompt the user for the passkey displayed on the peer # Prompt the user for the passkey displayed on the peer
@@ -976,8 +967,8 @@ class Session:
try: try:
self.connection.cancel_on_disconnection(display_passkey()) self.connection.cancel_on_disconnection(display_passkey())
except Exception as error: except Exception:
logger.warning(f'exception while displaying passkey: {error}') logger.exception('exception while displaying passkey')
else: else:
self.input_passkey(next_steps) self.input_passkey(next_steps)
@@ -1422,8 +1413,8 @@ class Session:
if handler is not None: if handler is not None:
try: try:
handler(command) handler(command)
except Exception as error: except Exception:
logger.exception(f'{color("!!! Exception in handler:", "red")} {error}') logger.exception(color("!!! Exception in handler:", "red"))
response = SMP_Pairing_Failed_Command( response = SMP_Pairing_Failed_Command(
reason=SMP_UNSPECIFIED_REASON_ERROR reason=SMP_UNSPECIFIED_REASON_ERROR
) )
@@ -1444,8 +1435,8 @@ class Session:
# Check if the request should proceed # Check if the request should proceed
try: try:
accepted = await self.pairing_config.delegate.accept() accepted = await self.pairing_config.delegate.accept()
except Exception as error: except Exception:
logger.warning(f'exception while accepting: {error}') logger.exception('exception while accepting')
accepted = False accepted = False
if not accepted: if not accepted:
logger.debug('pairing rejected by delegate') logger.debug('pairing rejected by delegate')
@@ -1569,11 +1560,12 @@ class Session:
if self.pairing_method == PairingMethod.CTKD_OVER_CLASSIC: if self.pairing_method == PairingMethod.CTKD_OVER_CLASSIC:
# Authentication is already done in SMP, so remote shall start keys distribution immediately # Authentication is already done in SMP, so remote shall start keys distribution immediately
return return
elif self.sc:
if self.sc:
self.send_public_key_command()
if self.pairing_method == PairingMethod.PASSKEY: if self.pairing_method == PairingMethod.PASSKEY:
self.display_or_input_passkey() self.display_or_input_passkey()
self.send_public_key_command()
else: else:
if self.pairing_method == PairingMethod.PASSKEY: if self.pairing_method == PairingMethod.PASSKEY:
self.display_or_input_passkey(self.send_pairing_confirm_command) self.display_or_input_passkey(self.send_pairing_confirm_command)
@@ -1846,10 +1838,10 @@ class Session:
elif self.pairing_method == PairingMethod.PASSKEY: elif self.pairing_method == PairingMethod.PASSKEY:
self.send_pairing_confirm_command() self.send_pairing_confirm_command()
else: else:
# Send our public key back to the initiator
self.send_public_key_command()
def next_steps() -> None: def next_steps() -> None:
# Send our public key back to the initiator
self.send_public_key_command()
if self.pairing_method in ( if self.pairing_method in (
PairingMethod.JUST_WORKS, PairingMethod.JUST_WORKS,

View File

@@ -12,21 +12,21 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import datetime
import logging
import os
import struct
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from contextlib import contextmanager from contextlib import contextmanager
from enum import IntEnum from enum import IntEnum
import logging
import struct
import datetime
from typing import BinaryIO, Generator from typing import BinaryIO, Generator
import os
from bumble import core from bumble import core
from bumble.hci import HCI_COMMAND_PACKET, HCI_EVENT_PACKET from bumble.hci import HCI_COMMAND_PACKET, HCI_EVENT_PACKET
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Logging # Logging
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------

View File

@@ -15,18 +15,14 @@
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from contextlib import asynccontextmanager
import logging import logging
import os import os
import re
from typing import Optional from typing import Optional
from bumble import utils from bumble import utils
from bumble.transport.common import (
Transport,
SnoopingTransport,
TransportSpecError,
)
from bumble.snoop import create_snooper from bumble.snoop import create_snooper
from bumble.transport.common import SnoopingTransport, Transport, TransportSpecError
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Logging # Logging
@@ -48,8 +44,8 @@ def _wrap_transport(transport: Transport) -> Transport:
return SnoopingTransport.create_with( return SnoopingTransport.create_with(
transport, create_snooper(snooper_spec) transport, create_snooper(snooper_spec)
) )
except Exception as exc: except Exception:
logger.warning(f'Exception while creating snooper: {exc}') logger.exception('Exception while creating snooper')
return transport return transport
@@ -88,12 +84,14 @@ async def open_transport(name: str) -> Transport:
scheme, *tail = name.split(':', 1) scheme, *tail = name.split(':', 1)
spec = tail[0] if tail else None spec = tail[0] if tail else None
metadata = None metadata = None
if spec: if spec and (m := re.search(r'\[(\w+=\w+(?:,\w+=\w+)*,?)\]', spec)):
# Metadata may precede the spec metadata_str = m.group(1)
if spec.startswith('['): if m.start() == 0:
metadata_str, *tail = spec[1:].split(']') # <metadata><spec>
spec = tail[0] if tail else None spec = spec[m.end() :]
metadata = dict([entry.split('=') for entry in metadata_str.split(',')]) else:
spec = spec[: m.start()]
metadata = dict([entry.split('=') for entry in metadata_str.split(',')])
transport = await _open_transport(scheme, spec) transport = await _open_transport(scheme, spec)
if metadata: if metadata:
@@ -185,12 +183,18 @@ async def _open_transport(scheme: str, spec: Optional[str]) -> Transport:
return await open_android_netsim_transport(spec) 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 from bumble.transport.unix import open_unix_client_transport
assert spec assert spec
return await open_unix_client_transport(spec) return await open_unix_client_transport(spec)
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') raise TransportSpecError('unknown transport scheme')

View File

@@ -16,28 +16,27 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
import logging import logging
import grpc.aio
from typing import Optional, Union from typing import Optional, Union
import grpc.aio
from bumble.transport.common import ( from bumble.transport.common import (
PumpedTransport,
PumpedPacketSource,
PumpedPacketSink, PumpedPacketSink,
PumpedPacketSource,
PumpedTransport,
Transport, Transport,
TransportSpecError, TransportSpecError,
) )
# pylint: disable=no-name-in-module # 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 ( from bumble.transport.grpc_protobuf.emulated_bluetooth_pb2_grpc import (
EmulatedBluetoothServiceStub, EmulatedBluetoothServiceStub,
) )
from bumble.transport.grpc_protobuf.emulated_bluetooth_packets_pb2 import HCIPacket
from bumble.transport.grpc_protobuf.emulated_bluetooth_vhci_pb2_grpc import ( from bumble.transport.grpc_protobuf.emulated_bluetooth_vhci_pb2_grpc import (
VhciForwardingServiceStub, VhciForwardingServiceStub,
) )
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Logging # Logging
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -77,21 +76,17 @@ async def open_android_emulator_transport(spec: Optional[str]) -> Transport:
# Parse the parameters # Parse the parameters
mode = 'host' mode = 'host'
server_host = 'localhost' server_address = 'localhost:8554'
server_port = '8554'
if spec: if spec:
params = spec.split(',') params = spec.split(',')
for param in params: for param in params:
if param.startswith('mode='): if param.startswith('mode='):
mode = param.split('=')[1] mode = param.split('=')[1]
elif ':' in param:
server_host, server_port = param.split(':')
else: else:
raise TransportSpecError('invalid parameter') server_address = param
# Connect to the gRPC server # Connect to the gRPC server
server_address = f'{server_host}:{server_port}' logger.debug('connecting to gRPC server at %s', server_address)
logger.debug(f'connecting to gRPC server at {server_address}')
channel = grpc.aio.insecure_channel(server_address) channel = grpc.aio.insecure_channel(server_address)
service: Union[EmulatedBluetoothServiceStub, VhciForwardingServiceStub] service: Union[EmulatedBluetoothServiceStub, VhciForwardingServiceStub]

View File

@@ -29,28 +29,27 @@ import grpc.aio
import bumble import bumble
from bumble.transport.common import ( from bumble.transport.common import (
ParserSource, ParserSource,
PumpedTransport,
PumpedPacketSource,
PumpedPacketSink, PumpedPacketSink,
PumpedPacketSource,
PumpedTransport,
Transport, Transport,
TransportSpecError,
TransportInitError, TransportInitError,
TransportSpecError,
) )
# pylint: disable=no-name-in-module # pylint: disable=no-name-in-module
from bumble.transport.grpc_protobuf.netsim.packet_streamer_pb2_grpc import ( from bumble.transport.grpc_protobuf.netsim.common_pb2 import ChipKind
PacketStreamerStub, from bumble.transport.grpc_protobuf.netsim.hci_packet_pb2 import HCIPacket
PacketStreamerServicer,
add_PacketStreamerServicer_to_server,
)
from bumble.transport.grpc_protobuf.netsim.packet_streamer_pb2 import ( from bumble.transport.grpc_protobuf.netsim.packet_streamer_pb2 import (
PacketRequest, PacketRequest,
PacketResponse, 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.startup_pb2 import Chip, ChipInfo, DeviceInfo
from bumble.transport.grpc_protobuf.netsim.common_pb2 import ChipKind
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Logging # Logging
@@ -145,8 +144,6 @@ def publish_grpc_port(grpc_port: int, instance_number: int) -> bool:
async def open_android_netsim_controller_transport( async def open_android_netsim_controller_transport(
server_host: Optional[str], server_port: int, options: dict[str, str] server_host: Optional[str], server_port: int, options: dict[str, str]
) -> Transport: ) -> Transport:
if not server_port:
raise TransportSpecError('invalid port')
if server_host == '_' or not server_host: if server_host == '_' or not server_host:
server_host = 'localhost' server_host = 'localhost'
@@ -168,14 +165,16 @@ async def open_android_netsim_controller_transport(
await self.pump_loop() await self.pump_loop()
except asyncio.CancelledError: except asyncio.CancelledError:
logger.debug('Pump task canceled') 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): async def pump_loop(self):
while True: while True:
request = await self.context.read() request = await self.context.read()
if request == grpc.aio.EOF: if request == grpc.aio.EOF:
logger.debug('End of request stream') logger.debug('End of request stream')
self.done.set_result(None) if not self.done.done():
self.done.set_result(None)
return return
# If we're not initialized yet, wait for a init packet. # 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): async def wait_for_termination(self):
await self.done await self.done
server_address = f'{server_host}:{server_port}'
class Server(PacketStreamerServicer, ParserSource): class Server(PacketStreamerServicer, ParserSource):
def __init__(self): def __init__(self):
PacketStreamerServicer.__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. # a server listening on that port, we get an exception.
self.grpc_server = grpc.aio.server(options=(('grpc.so_reuseport', 0),)) self.grpc_server = grpc.aio.server(options=(('grpc.so_reuseport', 0),))
add_PacketStreamerServicer_to_server(self, self.grpc_server) add_PacketStreamerServicer_to_server(self, self.grpc_server)
self.grpc_server.add_insecure_port(f'{server_host}:{server_port}') self.port = self.grpc_server.add_insecure_port(server_address)
logger.debug(f'gRPC server listening on {server_host}:{server_port}') logger.debug('gRPC server listening on %s', server_address)
async def start(self): async def start(self):
logger.debug('Starting gRPC server') 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 [] params = spec.split(',') if spec else []
if params and ':' in params[0]: if params and ':' in params[0]:
# Explicit <host>:<port> # Explicit <host>:<port>
host, port_str = params[0].split(':') host, port_str = params[0].rsplit(':', maxsplit=1)
port = int(port_str) port = int(port_str)
params_offset = 1 params_offset = 1
else: else:

View File

@@ -16,19 +16,18 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
import contextlib
import struct
import asyncio import asyncio
import logging import contextlib
import io import io
import logging
import struct
from typing import Any, ContextManager, Optional, Protocol from typing import Any, ContextManager, Optional, Protocol
from bumble import core from bumble import core, hci
from bumble import hci
from bumble.colors import color from bumble.colors import color
from bumble.snoop import Snooper from bumble.snoop import Snooper
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Logging # Logging
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -90,8 +89,8 @@ class PacketPump:
try: try:
# Deliver the packet to the sink # Deliver the packet to the sink
self.sink.on_packet(await self.reader.next_packet()) self.sink.on_packet(await self.reader.next_packet())
except Exception as error: except Exception:
logger.warning(f'!!! {error}') logger.exception('!!!')
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -158,10 +157,8 @@ class PacketParser:
if self.sink: if self.sink:
try: try:
self.sink.on_packet(bytes(self.packet)) self.sink.on_packet(bytes(self.packet))
except Exception as error: except Exception:
logger.exception( logger.exception(color('!!! Exception in on_packet', 'red'))
color(f'!!! Exception in on_packet: {error}', 'red')
)
self.reset() self.reset()
def set_packet_sink(self, sink: TransportSink) -> None: def set_packet_sink(self, sink: TransportSink) -> None:
@@ -378,7 +375,7 @@ class PumpedPacketSource(ParserSource):
self.terminated.set_result(None) self.terminated.set_result(None)
break break
except Exception as error: 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(): if not self.terminated.done():
self.terminated.set_exception(error) self.terminated.set_exception(error)
break break
@@ -409,8 +406,8 @@ class PumpedPacketSink:
except asyncio.CancelledError: except asyncio.CancelledError:
logger.debug('sink pump task done') logger.debug('sink pump task done')
break break
except Exception as error: except Exception:
logger.warning(f'exception while sending packet: {error}') logger.exception('exception while sending packet')
break break
self.pump_task = asyncio.create_task(pump_packets()) self.pump_task = asyncio.create_task(pump_packets())

Some files were not shown because too many files have changed in this diff Show More