forked from auracaster/bumble_mirror
Compare commits
87 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4bf0bc03af | |||
| 91ba2f61f1 | |||
| 116dc9b319 | |||
| 6f73b736d7 | |||
| 6091e6365d | |||
| 8bda7d2212 | |||
| 7aba36302a | |||
| ceefe8b2a5 | |||
| cd37027795 | |||
| bb2aa8229d | |||
| 4aed53c48d | |||
| 4a88e9a0cf | |||
| 3b8dd6f3cf | |||
| f41b7746d2 | |||
| 1b727741bf | |||
| d2bc8175fb | |||
| 84dfff290a | |||
| 17563e423a | |||
| 19d3616032 | |||
| 4a48309643 | |||
| 870217acb3 | |||
| f8077d7996 | |||
| 739907fa31 | |||
| a275c399a3 | |||
| c98275f385 | |||
| 0b19347bef | |||
| f61fd64c0b | |||
| ec12771be6 | |||
| 5b33e715da | |||
| b885f29318 | |||
| 7ca13188d5 | |||
| 89586d5d18 | |||
| 381032ceb9 | |||
| 12ca1c01f0 | |||
| a7111d0107 | |||
| c034297bc0 | |||
| a1eff958e6 | |||
| d6282a7247 | |||
| efdc770fde | |||
| 357d7f9c22 | |||
| 3bc08b4e0d | |||
| 982aaeabc3 | |||
| 1dc0950177 | |||
| df0fd74533 | |||
| 822f97fa84 | |||
| 4a6b0ef840 | |||
| a6ead0147e | |||
| 0665e9ca5c | |||
| b8b78ca1ee | |||
| d611d25802 | |||
| bf8a2cdcb5 | |||
| cce2e4d4e3 | |||
| 4bf7448a01 | |||
| 1b44e73f90 | |||
| 1a81c5d05c | |||
| d8a43f0151 | |||
| 858788f05e | |||
| 41f8797a4c | |||
| fc3fd7f25b | |||
| 48bbf9f1e0 | |||
| 3d6c595c6e | |||
| d9d971b8b3 | |||
| a5effb433b | |||
| 8802c95d31 | |||
| a184cae560 | |||
| fa6fe2aaca | |||
| 43a8cc37f8 | |||
| e45143e33d | |||
| 1c1b947455 | |||
| d7ddffd275 | |||
| 3cb97d2373 | |||
| bad037b010 | |||
| 88777710a4 | |||
| 0ab5b6c49a | |||
| 22ff0d5e32 | |||
| 2f5de37d76 | |||
| 799d730f88 | |||
| 1a05eebfdb | |||
| ebaa720e74 | |||
| a505badffc | |||
| 45d938c901 | |||
| a0498af626 | |||
| bf027cf38f | |||
| f2d7faa9af | |||
| a0248a1cdf | |||
| 630243e243 | |||
| 60e31884c8 |
@@ -6,6 +6,8 @@ on:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
workflow_dispatch:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -17,6 +17,8 @@ on:
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ main ]
|
||||
workflow_dispatch:
|
||||
branches: [main]
|
||||
schedule:
|
||||
- cron: '39 21 * * 4'
|
||||
|
||||
|
||||
@@ -7,6 +7,10 @@ on:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- 'extras/android/BtBench/**'
|
||||
workflow_dispatch:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'extras/android/BtBench/**'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -5,6 +5,8 @@ on:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
workflow_dispatch:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -6,6 +6,8 @@ on:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
workflow_dispatch:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
Vendored
+6
-1
@@ -102,5 +102,10 @@
|
||||
"."
|
||||
],
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.pytestEnabled": true
|
||||
"python.testing.pytestEnabled": true,
|
||||
"python-envs.defaultEnvManager": "ms-python.python:system",
|
||||
"python-envs.pythonProjects": [],
|
||||
"nrf-connect.applications": [
|
||||
"${workspaceFolder}/extras/zephyr/hci_usb"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -12,9 +12,6 @@ Apps
|
||||
## `show.py`
|
||||
Parse a file with HCI packets and print the details of each packet in a human readable form
|
||||
|
||||
## `link_relay.py`
|
||||
Simple WebSocket relay for virtual RemoteLink instances to communicate with each other through.
|
||||
|
||||
## `hci_bridge.py`
|
||||
This app acts as a simple bridge between two HCI transports, with a host on one side and
|
||||
a controller on the other. All the HCI packets bridged between the two are printed on the console
|
||||
|
||||
+20
-42
@@ -23,14 +23,8 @@ import contextlib
|
||||
import dataclasses
|
||||
import functools
|
||||
import logging
|
||||
import os
|
||||
import struct
|
||||
from typing import (
|
||||
Any,
|
||||
AsyncGenerator,
|
||||
Coroutine,
|
||||
Optional,
|
||||
)
|
||||
from typing import Any, AsyncGenerator, Coroutine, Optional
|
||||
|
||||
import click
|
||||
|
||||
@@ -41,19 +35,14 @@ except ImportError as e:
|
||||
"Try `python -m pip install \"git+https://github.com/google/liblc3.git\"`."
|
||||
) from e
|
||||
|
||||
from bumble.audio import io as audio_io
|
||||
from bumble.colors import color
|
||||
from bumble import company_ids
|
||||
from bumble import core
|
||||
from bumble import gatt
|
||||
from bumble import hci
|
||||
from bumble.profiles import bap
|
||||
from bumble.profiles import le_audio
|
||||
from bumble.profiles import pbp
|
||||
from bumble.profiles import bass
|
||||
import bumble.device
|
||||
import bumble.logging
|
||||
import bumble.transport
|
||||
import bumble.utils
|
||||
from bumble import company_ids, core, data_types, gatt, hci
|
||||
from bumble.audio import io as audio_io
|
||||
from bumble.colors import color
|
||||
from bumble.profiles import bap, bass, le_audio, pbp
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -128,7 +117,7 @@ class BroadcastScanner(bumble.utils.EventEmitter):
|
||||
broadcast_audio_announcement: Optional[bap.BroadcastAudioAnnouncement] = None
|
||||
basic_audio_announcement: Optional[bap.BasicAudioAnnouncement] = None
|
||||
appearance: Optional[core.Appearance] = None
|
||||
biginfo: Optional[bumble.device.BIGInfoAdvertisement] = None
|
||||
biginfo: Optional[bumble.device.BigInfoAdvertisement] = None
|
||||
manufacturer_data: Optional[tuple[str, bytes]] = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
@@ -255,8 +244,10 @@ class BroadcastScanner(bumble.utils.EventEmitter):
|
||||
print(color(' SDU Interval: ', 'magenta'), self.biginfo.sdu_interval)
|
||||
print(color(' Max SDU: ', 'magenta'), self.biginfo.max_sdu)
|
||||
print(color(' PHY: ', 'magenta'), self.biginfo.phy.name)
|
||||
print(color(' Framed: ', 'magenta'), self.biginfo.framed)
|
||||
print(color(' Encrypted: ', 'magenta'), self.biginfo.encrypted)
|
||||
print(color(' Framing: ', 'magenta'), self.biginfo.framing.name)
|
||||
print(
|
||||
color(' Encryption: ', 'magenta'), self.biginfo.encryption.name
|
||||
)
|
||||
|
||||
def on_sync_establishment(self) -> None:
|
||||
self.emit('sync_establishment')
|
||||
@@ -286,7 +277,7 @@ class BroadcastScanner(bumble.utils.EventEmitter):
|
||||
self.emit('change')
|
||||
|
||||
def on_biginfo_advertisement(
|
||||
self, advertisement: bumble.device.BIGInfoAdvertisement
|
||||
self, advertisement: bumble.device.BigInfoAdvertisement
|
||||
) -> None:
|
||||
self.biginfo = advertisement
|
||||
self.emit('change')
|
||||
@@ -868,21 +859,13 @@ async def run_transmit(
|
||||
)
|
||||
broadcast_audio_announcement = bap.BroadcastAudioAnnouncement(broadcast_id)
|
||||
|
||||
advertising_manufacturer_data = (
|
||||
b''
|
||||
if manufacturer_data is None
|
||||
else bytes(
|
||||
core.AdvertisingData(
|
||||
[
|
||||
(
|
||||
core.AdvertisingData.MANUFACTURER_SPECIFIC_DATA,
|
||||
struct.pack('<H', manufacturer_data[0])
|
||||
+ manufacturer_data[1],
|
||||
)
|
||||
]
|
||||
)
|
||||
advertising_data_types: list[core.DataType] = [
|
||||
data_types.BroadcastName(broadcast_name)
|
||||
]
|
||||
if manufacturer_data is not None:
|
||||
advertising_data_types.append(
|
||||
data_types.ManufacturerSpecificData(*manufacturer_data)
|
||||
)
|
||||
)
|
||||
|
||||
advertising_set = await device.create_advertising_set(
|
||||
advertising_parameters=bumble.device.AdvertisingParameters(
|
||||
@@ -894,12 +877,7 @@ async def run_transmit(
|
||||
),
|
||||
advertising_data=(
|
||||
broadcast_audio_announcement.get_advertising_data()
|
||||
+ bytes(
|
||||
core.AdvertisingData(
|
||||
[(core.AdvertisingData.BROADCAST_NAME, broadcast_name.encode())]
|
||||
)
|
||||
)
|
||||
+ advertising_manufacturer_data
|
||||
+ bytes(core.AdvertisingData(advertising_data_types))
|
||||
),
|
||||
periodic_advertising_parameters=bumble.device.PeriodicAdvertisingParameters(
|
||||
periodic_advertising_interval_min=80,
|
||||
@@ -1233,7 +1211,7 @@ def transmit(
|
||||
|
||||
|
||||
def main():
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||
bumble.logging.setup_basic_logging()
|
||||
auracast()
|
||||
|
||||
|
||||
|
||||
+371
-29
@@ -19,7 +19,6 @@ import asyncio
|
||||
import dataclasses
|
||||
import enum
|
||||
import logging
|
||||
import os
|
||||
import statistics
|
||||
import struct
|
||||
import time
|
||||
@@ -27,26 +26,39 @@ from typing import Optional
|
||||
|
||||
import click
|
||||
|
||||
import bumble.core
|
||||
import bumble.logging
|
||||
import bumble.rfcomm
|
||||
from bumble import l2cap
|
||||
from bumble.colors import color
|
||||
from bumble.core import (
|
||||
PhysicalTransport,
|
||||
BT_L2CAP_PROTOCOL_ID,
|
||||
BT_RFCOMM_PROTOCOL_ID,
|
||||
UUID,
|
||||
CommandTimeoutError,
|
||||
ConnectionPHY,
|
||||
PhysicalTransport,
|
||||
)
|
||||
from bumble.device import (
|
||||
CigParameters,
|
||||
CisLink,
|
||||
Connection,
|
||||
ConnectionParametersPreferences,
|
||||
Device,
|
||||
Peer,
|
||||
)
|
||||
from bumble.colors import color
|
||||
from bumble.device import Connection, ConnectionParametersPreferences, Device, Peer
|
||||
from bumble.gatt import Characteristic, CharacteristicValue, Service
|
||||
from bumble.hci import (
|
||||
HCI_LE_1M_PHY,
|
||||
HCI_LE_2M_PHY,
|
||||
HCI_LE_CODED_PHY,
|
||||
Role,
|
||||
HCI_Constant,
|
||||
HCI_Error,
|
||||
HCI_IsoDataPacket,
|
||||
HCI_StatusError,
|
||||
Role,
|
||||
)
|
||||
from bumble.pairing import PairingConfig
|
||||
from bumble.sdp import (
|
||||
SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
|
||||
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
@@ -56,12 +68,8 @@ from bumble.sdp import (
|
||||
DataElement,
|
||||
ServiceAttribute,
|
||||
)
|
||||
from bumble.transport import open_transport_or_link
|
||||
import bumble.rfcomm
|
||||
import bumble.core
|
||||
from bumble.transport import open_transport
|
||||
from bumble.utils import AsyncRunner
|
||||
from bumble.pairing import PairingConfig
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -83,11 +91,21 @@ SPEED_TX_UUID = 'E789C754-41A1-45F4-A948-A0A1A90DBA53'
|
||||
SPEED_RX_UUID = '016A2CC7-E14B-4819-935F-1F56EAE4098D'
|
||||
|
||||
DEFAULT_RFCOMM_UUID = 'E6D55659-C8B4-4B85-96BB-B1143AF6D3AE'
|
||||
|
||||
DEFAULT_L2CAP_PSM = 128
|
||||
DEFAULT_L2CAP_MAX_CREDITS = 128
|
||||
DEFAULT_L2CAP_MTU = 1024
|
||||
DEFAULT_L2CAP_MPS = 1024
|
||||
|
||||
DEFAULT_ISO_MAX_SDU_C_TO_P = 251
|
||||
DEFAULT_ISO_MAX_SDU_P_TO_C = 251
|
||||
DEFAULT_ISO_SDU_INTERVAL_C_TO_P = 10000
|
||||
DEFAULT_ISO_SDU_INTERVAL_P_TO_C = 10000
|
||||
DEFAULT_ISO_MAX_TRANSPORT_LATENCY_C_TO_P = 35
|
||||
DEFAULT_ISO_MAX_TRANSPORT_LATENCY_P_TO_C = 35
|
||||
DEFAULT_ISO_RTN_C_TO_P = 3
|
||||
DEFAULT_ISO_RTN_P_TO_C = 3
|
||||
|
||||
DEFAULT_LINGER_TIME = 1.0
|
||||
DEFAULT_POST_CONNECTION_WAIT_TIME = 1.0
|
||||
|
||||
@@ -104,14 +122,14 @@ def le_phy_name(phy_id):
|
||||
)
|
||||
|
||||
|
||||
def print_connection_phy(phy):
|
||||
def print_connection_phy(phy: ConnectionPHY) -> None:
|
||||
logging.info(
|
||||
color('@@@ PHY: ', 'yellow') + f'TX:{le_phy_name(phy.tx_phy)}/'
|
||||
f'RX:{le_phy_name(phy.rx_phy)}'
|
||||
)
|
||||
|
||||
|
||||
def print_connection(connection):
|
||||
def print_connection(connection: Connection) -> None:
|
||||
params = []
|
||||
if connection.transport == PhysicalTransport.LE:
|
||||
params.append(
|
||||
@@ -136,6 +154,34 @@ def print_connection(connection):
|
||||
logging.info(color('@@@ Connection: ', 'yellow') + ' '.join(params))
|
||||
|
||||
|
||||
def print_cis_link(cis_link: CisLink) -> None:
|
||||
logging.info(color("@@@ CIS established", "green"))
|
||||
logging.info(color('@@@ ISO interval: ', 'green') + f"{cis_link.iso_interval}ms")
|
||||
logging.info(color('@@@ NSE: ', 'green') + f"{cis_link.nse}")
|
||||
logging.info(color('@@@ Central->Peripheral:', 'green'))
|
||||
if cis_link.phy_c_to_p is not None:
|
||||
logging.info(
|
||||
color('@@@ PHY: ', 'green') + f"{cis_link.phy_c_to_p.name}"
|
||||
)
|
||||
logging.info(
|
||||
color('@@@ Latency: ', 'green') + f"{cis_link.transport_latency_c_to_p}µs"
|
||||
)
|
||||
logging.info(color('@@@ BN: ', 'green') + f"{cis_link.bn_c_to_p}")
|
||||
logging.info(color('@@@ FT: ', 'green') + f"{cis_link.ft_c_to_p}")
|
||||
logging.info(color('@@@ Max PDU: ', 'green') + f"{cis_link.max_pdu_c_to_p}")
|
||||
logging.info(color('@@@ Peripheral->Central:', 'green'))
|
||||
if cis_link.phy_p_to_c is not None:
|
||||
logging.info(
|
||||
color('@@@ PHY: ', 'green') + f"{cis_link.phy_p_to_c.name}"
|
||||
)
|
||||
logging.info(
|
||||
color('@@@ Latency: ', 'green') + f"{cis_link.transport_latency_p_to_c}µs"
|
||||
)
|
||||
logging.info(color('@@@ BN: ', 'green') + f"{cis_link.bn_p_to_c}")
|
||||
logging.info(color('@@@ FT: ', 'green') + f"{cis_link.ft_p_to_c}")
|
||||
logging.info(color('@@@ Max PDU: ', 'green') + f"{cis_link.max_pdu_p_to_c}")
|
||||
|
||||
|
||||
def make_sdp_records(channel):
|
||||
return {
|
||||
0x00010001: [
|
||||
@@ -461,7 +507,8 @@ class Sender:
|
||||
self.bytes_sent += len(packet)
|
||||
await self.packet_io.send_packet(packet)
|
||||
|
||||
await self.done.wait()
|
||||
if self.packet_io.can_receive():
|
||||
await self.done.wait()
|
||||
|
||||
run_counter = f'[{run + 1} of {self.repeat + 1}]' if self.repeat else ''
|
||||
logging.info(color(f'=== {run_counter} Done!', 'magenta'))
|
||||
@@ -491,6 +538,9 @@ class Sender:
|
||||
)
|
||||
self.done.set()
|
||||
|
||||
def is_sender(self):
|
||||
return True
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Receiver
|
||||
@@ -538,7 +588,8 @@ class Receiver:
|
||||
logging.info(
|
||||
color(
|
||||
f'!!! Unexpected packet, expected {self.expected_packet_index} '
|
||||
f'but received {packet.sequence}'
|
||||
f'but received {packet.sequence}',
|
||||
'red',
|
||||
)
|
||||
)
|
||||
|
||||
@@ -581,6 +632,9 @@ class Receiver:
|
||||
await self.done.wait()
|
||||
logging.info(color('=== Done!', 'magenta'))
|
||||
|
||||
def is_sender(self):
|
||||
return False
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Ping
|
||||
@@ -716,7 +770,8 @@ class Ping:
|
||||
color(
|
||||
f'!!! Unexpected packet, '
|
||||
f'expected {self.next_expected_packet_index} '
|
||||
f'but received {packet.sequence}'
|
||||
f'but received {packet.sequence}',
|
||||
'red',
|
||||
)
|
||||
)
|
||||
|
||||
@@ -724,6 +779,9 @@ class Ping:
|
||||
self.done.set()
|
||||
return
|
||||
|
||||
def is_sender(self):
|
||||
return True
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Pong
|
||||
@@ -768,7 +826,8 @@ class Pong:
|
||||
logging.info(
|
||||
color(
|
||||
f'!!! Unexpected packet, expected {self.expected_packet_index} '
|
||||
f'but received {packet.sequence}'
|
||||
f'but received {packet.sequence}',
|
||||
'red',
|
||||
)
|
||||
)
|
||||
|
||||
@@ -790,6 +849,9 @@ class Pong:
|
||||
await self.done.wait()
|
||||
logging.info(color('=== Done!', 'magenta'))
|
||||
|
||||
def is_sender(self):
|
||||
return False
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# GattClient
|
||||
@@ -953,6 +1015,9 @@ class StreamedPacketIO:
|
||||
# pylint: disable-next=not-callable
|
||||
self.io_sink(struct.pack('>H', len(packet)) + packet)
|
||||
|
||||
def can_receive(self):
|
||||
return True
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# L2capClient
|
||||
@@ -1224,6 +1289,96 @@ class RfcommServer(StreamedPacketIO):
|
||||
await self.dlc.drain()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# IsoClient
|
||||
# -----------------------------------------------------------------------------
|
||||
class IsoClient(StreamedPacketIO):
|
||||
def __init__(
|
||||
self,
|
||||
device: Device,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.device = device
|
||||
self.ready = asyncio.Event()
|
||||
self.cis_link: Optional[CisLink] = None
|
||||
|
||||
async def on_connection(
|
||||
self, connection: Connection, cis_link: CisLink, sender: bool
|
||||
) -> None:
|
||||
connection.on(connection.EVENT_DISCONNECTION, self.on_disconnection)
|
||||
self.cis_link = cis_link
|
||||
self.io_sink = cis_link.write
|
||||
await cis_link.setup_data_path(
|
||||
cis_link.Direction.HOST_TO_CONTROLLER
|
||||
if sender
|
||||
else cis_link.Direction.CONTROLLER_TO_HOST
|
||||
)
|
||||
cis_link.sink = self.on_iso_packet
|
||||
self.ready.set()
|
||||
|
||||
def on_iso_packet(self, iso_packet: HCI_IsoDataPacket) -> None:
|
||||
self.on_packet(iso_packet.iso_sdu_fragment)
|
||||
|
||||
def on_disconnection(self, _):
|
||||
pass
|
||||
|
||||
async def drain(self):
|
||||
if self.cis_link is None:
|
||||
return
|
||||
await self.cis_link.drain()
|
||||
|
||||
def can_receive(self):
|
||||
return False
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# IsoServer
|
||||
# -----------------------------------------------------------------------------
|
||||
class IsoServer(StreamedPacketIO):
|
||||
def __init__(
|
||||
self,
|
||||
device: Device,
|
||||
):
|
||||
super().__init__()
|
||||
self.device = device
|
||||
self.cis_link: Optional[CisLink] = None
|
||||
self.ready = asyncio.Event()
|
||||
|
||||
logging.info(
|
||||
color(
|
||||
'### Listening for ISO connection',
|
||||
'yellow',
|
||||
)
|
||||
)
|
||||
|
||||
async def on_connection(
|
||||
self, connection: Connection, cis_link: CisLink, sender: bool
|
||||
) -> None:
|
||||
connection.on(connection.EVENT_DISCONNECTION, self.on_disconnection)
|
||||
self.io_sink = cis_link.write
|
||||
await cis_link.setup_data_path(
|
||||
cis_link.Direction.HOST_TO_CONTROLLER
|
||||
if sender
|
||||
else cis_link.Direction.CONTROLLER_TO_HOST
|
||||
)
|
||||
cis_link.sink = self.on_iso_packet
|
||||
self.ready.set()
|
||||
|
||||
def on_iso_packet(self, iso_packet: HCI_IsoDataPacket) -> None:
|
||||
self.on_packet(iso_packet.iso_sdu_fragment)
|
||||
|
||||
def on_disconnection(self, _):
|
||||
pass
|
||||
|
||||
async def drain(self):
|
||||
if self.cis_link is None:
|
||||
return
|
||||
await self.cis_link.drain()
|
||||
|
||||
def can_receive(self):
|
||||
return False
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Central
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -1232,13 +1387,22 @@ class Central(Connection.Listener):
|
||||
self,
|
||||
transport,
|
||||
peripheral_address,
|
||||
classic,
|
||||
scenario_factory,
|
||||
mode_factory,
|
||||
connection_interval,
|
||||
phy,
|
||||
authenticate,
|
||||
encrypt,
|
||||
iso,
|
||||
iso_sdu_interval_c_to_p,
|
||||
iso_sdu_interval_p_to_c,
|
||||
iso_max_sdu_c_to_p,
|
||||
iso_max_sdu_p_to_c,
|
||||
iso_max_transport_latency_c_to_p,
|
||||
iso_max_transport_latency_p_to_c,
|
||||
iso_rtn_c_to_p,
|
||||
iso_rtn_p_to_c,
|
||||
classic,
|
||||
extended_data_length,
|
||||
role_switch,
|
||||
le_scan,
|
||||
@@ -1250,6 +1414,15 @@ class Central(Connection.Listener):
|
||||
self.transport = transport
|
||||
self.peripheral_address = peripheral_address
|
||||
self.classic = classic
|
||||
self.iso = iso
|
||||
self.iso_sdu_interval_c_to_p = iso_sdu_interval_c_to_p
|
||||
self.iso_sdu_interval_p_to_c = iso_sdu_interval_p_to_c
|
||||
self.iso_max_sdu_c_to_p = iso_max_sdu_c_to_p
|
||||
self.iso_max_sdu_p_to_c = iso_max_sdu_p_to_c
|
||||
self.iso_max_transport_latency_c_to_p = iso_max_transport_latency_c_to_p
|
||||
self.iso_max_transport_latency_p_to_c = iso_max_transport_latency_p_to_c
|
||||
self.iso_rtn_c_to_p = iso_rtn_c_to_p
|
||||
self.iso_rtn_p_to_c = iso_rtn_p_to_c
|
||||
self.scenario_factory = scenario_factory
|
||||
self.mode_factory = mode_factory
|
||||
self.authenticate = authenticate
|
||||
@@ -1296,7 +1469,7 @@ class Central(Connection.Listener):
|
||||
|
||||
async def run(self):
|
||||
logging.info(color('>>> Connecting to HCI...', 'green'))
|
||||
async with await open_transport_or_link(self.transport) as (
|
||||
async with await open_transport(self.transport) as (
|
||||
hci_source,
|
||||
hci_sink,
|
||||
):
|
||||
@@ -1308,6 +1481,13 @@ class Central(Connection.Listener):
|
||||
)
|
||||
mode = self.mode_factory(self.device)
|
||||
scenario = self.scenario_factory(mode)
|
||||
self.device.classic_enabled = self.classic
|
||||
self.device.cis_enabled = self.iso
|
||||
|
||||
# Set up a pairing config factory with minimal requirements.
|
||||
self.device.pairing_config_factory = lambda _: PairingConfig(
|
||||
sc=False, mitm=False, bonding=False
|
||||
)
|
||||
|
||||
await pre_power_on(self.device, self.classic)
|
||||
await self.device.power_on()
|
||||
@@ -1392,7 +1572,72 @@ class Central(Connection.Listener):
|
||||
)
|
||||
)
|
||||
|
||||
await mode.on_connection(self.connection)
|
||||
# Setup ISO streams.
|
||||
if self.iso:
|
||||
if scenario.is_sender():
|
||||
sdu_interval_c_to_p = (
|
||||
self.iso_sdu_interval_c_to_p or DEFAULT_ISO_SDU_INTERVAL_C_TO_P
|
||||
)
|
||||
sdu_interval_p_to_c = self.iso_sdu_interval_p_to_c or 0
|
||||
max_transport_latency_c_to_p = (
|
||||
self.iso_max_transport_latency_c_to_p
|
||||
or DEFAULT_ISO_MAX_TRANSPORT_LATENCY_C_TO_P
|
||||
)
|
||||
max_transport_latency_p_to_c = (
|
||||
self.iso_max_transport_latency_p_to_c or 0
|
||||
)
|
||||
max_sdu_c_to_p = (
|
||||
self.iso_max_sdu_c_to_p or DEFAULT_ISO_MAX_SDU_C_TO_P
|
||||
)
|
||||
max_sdu_p_to_c = self.iso_max_sdu_p_to_c or 0
|
||||
rtn_c_to_p = self.iso_rtn_c_to_p or DEFAULT_ISO_RTN_C_TO_P
|
||||
rtn_p_to_c = self.iso_rtn_p_to_c or 0
|
||||
else:
|
||||
sdu_interval_p_to_c = (
|
||||
self.iso_sdu_interval_p_to_c or DEFAULT_ISO_SDU_INTERVAL_P_TO_C
|
||||
)
|
||||
sdu_interval_c_to_p = self.iso_sdu_interval_c_to_p or 0
|
||||
max_transport_latency_p_to_c = (
|
||||
self.iso_max_transport_latency_p_to_c
|
||||
or DEFAULT_ISO_MAX_TRANSPORT_LATENCY_P_TO_C
|
||||
)
|
||||
max_transport_latency_c_to_p = (
|
||||
self.iso_max_transport_latency_c_to_p or 0
|
||||
)
|
||||
max_sdu_p_to_c = (
|
||||
self.iso_max_sdu_p_to_c or DEFAULT_ISO_MAX_SDU_P_TO_C
|
||||
)
|
||||
max_sdu_c_to_p = self.iso_max_sdu_c_to_p or 0
|
||||
rtn_p_to_c = self.iso_rtn_p_to_c or DEFAULT_ISO_RTN_P_TO_C
|
||||
rtn_c_to_p = self.iso_rtn_c_to_p or 0
|
||||
cis_handles = await self.device.setup_cig(
|
||||
CigParameters(
|
||||
cig_id=1,
|
||||
sdu_interval_c_to_p=sdu_interval_c_to_p,
|
||||
sdu_interval_p_to_c=sdu_interval_p_to_c,
|
||||
max_transport_latency_c_to_p=max_transport_latency_c_to_p,
|
||||
max_transport_latency_p_to_c=max_transport_latency_p_to_c,
|
||||
cis_parameters=[
|
||||
CigParameters.CisParameters(
|
||||
cis_id=2,
|
||||
max_sdu_c_to_p=max_sdu_c_to_p,
|
||||
max_sdu_p_to_c=max_sdu_p_to_c,
|
||||
rtn_c_to_p=rtn_c_to_p,
|
||||
rtn_p_to_c=rtn_p_to_c,
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
cis_link = (
|
||||
await self.device.create_cis([(cis_handles[0], self.connection)])
|
||||
)[0]
|
||||
print_cis_link(cis_link)
|
||||
|
||||
await mode.on_connection(
|
||||
self.connection, cis_link, scenario.is_sender()
|
||||
)
|
||||
else:
|
||||
await mode.on_connection(self.connection)
|
||||
|
||||
await scenario.run()
|
||||
await asyncio.sleep(DEFAULT_LINGER_TIME)
|
||||
@@ -1428,6 +1673,7 @@ class Peripheral(Device.Listener, Connection.Listener):
|
||||
scenario_factory,
|
||||
mode_factory,
|
||||
classic,
|
||||
iso,
|
||||
extended_data_length,
|
||||
role_switch,
|
||||
le_scan,
|
||||
@@ -1437,6 +1683,7 @@ class Peripheral(Device.Listener, Connection.Listener):
|
||||
):
|
||||
self.transport = transport
|
||||
self.classic = classic
|
||||
self.iso = iso
|
||||
self.scenario_factory = scenario_factory
|
||||
self.mode_factory = mode_factory
|
||||
self.extended_data_length = extended_data_length
|
||||
@@ -1457,7 +1704,7 @@ class Peripheral(Device.Listener, Connection.Listener):
|
||||
|
||||
async def run(self):
|
||||
logging.info(color('>>> Connecting to HCI...', 'green'))
|
||||
async with await open_transport_or_link(self.transport) as (
|
||||
async with await open_transport(self.transport) as (
|
||||
hci_source,
|
||||
hci_sink,
|
||||
):
|
||||
@@ -1470,6 +1717,13 @@ class Peripheral(Device.Listener, Connection.Listener):
|
||||
self.device.listener = self
|
||||
self.mode = self.mode_factory(self.device)
|
||||
self.scenario = self.scenario_factory(self.mode)
|
||||
self.device.classic_enabled = self.classic
|
||||
self.device.cis_enabled = self.iso
|
||||
|
||||
# Set up a pairing config factory with minimal requirements.
|
||||
self.device.pairing_config_factory = lambda _: PairingConfig(
|
||||
sc=False, mitm=False, bonding=False
|
||||
)
|
||||
|
||||
await pre_power_on(self.device, self.classic)
|
||||
await self.device.power_on()
|
||||
@@ -1501,7 +1755,21 @@ class Peripheral(Device.Listener, Connection.Listener):
|
||||
logging.info(color('### Connected', 'cyan'))
|
||||
print_connection(self.connection)
|
||||
|
||||
await self.mode.on_connection(self.connection)
|
||||
if self.iso:
|
||||
|
||||
async def on_cis_request(cis_link: CisLink) -> None:
|
||||
logging.info(color("@@@ Accepting CIS", "green"))
|
||||
await self.device.accept_cis_request(cis_link)
|
||||
print_cis_link(cis_link)
|
||||
|
||||
await self.mode.on_connection(
|
||||
self.connection, cis_link, self.scenario.is_sender()
|
||||
)
|
||||
|
||||
self.connection.on(self.connection.EVENT_CIS_REQUEST, on_cis_request)
|
||||
else:
|
||||
await self.mode.on_connection(self.connection)
|
||||
|
||||
await self.scenario.run()
|
||||
await asyncio.sleep(DEFAULT_LINGER_TIME)
|
||||
|
||||
@@ -1613,6 +1881,12 @@ def create_mode_factory(ctx, default_mode):
|
||||
credits_threshold=ctx.obj['rfcomm_credits_threshold'],
|
||||
)
|
||||
|
||||
if mode == 'iso-server':
|
||||
return IsoServer(device)
|
||||
|
||||
if mode == 'iso-client':
|
||||
return IsoClient(device)
|
||||
|
||||
raise ValueError('invalid mode')
|
||||
|
||||
return create_mode
|
||||
@@ -1640,6 +1914,9 @@ def create_scenario_factory(ctx, default_scenario):
|
||||
return Receiver(packet_io, ctx.obj['linger'])
|
||||
|
||||
if scenario == 'ping':
|
||||
if isinstance(packet_io, (IsoClient, IsoServer)):
|
||||
raise ValueError('ping not supported with ISO')
|
||||
|
||||
return Ping(
|
||||
packet_io,
|
||||
start_delay=ctx.obj['start_delay'],
|
||||
@@ -1651,6 +1928,9 @@ def create_scenario_factory(ctx, default_scenario):
|
||||
)
|
||||
|
||||
if scenario == 'pong':
|
||||
if isinstance(packet_io, (IsoClient, IsoServer)):
|
||||
raise ValueError('pong not supported with ISO')
|
||||
|
||||
return Pong(packet_io, ctx.obj['linger'])
|
||||
|
||||
raise ValueError('invalid scenario')
|
||||
@@ -1674,6 +1954,8 @@ def create_scenario_factory(ctx, default_scenario):
|
||||
'l2cap-server',
|
||||
'rfcomm-client',
|
||||
'rfcomm-server',
|
||||
'iso-client',
|
||||
'iso-server',
|
||||
]
|
||||
),
|
||||
)
|
||||
@@ -1896,6 +2178,7 @@ def bench(
|
||||
ctx.obj['classic_page_scan'] = classic_page_scan
|
||||
ctx.obj['classic_inquiry_scan'] = classic_inquiry_scan
|
||||
ctx.obj['classic'] = mode in ('rfcomm-client', 'rfcomm-server')
|
||||
ctx.obj['iso'] = mode in ('iso-client', 'iso-server')
|
||||
|
||||
|
||||
@bench.command()
|
||||
@@ -1917,26 +2200,88 @@ def bench(
|
||||
@click.option('--phy', type=click.Choice(['1m', '2m', 'coded']), help='PHY to use')
|
||||
@click.option('--authenticate', is_flag=True, help='Authenticate (RFComm only)')
|
||||
@click.option('--encrypt', is_flag=True, help='Encrypt the connection (RFComm only)')
|
||||
@click.option(
|
||||
'--iso-sdu-interval-c-to-p',
|
||||
type=int,
|
||||
help='ISO SDU central -> peripheral (microseconds)',
|
||||
)
|
||||
@click.option(
|
||||
'--iso-sdu-interval-p-to-c',
|
||||
type=int,
|
||||
help='ISO SDU interval peripheral -> central (microseconds)',
|
||||
)
|
||||
@click.option(
|
||||
'--iso-max-sdu-c-to-p',
|
||||
type=int,
|
||||
help='ISO max SDU central -> peripheral',
|
||||
)
|
||||
@click.option(
|
||||
'--iso-max-sdu-p-to-c',
|
||||
type=int,
|
||||
help='ISO max SDU peripheral -> central',
|
||||
)
|
||||
@click.option(
|
||||
'--iso-max-transport-latency-c-to-p',
|
||||
type=int,
|
||||
help='ISO max transport latency central -> peripheral (milliseconds)',
|
||||
)
|
||||
@click.option(
|
||||
'--iso-max-transport-latency-p-to-c',
|
||||
type=int,
|
||||
help='ISO max transport latency peripheral -> central (milliseconds)',
|
||||
)
|
||||
@click.option(
|
||||
'--iso-rtn-c-to-p',
|
||||
type=int,
|
||||
help='ISO RTN central -> peripheral (integer count)',
|
||||
)
|
||||
@click.option(
|
||||
'--iso-rtn-p-to-c',
|
||||
type=int,
|
||||
help='ISO RTN peripheral -> central (integer count)',
|
||||
)
|
||||
@click.pass_context
|
||||
def central(
|
||||
ctx, transport, peripheral_address, connection_interval, phy, authenticate, encrypt
|
||||
ctx,
|
||||
transport,
|
||||
peripheral_address,
|
||||
connection_interval,
|
||||
phy,
|
||||
authenticate,
|
||||
encrypt,
|
||||
iso_sdu_interval_c_to_p,
|
||||
iso_sdu_interval_p_to_c,
|
||||
iso_max_sdu_c_to_p,
|
||||
iso_max_sdu_p_to_c,
|
||||
iso_max_transport_latency_c_to_p,
|
||||
iso_max_transport_latency_p_to_c,
|
||||
iso_rtn_c_to_p,
|
||||
iso_rtn_p_to_c,
|
||||
):
|
||||
"""Run as a central (initiates the connection)"""
|
||||
scenario_factory = create_scenario_factory(ctx, 'send')
|
||||
mode_factory = create_mode_factory(ctx, 'gatt-client')
|
||||
classic = ctx.obj['classic']
|
||||
|
||||
async def run_central():
|
||||
await Central(
|
||||
transport,
|
||||
peripheral_address,
|
||||
classic,
|
||||
scenario_factory,
|
||||
mode_factory,
|
||||
connection_interval,
|
||||
phy,
|
||||
authenticate,
|
||||
encrypt or authenticate,
|
||||
ctx.obj['iso'],
|
||||
iso_sdu_interval_c_to_p,
|
||||
iso_sdu_interval_p_to_c,
|
||||
iso_max_sdu_c_to_p,
|
||||
iso_max_sdu_p_to_c,
|
||||
iso_max_transport_latency_c_to_p,
|
||||
iso_max_transport_latency_p_to_c,
|
||||
iso_rtn_c_to_p,
|
||||
iso_rtn_p_to_c,
|
||||
ctx.obj['classic'],
|
||||
ctx.obj['extended_data_length'],
|
||||
ctx.obj['role_switch'],
|
||||
ctx.obj['le_scan'],
|
||||
@@ -1962,6 +2307,7 @@ def peripheral(ctx, transport):
|
||||
scenario_factory,
|
||||
mode_factory,
|
||||
ctx.obj['classic'],
|
||||
ctx.obj['iso'],
|
||||
ctx.obj['extended_data_length'],
|
||||
ctx.obj['role_switch'],
|
||||
ctx.obj['le_scan'],
|
||||
@@ -1974,11 +2320,7 @@ def peripheral(ctx, transport):
|
||||
|
||||
|
||||
def main():
|
||||
logging.basicConfig(
|
||||
level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper(),
|
||||
format="[%(asctime)s.%(msecs)03d] %(levelname)s:%(name)s:%(message)s",
|
||||
datefmt="%H:%M:%S",
|
||||
)
|
||||
bumble.logging.setup_basic_logging('INFO')
|
||||
bench()
|
||||
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
# limitations under the License.
|
||||
|
||||
import click
|
||||
|
||||
from bumble.colors import color
|
||||
from bumble.hci import Address
|
||||
from bumble.helpers import generate_irk, verify_rpa_with_irk
|
||||
|
||||
+23
-26
@@ -23,58 +23,55 @@ import asyncio
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import humanize
|
||||
from typing import Optional, Union
|
||||
from collections import OrderedDict
|
||||
from typing import Optional, Union
|
||||
|
||||
import click
|
||||
import humanize
|
||||
from prettytable import PrettyTable
|
||||
|
||||
from prompt_toolkit import Application
|
||||
from prompt_toolkit.history import FileHistory
|
||||
from prompt_toolkit.completion import Completer, Completion, NestedCompleter
|
||||
from prompt_toolkit.key_binding import KeyBindings
|
||||
from prompt_toolkit.formatted_text import ANSI
|
||||
from prompt_toolkit.styles import Style
|
||||
from prompt_toolkit.filters import Condition
|
||||
from prompt_toolkit.widgets import TextArea, Frame
|
||||
from prompt_toolkit.widgets.toolbars import FormattedTextToolbar
|
||||
from prompt_toolkit.data_structures import Point
|
||||
from prompt_toolkit.filters import Condition
|
||||
from prompt_toolkit.formatted_text import ANSI
|
||||
from prompt_toolkit.history import FileHistory
|
||||
from prompt_toolkit.key_binding import KeyBindings
|
||||
from prompt_toolkit.layout import (
|
||||
Layout,
|
||||
HSplit,
|
||||
Window,
|
||||
CompletionsMenu,
|
||||
Float,
|
||||
FormattedTextControl,
|
||||
FloatContainer,
|
||||
ConditionalContainer,
|
||||
Dimension,
|
||||
Float,
|
||||
FloatContainer,
|
||||
FormattedTextControl,
|
||||
HSplit,
|
||||
Layout,
|
||||
Window,
|
||||
)
|
||||
from prompt_toolkit.styles import Style
|
||||
from prompt_toolkit.widgets import Frame, TextArea
|
||||
from prompt_toolkit.widgets.toolbars import FormattedTextToolbar
|
||||
|
||||
from bumble import __version__
|
||||
import bumble.core
|
||||
from bumble import colors
|
||||
from bumble import __version__, colors
|
||||
from bumble.core import UUID, AdvertisingData
|
||||
from bumble.device import (
|
||||
Connection,
|
||||
ConnectionParametersPreferences,
|
||||
ConnectionPHY,
|
||||
Device,
|
||||
Connection,
|
||||
Peer,
|
||||
)
|
||||
from bumble.utils import AsyncRunner
|
||||
from bumble.transport import open_transport_or_link
|
||||
from bumble.gatt import Characteristic, Service, CharacteristicDeclaration, Descriptor
|
||||
from bumble.gatt import Characteristic, CharacteristicDeclaration, Descriptor, Service
|
||||
from bumble.gatt_client import CharacteristicProxy
|
||||
from bumble.hci import (
|
||||
Address,
|
||||
HCI_Constant,
|
||||
HCI_LE_1M_PHY,
|
||||
HCI_LE_2M_PHY,
|
||||
HCI_LE_CODED_PHY,
|
||||
Address,
|
||||
HCI_Constant,
|
||||
)
|
||||
|
||||
from bumble.transport import open_transport
|
||||
from bumble.utils import AsyncRunner
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
@@ -291,7 +288,7 @@ class ConsoleApp:
|
||||
async def run_async(self, device_config, transport):
|
||||
rssi_monitoring_task = asyncio.create_task(self.rssi_monitor_loop())
|
||||
|
||||
async with await open_transport_or_link(transport) as (hci_source, hci_sink):
|
||||
async with await open_transport(transport) as (hci_source, hci_sink):
|
||||
if device_config:
|
||||
self.device = Device.from_config_file_with_hci(
|
||||
device_config, hci_source, hci_sink
|
||||
|
||||
+63
-31
@@ -16,49 +16,48 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import os
|
||||
import logging
|
||||
import time
|
||||
|
||||
import click
|
||||
|
||||
from bumble.company_ids import COMPANY_IDENTIFIERS
|
||||
import bumble.logging
|
||||
from bumble.colors import color
|
||||
from bumble.company_ids import COMPANY_IDENTIFIERS
|
||||
from bumble.core import name_or_number
|
||||
from bumble.hci import (
|
||||
map_null_terminated_utf8_string,
|
||||
CodecID,
|
||||
LeFeature,
|
||||
HCI_LE_READ_BUFFER_SIZE_COMMAND,
|
||||
HCI_LE_READ_BUFFER_SIZE_V2_COMMAND,
|
||||
HCI_LE_READ_MAXIMUM_ADVERTISING_DATA_LENGTH_COMMAND,
|
||||
HCI_LE_READ_MAXIMUM_DATA_LENGTH_COMMAND,
|
||||
HCI_LE_READ_NUMBER_OF_SUPPORTED_ADVERTISING_SETS_COMMAND,
|
||||
HCI_LE_READ_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND,
|
||||
HCI_READ_BD_ADDR_COMMAND,
|
||||
HCI_READ_BUFFER_SIZE_COMMAND,
|
||||
HCI_READ_LOCAL_NAME_COMMAND,
|
||||
HCI_SUCCESS,
|
||||
HCI_VERSION_NAMES,
|
||||
LMP_VERSION_NAMES,
|
||||
CodecID,
|
||||
HCI_Command,
|
||||
HCI_Command_Complete_Event,
|
||||
HCI_Command_Status_Event,
|
||||
HCI_READ_BUFFER_SIZE_COMMAND,
|
||||
HCI_Read_Buffer_Size_Command,
|
||||
HCI_LE_READ_BUFFER_SIZE_V2_COMMAND,
|
||||
HCI_LE_Read_Buffer_Size_V2_Command,
|
||||
HCI_READ_BD_ADDR_COMMAND,
|
||||
HCI_Read_BD_ADDR_Command,
|
||||
HCI_READ_LOCAL_NAME_COMMAND,
|
||||
HCI_Read_Local_Name_Command,
|
||||
HCI_LE_READ_BUFFER_SIZE_COMMAND,
|
||||
HCI_LE_Read_Buffer_Size_Command,
|
||||
HCI_LE_READ_MAXIMUM_DATA_LENGTH_COMMAND,
|
||||
HCI_LE_Read_Maximum_Data_Length_Command,
|
||||
HCI_LE_READ_NUMBER_OF_SUPPORTED_ADVERTISING_SETS_COMMAND,
|
||||
HCI_LE_Read_Number_Of_Supported_Advertising_Sets_Command,
|
||||
HCI_LE_READ_MAXIMUM_ADVERTISING_DATA_LENGTH_COMMAND,
|
||||
HCI_LE_Read_Buffer_Size_V2_Command,
|
||||
HCI_LE_Read_Maximum_Advertising_Data_Length_Command,
|
||||
HCI_LE_READ_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND,
|
||||
HCI_LE_Read_Maximum_Data_Length_Command,
|
||||
HCI_LE_Read_Number_Of_Supported_Advertising_Sets_Command,
|
||||
HCI_LE_Read_Suggested_Default_Data_Length_Command,
|
||||
HCI_Read_BD_ADDR_Command,
|
||||
HCI_Read_Buffer_Size_Command,
|
||||
HCI_Read_Local_Name_Command,
|
||||
HCI_Read_Local_Supported_Codecs_Command,
|
||||
HCI_Read_Local_Supported_Codecs_V2_Command,
|
||||
HCI_Read_Local_Version_Information_Command,
|
||||
LeFeature,
|
||||
map_null_terminated_utf8_string,
|
||||
)
|
||||
from bumble.host import Host
|
||||
from bumble.transport import open_transport_or_link
|
||||
from bumble.transport import open_transport
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -242,28 +241,43 @@ async def get_codecs_info(host: Host) -> None:
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def async_main(latency_probes, transport):
|
||||
async def async_main(
|
||||
latency_probes, latency_probe_interval, latency_probe_command, transport
|
||||
):
|
||||
print('<<< connecting to HCI...')
|
||||
async with await open_transport_or_link(transport) as (hci_source, hci_sink):
|
||||
async with await open_transport(transport) as (hci_source, hci_sink):
|
||||
print('<<< connected')
|
||||
|
||||
host = Host(hci_source, hci_sink)
|
||||
await host.reset()
|
||||
|
||||
# Measure the latency if requested
|
||||
# (we add an extra probe at the start, that we ignore, just to ensure that
|
||||
# the transport is primed)
|
||||
latencies = []
|
||||
if latency_probes:
|
||||
for _ in range(latency_probes):
|
||||
if latency_probe_command:
|
||||
probe_hci_command = HCI_Command.from_bytes(
|
||||
bytes.fromhex(latency_probe_command)
|
||||
)
|
||||
else:
|
||||
probe_hci_command = HCI_Read_Local_Version_Information_Command()
|
||||
|
||||
for iteration in range(1 + latency_probes):
|
||||
if latency_probe_interval:
|
||||
await asyncio.sleep(latency_probe_interval / 1000)
|
||||
start = time.time()
|
||||
await host.send_command(HCI_Read_Local_Version_Information_Command())
|
||||
latencies.append(1000 * (time.time() - start))
|
||||
await host.send_command(probe_hci_command)
|
||||
if iteration:
|
||||
latencies.append(1000 * (time.time() - start))
|
||||
print(
|
||||
color('HCI Command Latency:', 'yellow'),
|
||||
(
|
||||
f'min={min(latencies):.2f}, '
|
||||
f'max={max(latencies):.2f}, '
|
||||
f'average={sum(latencies)/len(latencies):.2f}'
|
||||
f'average={sum(latencies)/len(latencies):.2f},'
|
||||
),
|
||||
[f'{latency:.4}' for latency in latencies],
|
||||
'\n',
|
||||
)
|
||||
|
||||
@@ -311,10 +325,28 @@ async def async_main(latency_probes, transport):
|
||||
type=int,
|
||||
help='Send N commands to measure HCI transport latency statistics',
|
||||
)
|
||||
@click.option(
|
||||
'--latency-probe-interval',
|
||||
metavar='INTERVAL',
|
||||
type=int,
|
||||
help='Interval between latency probes (milliseconds)',
|
||||
)
|
||||
@click.option(
|
||||
'--latency-probe-command',
|
||||
metavar='COMMAND_HEX',
|
||||
help=(
|
||||
'Probe command (HCI Command packet bytes, in hex. Use 0177FC00 for'
|
||||
' a loopback test with the HCI remote proxy app)'
|
||||
),
|
||||
)
|
||||
@click.argument('transport')
|
||||
def main(latency_probes, transport):
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
|
||||
asyncio.run(async_main(latency_probes, transport))
|
||||
def main(latency_probes, latency_probe_interval, latency_probe_command, transport):
|
||||
bumble.logging.setup_basic_logging()
|
||||
asyncio.run(
|
||||
async_main(
|
||||
latency_probes, latency_probe_interval, latency_probe_command, transport
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -16,21 +16,22 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
import click
|
||||
|
||||
import bumble.logging
|
||||
from bumble.colors import color
|
||||
from bumble.hci import (
|
||||
HCI_READ_LOOPBACK_MODE_COMMAND,
|
||||
HCI_Read_Loopback_Mode_Command,
|
||||
HCI_WRITE_LOOPBACK_MODE_COMMAND,
|
||||
HCI_Read_Loopback_Mode_Command,
|
||||
HCI_Write_Loopback_Mode_Command,
|
||||
LoopbackMode,
|
||||
)
|
||||
from bumble.host import Host
|
||||
from bumble.transport import open_transport_or_link
|
||||
import click
|
||||
from bumble.transport import open_transport
|
||||
|
||||
|
||||
class Loopback:
|
||||
@@ -88,7 +89,7 @@ class Loopback:
|
||||
async def run(self):
|
||||
"""Run a loopback throughput test"""
|
||||
print(color('>>> Connecting to HCI...', 'green'))
|
||||
async with await open_transport_or_link(self.transport) as (
|
||||
async with await open_transport(self.transport) as (
|
||||
hci_source,
|
||||
hci_sink,
|
||||
):
|
||||
@@ -194,8 +195,7 @@ class Loopback:
|
||||
)
|
||||
@click.argument('transport')
|
||||
def main(packet_size, packet_count, transport):
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
|
||||
|
||||
bumble.logging.setup_basic_logging()
|
||||
loopback = Loopback(packet_size, packet_count, transport)
|
||||
asyncio.run(loopback.run())
|
||||
|
||||
|
||||
+4
-5
@@ -15,14 +15,13 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import logging
|
||||
import asyncio
|
||||
import sys
|
||||
import os
|
||||
|
||||
import bumble.logging
|
||||
from bumble.controller import Controller
|
||||
from bumble.link import LocalLink
|
||||
from bumble.transport import open_transport_or_link
|
||||
from bumble.transport import open_transport
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -42,7 +41,7 @@ async def async_main():
|
||||
transports = []
|
||||
controllers = []
|
||||
for index, transport_name in enumerate(sys.argv[1:]):
|
||||
transport = await open_transport_or_link(transport_name)
|
||||
transport = await open_transport(transport_name)
|
||||
transports.append(transport)
|
||||
controller = Controller(
|
||||
f'C{index}',
|
||||
@@ -62,7 +61,7 @@ async def async_main():
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def main():
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||
bumble.logging.setup_basic_logging()
|
||||
asyncio.run(async_main())
|
||||
|
||||
|
||||
|
||||
+6
-7
@@ -16,23 +16,22 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import os
|
||||
import logging
|
||||
from typing import Callable, Iterable, Optional
|
||||
|
||||
import click
|
||||
|
||||
from bumble.core import ProtocolError
|
||||
import bumble.logging
|
||||
from bumble.colors import color
|
||||
from bumble.core import ProtocolError
|
||||
from bumble.device import Device, Peer
|
||||
from bumble.gatt import Service
|
||||
from bumble.profiles.device_information_service import DeviceInformationServiceProxy
|
||||
from bumble.profiles.battery_service import BatteryServiceProxy
|
||||
from bumble.profiles.device_information_service import DeviceInformationServiceProxy
|
||||
from bumble.profiles.gap import GenericAccessServiceProxy
|
||||
from bumble.profiles.pacs import PublishedAudioCapabilitiesServiceProxy
|
||||
from bumble.profiles.tmap import TelephonyAndMediaAudioServiceProxy
|
||||
from bumble.profiles.vcs import VolumeControlServiceProxy
|
||||
from bumble.transport import open_transport_or_link
|
||||
from bumble.transport import open_transport
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -215,7 +214,7 @@ async def show_device_info(peer, done: Optional[asyncio.Future]) -> None:
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def async_main(device_config, encrypt, transport, address_or_name):
|
||||
async with await open_transport_or_link(transport) as (hci_source, hci_sink):
|
||||
async with await open_transport(transport) as (hci_source, hci_sink):
|
||||
|
||||
# Create a device
|
||||
if device_config:
|
||||
@@ -267,7 +266,7 @@ def main(device_config, encrypt, transport, address_or_name):
|
||||
Dump the GATT database on a remote device. If ADDRESS_OR_NAME is not specified,
|
||||
wait for an incoming connection.
|
||||
"""
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||
bumble.logging.setup_basic_logging()
|
||||
asyncio.run(async_main(device_config, encrypt, transport, address_or_name))
|
||||
|
||||
|
||||
|
||||
+5
-5
@@ -16,15 +16,15 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import os
|
||||
import logging
|
||||
|
||||
import click
|
||||
|
||||
import bumble.core
|
||||
import bumble.logging
|
||||
from bumble.colors import color
|
||||
from bumble.device import Device, Peer
|
||||
from bumble.gatt import show_services
|
||||
from bumble.transport import open_transport_or_link
|
||||
from bumble.transport import open_transport
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -60,7 +60,7 @@ async def dump_gatt_db(peer, done):
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def async_main(device_config, encrypt, transport, address_or_name):
|
||||
async with await open_transport_or_link(transport) as (hci_source, hci_sink):
|
||||
async with await open_transport(transport) as (hci_source, hci_sink):
|
||||
|
||||
# Create a device
|
||||
if device_config:
|
||||
@@ -112,7 +112,7 @@ def main(device_config, encrypt, transport, address_or_name):
|
||||
Dump the GATT database on a remote device. If ADDRESS_OR_NAME is not specified,
|
||||
wait for an incoming connection.
|
||||
"""
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||
bumble.logging.setup_basic_logging()
|
||||
asyncio.run(async_main(device_config, encrypt, transport, address_or_name))
|
||||
|
||||
|
||||
|
||||
+8
-9
@@ -16,20 +16,19 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import os
|
||||
import struct
|
||||
import logging
|
||||
|
||||
import click
|
||||
|
||||
import bumble.logging
|
||||
from bumble import l2cap
|
||||
from bumble.colors import color
|
||||
from bumble.device import Device, Peer
|
||||
from bumble.core import AdvertisingData
|
||||
from bumble.gatt import Service, Characteristic, CharacteristicValue
|
||||
from bumble.utils import AsyncRunner
|
||||
from bumble.transport import open_transport_or_link
|
||||
from bumble.device import Device, Peer
|
||||
from bumble.gatt import Characteristic, CharacteristicValue, Service
|
||||
from bumble.hci import HCI_Constant
|
||||
|
||||
from bumble.transport import open_transport
|
||||
from bumble.utils import AsyncRunner
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
@@ -325,7 +324,7 @@ async def run(
|
||||
receive_port,
|
||||
):
|
||||
print('<<< connecting to HCI...')
|
||||
async with await open_transport_or_link(hci_transport) as (hci_source, hci_sink):
|
||||
async with await open_transport(hci_transport) as (hci_source, hci_sink):
|
||||
print('<<< connected')
|
||||
|
||||
# Instantiate a bridge object
|
||||
@@ -383,6 +382,7 @@ def main(
|
||||
receive_host,
|
||||
receive_port,
|
||||
):
|
||||
bumble.logging.setup_basic_logging('WARNING')
|
||||
asyncio.run(
|
||||
run(
|
||||
hci_transport,
|
||||
@@ -397,6 +397,5 @@ def main(
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
+6
-5
@@ -12,14 +12,15 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import asyncio
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import logging
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
import bumble.logging
|
||||
from bumble import hci, transport
|
||||
from bumble.bridge import HCI_Bridge
|
||||
|
||||
@@ -46,14 +47,14 @@ async def async_main():
|
||||
return
|
||||
|
||||
print('>>> connecting to HCI...')
|
||||
async with await transport.open_transport_or_link(sys.argv[1]) as (
|
||||
async with await transport.open_transport(sys.argv[1]) as (
|
||||
hci_host_source,
|
||||
hci_host_sink,
|
||||
):
|
||||
print('>>> connected')
|
||||
|
||||
print('>>> connecting to HCI...')
|
||||
async with await transport.open_transport_or_link(sys.argv[2]) as (
|
||||
async with await transport.open_transport(sys.argv[2]) as (
|
||||
hci_controller_source,
|
||||
hci_controller_sink,
|
||||
):
|
||||
@@ -100,7 +101,7 @@ async def async_main():
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def main():
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||
bumble.logging.setup_basic_logging()
|
||||
asyncio.run(async_main())
|
||||
|
||||
|
||||
|
||||
@@ -16,16 +16,16 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
|
||||
import click
|
||||
|
||||
import bumble.logging
|
||||
from bumble import l2cap
|
||||
from bumble.colors import color
|
||||
from bumble.transport import open_transport_or_link
|
||||
from bumble.device import Device
|
||||
from bumble.utils import FlowControlAsyncPipe
|
||||
from bumble.hci import HCI_Constant
|
||||
from bumble.transport import open_transport
|
||||
from bumble.utils import FlowControlAsyncPipe
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -258,7 +258,7 @@ class ClientBridge:
|
||||
# -----------------------------------------------------------------------------
|
||||
async def run(device_config, hci_transport, bridge):
|
||||
print('<<< connecting to HCI...')
|
||||
async with await open_transport_or_link(hci_transport) as (hci_source, hci_sink):
|
||||
async with await open_transport(hci_transport) as (hci_source, hci_sink):
|
||||
print('<<< connected')
|
||||
|
||||
device = Device.from_config_file_with_hci(device_config, hci_source, hci_sink)
|
||||
@@ -356,6 +356,6 @@ def client(context, bluetooth_address, tcp_host, tcp_port):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
|
||||
if __name__ == '__main__':
|
||||
bumble.logging.setup_basic_logging('WARNING')
|
||||
cli(obj={}) # pylint: disable=no-value-for-parameter
|
||||
|
||||
+16
-21
@@ -20,31 +20,30 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import datetime
|
||||
import functools
|
||||
from importlib import resources
|
||||
import json
|
||||
import os
|
||||
import logging
|
||||
import pathlib
|
||||
import weakref
|
||||
import wave
|
||||
import weakref
|
||||
from importlib import resources
|
||||
|
||||
try:
|
||||
import lc3 # type: ignore # pylint: disable=E0401
|
||||
except ImportError as e:
|
||||
raise ImportError("Try `python -m pip install \".[lc3]\"`.") from e
|
||||
|
||||
import click
|
||||
import aiohttp.web
|
||||
import click
|
||||
|
||||
import bumble
|
||||
from bumble import utils
|
||||
from bumble.core import AdvertisingData
|
||||
import bumble.logging
|
||||
from bumble import data_types, utils
|
||||
from bumble.colors import color
|
||||
from bumble.device import Device, DeviceConfiguration, AdvertisingParameters, CisLink
|
||||
from bumble.transport import open_transport
|
||||
from bumble.profiles import ascs, bap, pacs
|
||||
from bumble.core import AdvertisingData
|
||||
from bumble.device import AdvertisingParameters, CisLink, Device, DeviceConfiguration
|
||||
from bumble.hci import Address, CodecID, CodingFormat, HCI_IsoDataPacket
|
||||
|
||||
from bumble.profiles import ascs, bap, pacs
|
||||
from bumble.transport import open_transport
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -331,17 +330,13 @@ class Speaker:
|
||||
advertising_data = bytes(
|
||||
AdvertisingData(
|
||||
[
|
||||
(
|
||||
AdvertisingData.COMPLETE_LOCAL_NAME,
|
||||
bytes(device_config.name, 'utf-8'),
|
||||
data_types.CompleteLocalName(device_config.name),
|
||||
data_types.Flags(
|
||||
AdvertisingData.Flags.LE_GENERAL_DISCOVERABLE_MODE
|
||||
| AdvertisingData.Flags.BR_EDR_NOT_SUPPORTED
|
||||
),
|
||||
(
|
||||
AdvertisingData.FLAGS,
|
||||
bytes([AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG]),
|
||||
),
|
||||
(
|
||||
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
|
||||
bytes(pacs.PublishedAudioCapabilitiesService.UUID),
|
||||
data_types.IncompleteListOf16BitServiceUUIDs(
|
||||
[pacs.PublishedAudioCapabilitiesService.UUID]
|
||||
),
|
||||
]
|
||||
)
|
||||
@@ -449,7 +444,7 @@ def speaker(ui_port: int, device_config: str, transport: str, lc3_file: str) ->
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def main():
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||
bumble.logging.setup_basic_logging()
|
||||
speaker()
|
||||
|
||||
|
||||
|
||||
@@ -1,289 +0,0 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# ----------------------------------------------------------------------------
|
||||
import sys
|
||||
import logging
|
||||
import json
|
||||
import asyncio
|
||||
import argparse
|
||||
import uuid
|
||||
import os
|
||||
from urllib.parse import urlparse
|
||||
import websockets
|
||||
|
||||
from bumble.colors import color
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Constants
|
||||
# ----------------------------------------------------------------------------
|
||||
DEFAULT_RELAY_PORT = 10723
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Utils
|
||||
# ----------------------------------------------------------------------------
|
||||
def error_to_json(error):
|
||||
return json.dumps({'error': error})
|
||||
|
||||
|
||||
def error_to_result(error):
|
||||
return f'result:{error_to_json(error)}'
|
||||
|
||||
|
||||
async def broadcast_message(message, connections):
|
||||
# Send to all the connections
|
||||
tasks = [connection.send_message(message) for connection in connections]
|
||||
if tasks:
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Connection class
|
||||
# ----------------------------------------------------------------------------
|
||||
class Connection:
|
||||
"""
|
||||
A Connection represents a client connected to the relay over a websocket
|
||||
"""
|
||||
|
||||
def __init__(self, room, websocket):
|
||||
self.room = room
|
||||
self.websocket = websocket
|
||||
self.address = str(uuid.uuid4())
|
||||
|
||||
async def send_message(self, message):
|
||||
try:
|
||||
logger.debug(color(f'->{self.address}: {message}', 'yellow'))
|
||||
return await self.websocket.send(message)
|
||||
except websockets.exceptions.WebSocketException as error:
|
||||
logger.info(f'! client "{self}" disconnected: {error}')
|
||||
await self.cleanup()
|
||||
|
||||
async def send_error(self, error):
|
||||
return await self.send_message(f'result:{error_to_json(error)}')
|
||||
|
||||
async def receive_message(self):
|
||||
try:
|
||||
message = await self.websocket.recv()
|
||||
logger.debug(color(f'<-{self.address}: {message}', 'blue'))
|
||||
return message
|
||||
except websockets.exceptions.WebSocketException as error:
|
||||
logger.info(color(f'! client "{self}" disconnected: {error}', 'red'))
|
||||
await self.cleanup()
|
||||
|
||||
async def cleanup(self):
|
||||
if self.room:
|
||||
await self.room.remove_connection(self)
|
||||
|
||||
def set_address(self, address):
|
||||
logger.info(f'Connection address changed: {self.address} -> {address}')
|
||||
self.address = address
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f'Connection(address="{self.address}", '
|
||||
f'client={self.websocket.remote_address[0]}:'
|
||||
f'{self.websocket.remote_address[1]})'
|
||||
)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Room class
|
||||
# ----------------------------------------------------------------------------
|
||||
class Room:
|
||||
"""
|
||||
A Room is a collection of bridged connections
|
||||
"""
|
||||
|
||||
def __init__(self, relay, name):
|
||||
self.relay = relay
|
||||
self.name = name
|
||||
self.observers = []
|
||||
self.connections = []
|
||||
|
||||
async def add_connection(self, connection):
|
||||
logger.info(f'New participant in {self.name}: {connection}')
|
||||
self.connections.append(connection)
|
||||
await self.broadcast_message(connection, f'joined:{connection.address}')
|
||||
|
||||
async def remove_connection(self, connection):
|
||||
if connection in self.connections:
|
||||
self.connections.remove(connection)
|
||||
await self.broadcast_message(connection, f'left:{connection.address}')
|
||||
|
||||
def find_connections_by_address(self, address):
|
||||
return [c for c in self.connections if c.address == address]
|
||||
|
||||
async def bridge_connection(self, connection):
|
||||
while True:
|
||||
# Wait for a message
|
||||
message = await connection.receive_message()
|
||||
|
||||
# Skip empty messages
|
||||
if message is None:
|
||||
return
|
||||
|
||||
# Parse the message to decide how to handle it
|
||||
if message.startswith('@'):
|
||||
# This is a targeted message
|
||||
await self.on_targeted_message(connection, message)
|
||||
elif message.startswith('/'):
|
||||
# This is an RPC request
|
||||
await self.on_rpc_request(connection, message)
|
||||
else:
|
||||
await connection.send_message(
|
||||
f'result:{error_to_json("error: invalid message")}'
|
||||
)
|
||||
|
||||
async def broadcast_message(self, sender, message):
|
||||
'''
|
||||
Send to all connections in the room except back to the sender
|
||||
'''
|
||||
await broadcast_message(message, [c for c in self.connections if c != sender])
|
||||
|
||||
async def on_rpc_request(self, connection, message):
|
||||
command, *params = message.split(' ', 1)
|
||||
if handler := getattr(
|
||||
self, f'on_{command[1:].lower().replace("-","_")}_command', None
|
||||
):
|
||||
try:
|
||||
result = await handler(connection, params)
|
||||
except Exception as error:
|
||||
result = error_to_result(error)
|
||||
else:
|
||||
result = error_to_result('unknown command')
|
||||
|
||||
await connection.send_message(result or 'result:{}')
|
||||
|
||||
async def on_targeted_message(self, connection, message):
|
||||
target, *payload = message.split(' ', 1)
|
||||
if not payload:
|
||||
return error_to_json('missing arguments')
|
||||
payload = payload[0]
|
||||
target = target[1:]
|
||||
|
||||
# Determine what targets to send to
|
||||
if target == '*':
|
||||
# Send to all connections in the room except the connection from which the
|
||||
# message was received
|
||||
connections = [c for c in self.connections if c != connection]
|
||||
else:
|
||||
connections = self.find_connections_by_address(target)
|
||||
if not connections:
|
||||
# Unicast with no recipient, let the sender know
|
||||
await connection.send_message(f'unreachable:{target}')
|
||||
|
||||
# Send to targets
|
||||
await broadcast_message(f'message:{connection.address}/{payload}', connections)
|
||||
|
||||
async def on_set_address_command(self, connection, params):
|
||||
if not params:
|
||||
return error_to_result('missing address')
|
||||
|
||||
current_address = connection.address
|
||||
new_address = params[0]
|
||||
connection.set_address(new_address)
|
||||
await self.broadcast_message(
|
||||
connection, f'address-changed:from={current_address},to={new_address}'
|
||||
)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
class Relay:
|
||||
"""
|
||||
A relay accepts connections with the following url: ws://<hostname>/<room>.
|
||||
Participants in a room can communicate with each other
|
||||
"""
|
||||
|
||||
def __init__(self, port):
|
||||
self.port = port
|
||||
self.rooms = {}
|
||||
self.observers = []
|
||||
|
||||
def start(self):
|
||||
logger.info(f'Starting Relay on port {self.port}')
|
||||
|
||||
# pylint: disable-next=no-member
|
||||
return websockets.serve(self.serve, '0.0.0.0', self.port, ping_interval=None)
|
||||
|
||||
async def serve_as_controller(self, connection):
|
||||
pass
|
||||
|
||||
async def serve(self, websocket, path):
|
||||
logger.debug(f'New connection with path {path}')
|
||||
|
||||
# Parse the path
|
||||
parsed = urlparse(path)
|
||||
|
||||
# Check if this is a controller client
|
||||
if parsed.path == '/':
|
||||
return await self.serve_as_controller(Connection('', websocket))
|
||||
|
||||
# Find or create a room for this connection
|
||||
room_name = parsed.path[1:].split('/')[0]
|
||||
if room_name not in self.rooms:
|
||||
self.rooms[room_name] = Room(self, room_name)
|
||||
room = self.rooms[room_name]
|
||||
|
||||
# Add the connection to the room
|
||||
connection = Connection(room, websocket)
|
||||
await room.add_connection(connection)
|
||||
|
||||
# Bridge until the connection is closed
|
||||
await room.bridge_connection(connection)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
def main():
|
||||
# Check the Python version
|
||||
if sys.version_info < (3, 6, 1):
|
||||
print('ERROR: Python 3.6.1 or higher is required')
|
||||
sys.exit(1)
|
||||
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||
|
||||
# Parse arguments
|
||||
arg_parser = argparse.ArgumentParser(description='Bumble Link Relay')
|
||||
arg_parser.add_argument('--log-level', default='INFO', help='logger level')
|
||||
arg_parser.add_argument('--log-config', help='logger config file (YAML)')
|
||||
arg_parser.add_argument(
|
||||
'--port', type=int, default=DEFAULT_RELAY_PORT, help='Port to listen on'
|
||||
)
|
||||
args = arg_parser.parse_args()
|
||||
|
||||
# Setup logger
|
||||
if args.log_config:
|
||||
from logging import config # pylint: disable=import-outside-toplevel
|
||||
|
||||
config.fileConfig(args.log_config)
|
||||
else:
|
||||
logging.basicConfig(level=getattr(logging, args.log_level.upper()))
|
||||
|
||||
# Start a relay
|
||||
relay = Relay(args.port)
|
||||
asyncio.get_event_loop().run_until_complete(relay.start())
|
||||
asyncio.get_event_loop().run_forever()
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,21 +0,0 @@
|
||||
[loggers]
|
||||
keys=root
|
||||
|
||||
[handlers]
|
||||
keys=stream_handler
|
||||
|
||||
[formatters]
|
||||
keys=formatter
|
||||
|
||||
[logger_root]
|
||||
level=DEBUG
|
||||
handlers=stream_handler
|
||||
|
||||
[handler_stream_handler]
|
||||
class=StreamHandler
|
||||
level=DEBUG
|
||||
formatter=formatter
|
||||
args=(sys.stderr,)
|
||||
|
||||
[formatter_formatter]
|
||||
format=%(asctime)s %(name)-12s %(levelname)-8s %(message)s
|
||||
+48
-53
@@ -16,42 +16,44 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import os
|
||||
import logging
|
||||
import os
|
||||
import struct
|
||||
|
||||
import click
|
||||
from prompt_toolkit.shortcuts import PromptSession
|
||||
|
||||
from bumble import data_types
|
||||
from bumble.a2dp import make_audio_sink_service_sdp_records
|
||||
from bumble.att import (
|
||||
ATT_INSUFFICIENT_AUTHENTICATION_ERROR,
|
||||
ATT_INSUFFICIENT_ENCRYPTION_ERROR,
|
||||
ATT_Error,
|
||||
)
|
||||
from bumble.colors import color
|
||||
from bumble.device import Device, Peer
|
||||
from bumble.transport import open_transport_or_link
|
||||
from bumble.pairing import OobData, PairingDelegate, PairingConfig
|
||||
from bumble.smp import OobContext, OobLegacyContext
|
||||
from bumble.smp import error_name as smp_error_name
|
||||
from bumble.keys import JsonKeyStore
|
||||
from bumble.core import (
|
||||
UUID,
|
||||
AdvertisingData,
|
||||
Appearance,
|
||||
ProtocolError,
|
||||
DataType,
|
||||
PhysicalTransport,
|
||||
UUID,
|
||||
ProtocolError,
|
||||
)
|
||||
from bumble.device import Device, Peer
|
||||
from bumble.gatt import (
|
||||
GATT_DEVICE_NAME_CHARACTERISTIC,
|
||||
GATT_GENERIC_ACCESS_SERVICE,
|
||||
GATT_HEART_RATE_SERVICE,
|
||||
GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC,
|
||||
Service,
|
||||
GATT_HEART_RATE_SERVICE,
|
||||
Characteristic,
|
||||
Service,
|
||||
)
|
||||
from bumble.hci import OwnAddressType
|
||||
from bumble.att import (
|
||||
ATT_Error,
|
||||
ATT_INSUFFICIENT_AUTHENTICATION_ERROR,
|
||||
ATT_INSUFFICIENT_ENCRYPTION_ERROR,
|
||||
)
|
||||
from bumble.keys import JsonKeyStore
|
||||
from bumble.pairing import OobData, PairingConfig, PairingDelegate
|
||||
from bumble.smp import OobContext, OobLegacyContext
|
||||
from bumble.smp import error_name as smp_error_name
|
||||
from bumble.transport import open_transport
|
||||
from bumble.utils import AsyncRunner
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -349,7 +351,7 @@ async def pair(
|
||||
Waiter.instance = Waiter(linger=linger)
|
||||
|
||||
print('<<< connecting to HCI...')
|
||||
async with await open_transport_or_link(hci_transport) as (hci_source, hci_sink):
|
||||
async with await open_transport(hci_transport) as (hci_source, hci_sink):
|
||||
print('<<< connected')
|
||||
|
||||
# Create a device to manage the host
|
||||
@@ -402,14 +404,19 @@ async def pair(
|
||||
# Create an OOB context if needed
|
||||
if oob:
|
||||
our_oob_context = OobContext()
|
||||
shared_data = (
|
||||
None
|
||||
if oob == '-'
|
||||
else OobData.from_ad(
|
||||
if oob == '-':
|
||||
shared_data = None
|
||||
legacy_context = OobLegacyContext()
|
||||
else:
|
||||
oob_data = OobData.from_ad(
|
||||
AdvertisingData.from_bytes(bytes.fromhex(oob))
|
||||
).shared_data
|
||||
)
|
||||
legacy_context = OobLegacyContext()
|
||||
)
|
||||
shared_data = oob_data.shared_data
|
||||
legacy_context = oob_data.legacy_context
|
||||
if legacy_context is None and not sc:
|
||||
print(color('OOB pairing in legacy mode requires TK', 'red'))
|
||||
return
|
||||
|
||||
oob_contexts = PairingConfig.OobConfig(
|
||||
our_context=our_oob_context,
|
||||
peer_data=shared_data,
|
||||
@@ -419,7 +426,9 @@ async def pair(
|
||||
print(color('@@@ OOB Data:', 'yellow'))
|
||||
if shared_data is None:
|
||||
oob_data = OobData(
|
||||
address=device.random_address, shared_data=our_oob_context.share()
|
||||
address=device.random_address,
|
||||
shared_data=our_oob_context.share(),
|
||||
legacy_context=(None if sc else legacy_context),
|
||||
)
|
||||
print(
|
||||
color(
|
||||
@@ -427,7 +436,8 @@ async def pair(
|
||||
'yellow',
|
||||
)
|
||||
)
|
||||
print(color(f'@@@ TK={legacy_context.tk.hex()}', 'yellow'))
|
||||
if legacy_context:
|
||||
print(color(f'@@@ TK={legacy_context.tk.hex()}', 'yellow'))
|
||||
print(color('@@@-----------------------------------', 'yellow'))
|
||||
else:
|
||||
oob_contexts = None
|
||||
@@ -498,33 +508,21 @@ async def pair(
|
||||
if mode == 'dual':
|
||||
flags |= AdvertisingData.Flags.SIMULTANEOUS_LE_BR_EDR_CAPABLE
|
||||
|
||||
ad_structs = [
|
||||
(
|
||||
AdvertisingData.FLAGS,
|
||||
bytes([flags]),
|
||||
),
|
||||
(AdvertisingData.COMPLETE_LOCAL_NAME, 'Bumble'.encode()),
|
||||
advertising_data_types: list[DataType] = [
|
||||
data_types.Flags(flags),
|
||||
data_types.CompleteLocalName('Bumble'),
|
||||
]
|
||||
if service_uuids_16:
|
||||
ad_structs.append(
|
||||
(
|
||||
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
|
||||
b"".join(bytes(uuid) for uuid in service_uuids_16),
|
||||
)
|
||||
advertising_data_types.append(
|
||||
data_types.IncompleteListOf16BitServiceUUIDs(service_uuids_16)
|
||||
)
|
||||
if service_uuids_32:
|
||||
ad_structs.append(
|
||||
(
|
||||
AdvertisingData.INCOMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS,
|
||||
b"".join(bytes(uuid) for uuid in service_uuids_32),
|
||||
)
|
||||
advertising_data_types.append(
|
||||
data_types.IncompleteListOf32BitServiceUUIDs(service_uuids_32)
|
||||
)
|
||||
if service_uuids_128:
|
||||
ad_structs.append(
|
||||
(
|
||||
AdvertisingData.INCOMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS,
|
||||
b"".join(bytes(uuid) for uuid in service_uuids_128),
|
||||
)
|
||||
advertising_data_types.append(
|
||||
data_types.IncompleteListOf128BitServiceUUIDs(service_uuids_128)
|
||||
)
|
||||
|
||||
if advertise_appearance:
|
||||
@@ -551,13 +549,10 @@ async def pair(
|
||||
advertise_appearance_int = int(
|
||||
Appearance(category_enum, subcategory_enum)
|
||||
)
|
||||
ad_structs.append(
|
||||
(
|
||||
AdvertisingData.APPEARANCE,
|
||||
struct.pack('<H', advertise_appearance_int),
|
||||
)
|
||||
advertising_data_types.append(
|
||||
data_types.Appearance(category_enum, subcategory_enum)
|
||||
)
|
||||
device.advertising_data = bytes(AdvertisingData(ad_structs))
|
||||
device.advertising_data = bytes(AdvertisingData(advertising_data_types))
|
||||
await device.start_advertising(
|
||||
auto_restart=True,
|
||||
own_address_type=(
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import asyncio
|
||||
import click
|
||||
import logging
|
||||
import json
|
||||
|
||||
from bumble.pandora import PandoraDevice, Config, serve
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import click
|
||||
|
||||
from bumble.pandora import Config, PandoraDevice, serve
|
||||
|
||||
BUMBLE_SERVER_GRPC_PORT = 7999
|
||||
ROOTCANAL_PORT_CUTTLEFISH = 7300
|
||||
|
||||
|
||||
+21
-25
@@ -16,55 +16,51 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import asyncio.subprocess
|
||||
import os
|
||||
import logging
|
||||
from typing import Optional, Union
|
||||
|
||||
import click
|
||||
|
||||
import bumble.logging
|
||||
from bumble.a2dp import (
|
||||
make_audio_source_service_sdp_records,
|
||||
A2DP_SBC_CODEC_TYPE,
|
||||
A2DP_MPEG_2_4_AAC_CODEC_TYPE,
|
||||
A2DP_NON_A2DP_CODEC_TYPE,
|
||||
A2DP_SBC_CODEC_TYPE,
|
||||
AacFrame,
|
||||
AacParser,
|
||||
AacPacketSource,
|
||||
AacMediaCodecInformation,
|
||||
SbcFrame,
|
||||
SbcParser,
|
||||
SbcPacketSource,
|
||||
SbcMediaCodecInformation,
|
||||
OpusPacket,
|
||||
OpusParser,
|
||||
OpusPacketSource,
|
||||
AacPacketSource,
|
||||
AacParser,
|
||||
OpusMediaCodecInformation,
|
||||
OpusPacket,
|
||||
OpusPacketSource,
|
||||
OpusParser,
|
||||
SbcFrame,
|
||||
SbcMediaCodecInformation,
|
||||
SbcPacketSource,
|
||||
SbcParser,
|
||||
make_audio_source_service_sdp_records,
|
||||
)
|
||||
from bumble.avrcp import Protocol as AvrcpProtocol
|
||||
from bumble.avdtp import (
|
||||
find_avdtp_service_with_connection,
|
||||
AVDTP_AUDIO_MEDIA_TYPE,
|
||||
AVDTP_DELAY_REPORTING_SERVICE_CATEGORY,
|
||||
MediaCodecCapabilities,
|
||||
MediaPacketPump,
|
||||
Protocol as AvdtpProtocol,
|
||||
)
|
||||
from bumble.avdtp import Protocol as AvdtpProtocol
|
||||
from bumble.avdtp import find_avdtp_service_with_connection
|
||||
from bumble.avrcp import Protocol as AvrcpProtocol
|
||||
from bumble.colors import color
|
||||
from bumble.core import (
|
||||
AdvertisingData,
|
||||
ConnectionError as BumbleConnectionError,
|
||||
DeviceClass,
|
||||
PhysicalTransport,
|
||||
)
|
||||
from bumble.core import AdvertisingData
|
||||
from bumble.core import ConnectionError as BumbleConnectionError
|
||||
from bumble.core import DeviceClass, PhysicalTransport
|
||||
from bumble.device import Connection, Device, DeviceConfiguration
|
||||
from bumble.hci import Address, HCI_CONNECTION_ALREADY_EXISTS_ERROR, HCI_Constant
|
||||
from bumble.hci import HCI_CONNECTION_ALREADY_EXISTS_ERROR, Address, HCI_Constant
|
||||
from bumble.pairing import PairingConfig
|
||||
from bumble.transport import open_transport
|
||||
from bumble.utils import AsyncRunner
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -599,7 +595,7 @@ def play(context, address, audio_format, audio_file):
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def main():
|
||||
logging.basicConfig(level=os.environ.get("BUMBLE_LOGLEVEL", "WARNING").upper())
|
||||
bumble.logging.setup_basic_logging("WARNING")
|
||||
player_cli()
|
||||
|
||||
|
||||
|
||||
+5
-11
@@ -16,21 +16,15 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
import click
|
||||
|
||||
import bumble.logging
|
||||
from bumble import core, hci, rfcomm, transport, utils
|
||||
from bumble.colors import color
|
||||
from bumble.device import Device, DeviceConfiguration, Connection
|
||||
from bumble import core
|
||||
from bumble import hci
|
||||
from bumble import rfcomm
|
||||
from bumble import transport
|
||||
from bumble import utils
|
||||
|
||||
from bumble.device import Connection, Device, DeviceConfiguration
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
@@ -406,7 +400,7 @@ class ClientBridge:
|
||||
# -----------------------------------------------------------------------------
|
||||
async def run(device_config, hci_transport, bridge):
|
||||
print("<<< connecting to HCI...")
|
||||
async with await transport.open_transport_or_link(hci_transport) as (
|
||||
async with await transport.open_transport(hci_transport) as (
|
||||
hci_source,
|
||||
hci_sink,
|
||||
):
|
||||
@@ -515,6 +509,6 @@ def client(context, bluetooth_address, tcp_host, tcp_port, authenticate, encrypt
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
logging.basicConfig(level=os.environ.get("BUMBLE_LOGLEVEL", "WARNING").upper())
|
||||
if __name__ == "__main__":
|
||||
bumble.logging.setup_basic_logging("WARNING")
|
||||
cli(obj={}) # pylint: disable=no-value-for-parameter
|
||||
|
||||
+18
-9
@@ -16,17 +16,17 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import os
|
||||
import logging
|
||||
|
||||
import click
|
||||
|
||||
import bumble.logging
|
||||
from bumble import data_types
|
||||
from bumble.colors import color
|
||||
from bumble.device import Device
|
||||
from bumble.transport import open_transport_or_link
|
||||
from bumble.device import Advertisement, Device
|
||||
from bumble.hci import HCI_LE_1M_PHY, HCI_LE_CODED_PHY, Address, HCI_Constant
|
||||
from bumble.keys import JsonKeyStore
|
||||
from bumble.smp import AddressResolver
|
||||
from bumble.device import Advertisement
|
||||
from bumble.hci import Address, HCI_Constant, HCI_LE_1M_PHY, HCI_LE_CODED_PHY
|
||||
from bumble.transport import open_transport
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -95,13 +95,22 @@ class AdvertisementPrinter:
|
||||
else:
|
||||
phy_info = ''
|
||||
|
||||
details = separator.join(
|
||||
[
|
||||
data_type.to_string(use_label=True)
|
||||
for data_type in data_types.data_types_from_advertising_data(
|
||||
advertisement.data
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
print(
|
||||
f'>>> {color(address, address_color)} '
|
||||
f'[{color(address_type_string, type_color)}]{address_qualifier}'
|
||||
f'{resolution_qualifier}:{separator}'
|
||||
f'{phy_info}'
|
||||
f'RSSI:{advertisement.rssi:4} {rssi_bar}{separator}'
|
||||
f'{advertisement.data.to_string(separator)}\n'
|
||||
f'{details}\n'
|
||||
)
|
||||
|
||||
def on_advertisement(self, advertisement):
|
||||
@@ -127,7 +136,7 @@ async def scan(
|
||||
transport,
|
||||
):
|
||||
print('<<< connecting to HCI...')
|
||||
async with await open_transport_or_link(transport) as (hci_source, hci_sink):
|
||||
async with await open_transport(transport) as (hci_source, hci_sink):
|
||||
print('<<< connected')
|
||||
|
||||
if device_config:
|
||||
@@ -237,7 +246,7 @@ def main(
|
||||
device_config,
|
||||
transport,
|
||||
):
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
|
||||
bumble.logging.setup_basic_logging('WARNING')
|
||||
asyncio.run(
|
||||
scan(
|
||||
min_rssi,
|
||||
|
||||
+4
-5
@@ -18,16 +18,15 @@
|
||||
import datetime
|
||||
import importlib
|
||||
import logging
|
||||
import os
|
||||
import struct
|
||||
|
||||
import click
|
||||
|
||||
from bumble.colors import color
|
||||
import bumble.logging
|
||||
from bumble import hci
|
||||
from bumble.transport.common import PacketReader
|
||||
from bumble.colors import color
|
||||
from bumble.helpers import PacketTracer
|
||||
|
||||
from bumble.transport.common import PacketReader
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -188,5 +187,5 @@ def main(format, vendor, filename):
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
if __name__ == '__main__':
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
|
||||
bumble.logging.setup_basic_logging('WARNING')
|
||||
main() # pylint: disable=no-value-for-parameter
|
||||
|
||||
+23
-27
@@ -16,49 +16,49 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import asyncio.subprocess
|
||||
from importlib import resources
|
||||
import enum
|
||||
import json
|
||||
import os
|
||||
import logging
|
||||
import pathlib
|
||||
import subprocess
|
||||
from typing import Optional
|
||||
import weakref
|
||||
from importlib import resources
|
||||
from typing import Optional
|
||||
|
||||
import click
|
||||
import aiohttp
|
||||
import click
|
||||
from aiohttp import web
|
||||
|
||||
import bumble
|
||||
from bumble.colors import color
|
||||
from bumble.core import PhysicalTransport, CommandTimeoutError
|
||||
from bumble.device import Connection, Device, DeviceConfiguration
|
||||
from bumble.hci import HCI_StatusError
|
||||
from bumble.pairing import PairingConfig
|
||||
from bumble.sdp import ServiceAttribute
|
||||
from bumble.transport import open_transport
|
||||
import bumble.logging
|
||||
from bumble.a2dp import (
|
||||
A2DP_MPEG_2_4_AAC_CODEC_TYPE,
|
||||
A2DP_NON_A2DP_CODEC_TYPE,
|
||||
A2DP_SBC_CODEC_TYPE,
|
||||
AacMediaCodecInformation,
|
||||
OpusMediaCodecInformation,
|
||||
SbcMediaCodecInformation,
|
||||
make_audio_sink_service_sdp_records,
|
||||
)
|
||||
from bumble.avdtp import (
|
||||
AVDTP_AUDIO_MEDIA_TYPE,
|
||||
Listener,
|
||||
MediaCodecCapabilities,
|
||||
Protocol,
|
||||
)
|
||||
from bumble.a2dp import (
|
||||
make_audio_sink_service_sdp_records,
|
||||
A2DP_SBC_CODEC_TYPE,
|
||||
A2DP_MPEG_2_4_AAC_CODEC_TYPE,
|
||||
A2DP_NON_A2DP_CODEC_TYPE,
|
||||
SbcMediaCodecInformation,
|
||||
AacMediaCodecInformation,
|
||||
OpusMediaCodecInformation,
|
||||
)
|
||||
from bumble.utils import AsyncRunner
|
||||
from bumble.codecs import AacAudioRtpPacket
|
||||
from bumble.colors import color
|
||||
from bumble.core import CommandTimeoutError, PhysicalTransport
|
||||
from bumble.device import Connection, Device, DeviceConfiguration
|
||||
from bumble.hci import HCI_StatusError
|
||||
from bumble.pairing import PairingConfig
|
||||
from bumble.rtp import MediaPacket
|
||||
|
||||
from bumble.sdp import ServiceAttribute
|
||||
from bumble.transport import open_transport
|
||||
from bumble.utils import AsyncRunner
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -833,11 +833,7 @@ def speaker(
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def main():
|
||||
logging.basicConfig(
|
||||
level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper(),
|
||||
format="[%(asctime)s.%(msecs)03d] %(levelname)s:%(name)s:%(message)s",
|
||||
datefmt="%H:%M:%S",
|
||||
)
|
||||
bumble.logging.setup_basic_logging('WARNING')
|
||||
speaker()
|
||||
|
||||
|
||||
|
||||
+3
-3
@@ -16,10 +16,10 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import os
|
||||
import logging
|
||||
|
||||
import click
|
||||
|
||||
import bumble.logging
|
||||
from bumble.device import Device
|
||||
from bumble.keys import JsonKeyStore
|
||||
from bumble.transport import open_transport
|
||||
@@ -68,7 +68,7 @@ def main(keystore_file, hci_transport, device_config, address):
|
||||
instantiated.
|
||||
If no address is passed, the existing pairing keys for all addresses are printed.
|
||||
"""
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||
bumble.logging.setup_basic_logging()
|
||||
|
||||
if not keystore_file and not hci_transport:
|
||||
print('either --keystore-file or --hci-transport must be specified.')
|
||||
|
||||
+2
-4
@@ -26,15 +26,13 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import os
|
||||
import logging
|
||||
import click
|
||||
import usb1
|
||||
|
||||
import bumble.logging
|
||||
from bumble.colors import color
|
||||
from bumble.transport.usb import load_libusb
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -169,7 +167,7 @@ def is_bluetooth_hci(device):
|
||||
@click.command()
|
||||
@click.option('--verbose', is_flag=True, default=False, help='Print more details')
|
||||
def main(verbose):
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
|
||||
bumble.logging.setup_basic_logging('WARNING')
|
||||
|
||||
load_libusb()
|
||||
with usb1.USBContext() as context:
|
||||
|
||||
+16
-17
@@ -17,37 +17,36 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import AsyncGenerator
|
||||
import dataclasses
|
||||
import enum
|
||||
import logging
|
||||
import struct
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Awaitable, Callable
|
||||
from typing_extensions import ClassVar, Self
|
||||
|
||||
from typing_extensions import ClassVar, Self
|
||||
|
||||
from bumble.codecs import AacAudioRtpPacket
|
||||
from bumble.company_ids import COMPANY_IDENTIFIERS
|
||||
from bumble.sdp import (
|
||||
DataElement,
|
||||
ServiceAttribute,
|
||||
SDP_PUBLIC_BROWSE_ROOT,
|
||||
SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
|
||||
SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
|
||||
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
|
||||
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
)
|
||||
from bumble.core import (
|
||||
BT_L2CAP_PROTOCOL_ID,
|
||||
BT_AUDIO_SOURCE_SERVICE,
|
||||
BT_AUDIO_SINK_SERVICE,
|
||||
BT_AVDTP_PROTOCOL_ID,
|
||||
BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE,
|
||||
BT_AUDIO_SINK_SERVICE,
|
||||
BT_AUDIO_SOURCE_SERVICE,
|
||||
BT_AVDTP_PROTOCOL_ID,
|
||||
BT_L2CAP_PROTOCOL_ID,
|
||||
name_or_number,
|
||||
)
|
||||
from bumble.rtp import MediaPacket
|
||||
|
||||
from bumble.sdp import (
|
||||
SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
|
||||
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
SDP_PUBLIC_BROWSE_ROOT,
|
||||
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
|
||||
SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
|
||||
DataElement,
|
||||
ServiceAttribute,
|
||||
)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
|
||||
+244
-249
@@ -24,24 +24,26 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import enum
|
||||
import functools
|
||||
import inspect
|
||||
import struct
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Awaitable,
|
||||
Callable,
|
||||
ClassVar,
|
||||
Generic,
|
||||
Optional,
|
||||
TypeVar,
|
||||
Union,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
|
||||
from bumble import utils
|
||||
from bumble.core import UUID, name_or_number, InvalidOperationError, ProtocolError
|
||||
from bumble.hci import HCI_Object, key_with_value
|
||||
from bumble import hci, utils
|
||||
from bumble.colors import color
|
||||
from bumble.core import UUID, InvalidOperationError, ProtocolError
|
||||
from bumble.hci import HCI_Object
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Typing
|
||||
@@ -60,96 +62,66 @@ _T = TypeVar('_T')
|
||||
ATT_CID = 0x04
|
||||
ATT_PSM = 0x001F
|
||||
|
||||
ATT_ERROR_RESPONSE = 0x01
|
||||
ATT_EXCHANGE_MTU_REQUEST = 0x02
|
||||
ATT_EXCHANGE_MTU_RESPONSE = 0x03
|
||||
ATT_FIND_INFORMATION_REQUEST = 0x04
|
||||
ATT_FIND_INFORMATION_RESPONSE = 0x05
|
||||
ATT_FIND_BY_TYPE_VALUE_REQUEST = 0x06
|
||||
ATT_FIND_BY_TYPE_VALUE_RESPONSE = 0x07
|
||||
ATT_READ_BY_TYPE_REQUEST = 0x08
|
||||
ATT_READ_BY_TYPE_RESPONSE = 0x09
|
||||
ATT_READ_REQUEST = 0x0A
|
||||
ATT_READ_RESPONSE = 0x0B
|
||||
ATT_READ_BLOB_REQUEST = 0x0C
|
||||
ATT_READ_BLOB_RESPONSE = 0x0D
|
||||
ATT_READ_MULTIPLE_REQUEST = 0x0E
|
||||
ATT_READ_MULTIPLE_RESPONSE = 0x0F
|
||||
ATT_READ_BY_GROUP_TYPE_REQUEST = 0x10
|
||||
ATT_READ_BY_GROUP_TYPE_RESPONSE = 0x11
|
||||
ATT_WRITE_REQUEST = 0x12
|
||||
ATT_WRITE_RESPONSE = 0x13
|
||||
ATT_WRITE_COMMAND = 0x52
|
||||
ATT_SIGNED_WRITE_COMMAND = 0xD2
|
||||
ATT_PREPARE_WRITE_REQUEST = 0x16
|
||||
ATT_PREPARE_WRITE_RESPONSE = 0x17
|
||||
ATT_EXECUTE_WRITE_REQUEST = 0x18
|
||||
ATT_EXECUTE_WRITE_RESPONSE = 0x19
|
||||
ATT_HANDLE_VALUE_NOTIFICATION = 0x1B
|
||||
ATT_HANDLE_VALUE_INDICATION = 0x1D
|
||||
ATT_HANDLE_VALUE_CONFIRMATION = 0x1E
|
||||
|
||||
ATT_PDU_NAMES = {
|
||||
ATT_ERROR_RESPONSE: 'ATT_ERROR_RESPONSE',
|
||||
ATT_EXCHANGE_MTU_REQUEST: 'ATT_EXCHANGE_MTU_REQUEST',
|
||||
ATT_EXCHANGE_MTU_RESPONSE: 'ATT_EXCHANGE_MTU_RESPONSE',
|
||||
ATT_FIND_INFORMATION_REQUEST: 'ATT_FIND_INFORMATION_REQUEST',
|
||||
ATT_FIND_INFORMATION_RESPONSE: 'ATT_FIND_INFORMATION_RESPONSE',
|
||||
ATT_FIND_BY_TYPE_VALUE_REQUEST: 'ATT_FIND_BY_TYPE_VALUE_REQUEST',
|
||||
ATT_FIND_BY_TYPE_VALUE_RESPONSE: 'ATT_FIND_BY_TYPE_VALUE_RESPONSE',
|
||||
ATT_READ_BY_TYPE_REQUEST: 'ATT_READ_BY_TYPE_REQUEST',
|
||||
ATT_READ_BY_TYPE_RESPONSE: 'ATT_READ_BY_TYPE_RESPONSE',
|
||||
ATT_READ_REQUEST: 'ATT_READ_REQUEST',
|
||||
ATT_READ_RESPONSE: 'ATT_READ_RESPONSE',
|
||||
ATT_READ_BLOB_REQUEST: 'ATT_READ_BLOB_REQUEST',
|
||||
ATT_READ_BLOB_RESPONSE: 'ATT_READ_BLOB_RESPONSE',
|
||||
ATT_READ_MULTIPLE_REQUEST: 'ATT_READ_MULTIPLE_REQUEST',
|
||||
ATT_READ_MULTIPLE_RESPONSE: 'ATT_READ_MULTIPLE_RESPONSE',
|
||||
ATT_READ_BY_GROUP_TYPE_REQUEST: 'ATT_READ_BY_GROUP_TYPE_REQUEST',
|
||||
ATT_READ_BY_GROUP_TYPE_RESPONSE: 'ATT_READ_BY_GROUP_TYPE_RESPONSE',
|
||||
ATT_WRITE_REQUEST: 'ATT_WRITE_REQUEST',
|
||||
ATT_WRITE_RESPONSE: 'ATT_WRITE_RESPONSE',
|
||||
ATT_WRITE_COMMAND: 'ATT_WRITE_COMMAND',
|
||||
ATT_SIGNED_WRITE_COMMAND: 'ATT_SIGNED_WRITE_COMMAND',
|
||||
ATT_PREPARE_WRITE_REQUEST: 'ATT_PREPARE_WRITE_REQUEST',
|
||||
ATT_PREPARE_WRITE_RESPONSE: 'ATT_PREPARE_WRITE_RESPONSE',
|
||||
ATT_EXECUTE_WRITE_REQUEST: 'ATT_EXECUTE_WRITE_REQUEST',
|
||||
ATT_EXECUTE_WRITE_RESPONSE: 'ATT_EXECUTE_WRITE_RESPONSE',
|
||||
ATT_HANDLE_VALUE_NOTIFICATION: 'ATT_HANDLE_VALUE_NOTIFICATION',
|
||||
ATT_HANDLE_VALUE_INDICATION: 'ATT_HANDLE_VALUE_INDICATION',
|
||||
ATT_HANDLE_VALUE_CONFIRMATION: 'ATT_HANDLE_VALUE_CONFIRMATION'
|
||||
}
|
||||
class Opcode(hci.SpecableEnum):
|
||||
ATT_ERROR_RESPONSE = 0x01
|
||||
ATT_EXCHANGE_MTU_REQUEST = 0x02
|
||||
ATT_EXCHANGE_MTU_RESPONSE = 0x03
|
||||
ATT_FIND_INFORMATION_REQUEST = 0x04
|
||||
ATT_FIND_INFORMATION_RESPONSE = 0x05
|
||||
ATT_FIND_BY_TYPE_VALUE_REQUEST = 0x06
|
||||
ATT_FIND_BY_TYPE_VALUE_RESPONSE = 0x07
|
||||
ATT_READ_BY_TYPE_REQUEST = 0x08
|
||||
ATT_READ_BY_TYPE_RESPONSE = 0x09
|
||||
ATT_READ_REQUEST = 0x0A
|
||||
ATT_READ_RESPONSE = 0x0B
|
||||
ATT_READ_BLOB_REQUEST = 0x0C
|
||||
ATT_READ_BLOB_RESPONSE = 0x0D
|
||||
ATT_READ_MULTIPLE_REQUEST = 0x0E
|
||||
ATT_READ_MULTIPLE_RESPONSE = 0x0F
|
||||
ATT_READ_BY_GROUP_TYPE_REQUEST = 0x10
|
||||
ATT_READ_BY_GROUP_TYPE_RESPONSE = 0x11
|
||||
ATT_WRITE_REQUEST = 0x12
|
||||
ATT_WRITE_RESPONSE = 0x13
|
||||
ATT_WRITE_COMMAND = 0x52
|
||||
ATT_SIGNED_WRITE_COMMAND = 0xD2
|
||||
ATT_PREPARE_WRITE_REQUEST = 0x16
|
||||
ATT_PREPARE_WRITE_RESPONSE = 0x17
|
||||
ATT_EXECUTE_WRITE_REQUEST = 0x18
|
||||
ATT_EXECUTE_WRITE_RESPONSE = 0x19
|
||||
ATT_HANDLE_VALUE_NOTIFICATION = 0x1B
|
||||
ATT_HANDLE_VALUE_INDICATION = 0x1D
|
||||
ATT_HANDLE_VALUE_CONFIRMATION = 0x1E
|
||||
|
||||
ATT_REQUESTS = [
|
||||
ATT_EXCHANGE_MTU_REQUEST,
|
||||
ATT_FIND_INFORMATION_REQUEST,
|
||||
ATT_FIND_BY_TYPE_VALUE_REQUEST,
|
||||
ATT_READ_BY_TYPE_REQUEST,
|
||||
ATT_READ_REQUEST,
|
||||
ATT_READ_BLOB_REQUEST,
|
||||
ATT_READ_MULTIPLE_REQUEST,
|
||||
ATT_READ_BY_GROUP_TYPE_REQUEST,
|
||||
ATT_WRITE_REQUEST,
|
||||
ATT_PREPARE_WRITE_REQUEST,
|
||||
ATT_EXECUTE_WRITE_REQUEST
|
||||
Opcode.ATT_EXCHANGE_MTU_REQUEST,
|
||||
Opcode.ATT_FIND_INFORMATION_REQUEST,
|
||||
Opcode.ATT_FIND_BY_TYPE_VALUE_REQUEST,
|
||||
Opcode.ATT_READ_BY_TYPE_REQUEST,
|
||||
Opcode.ATT_READ_REQUEST,
|
||||
Opcode.ATT_READ_BLOB_REQUEST,
|
||||
Opcode.ATT_READ_MULTIPLE_REQUEST,
|
||||
Opcode.ATT_READ_BY_GROUP_TYPE_REQUEST,
|
||||
Opcode.ATT_WRITE_REQUEST,
|
||||
Opcode.ATT_PREPARE_WRITE_REQUEST,
|
||||
Opcode.ATT_EXECUTE_WRITE_REQUEST
|
||||
]
|
||||
|
||||
ATT_RESPONSES = [
|
||||
ATT_ERROR_RESPONSE,
|
||||
ATT_EXCHANGE_MTU_RESPONSE,
|
||||
ATT_FIND_INFORMATION_RESPONSE,
|
||||
ATT_FIND_BY_TYPE_VALUE_RESPONSE,
|
||||
ATT_READ_BY_TYPE_RESPONSE,
|
||||
ATT_READ_RESPONSE,
|
||||
ATT_READ_BLOB_RESPONSE,
|
||||
ATT_READ_MULTIPLE_RESPONSE,
|
||||
ATT_READ_BY_GROUP_TYPE_RESPONSE,
|
||||
ATT_WRITE_RESPONSE,
|
||||
ATT_PREPARE_WRITE_RESPONSE,
|
||||
ATT_EXECUTE_WRITE_RESPONSE
|
||||
Opcode.ATT_ERROR_RESPONSE,
|
||||
Opcode.ATT_EXCHANGE_MTU_RESPONSE,
|
||||
Opcode.ATT_FIND_INFORMATION_RESPONSE,
|
||||
Opcode.ATT_FIND_BY_TYPE_VALUE_RESPONSE,
|
||||
Opcode.ATT_READ_BY_TYPE_RESPONSE,
|
||||
Opcode.ATT_READ_RESPONSE,
|
||||
Opcode.ATT_READ_BLOB_RESPONSE,
|
||||
Opcode.ATT_READ_MULTIPLE_RESPONSE,
|
||||
Opcode.ATT_READ_BY_GROUP_TYPE_RESPONSE,
|
||||
Opcode.ATT_WRITE_RESPONSE,
|
||||
Opcode.ATT_PREPARE_WRITE_RESPONSE,
|
||||
Opcode.ATT_EXECUTE_WRITE_RESPONSE
|
||||
]
|
||||
|
||||
class ErrorCode(utils.OpenIntEnum):
|
||||
class ErrorCode(hci.SpecableEnum):
|
||||
'''
|
||||
See
|
||||
|
||||
@@ -204,10 +176,6 @@ ATT_INSUFFICIENT_RESOURCES_ERROR = ErrorCode.INSUFFICIENT_RESOURCES
|
||||
ATT_DEFAULT_MTU = 23
|
||||
|
||||
HANDLE_FIELD_SPEC = {'size': 2, 'mapper': lambda x: f'0x{x:04X}'}
|
||||
# pylint: disable-next=unnecessary-lambda-assignment,unnecessary-lambda
|
||||
UUID_2_16_FIELD_SPEC = lambda x, y: UUID.parse_uuid(x, y)
|
||||
# pylint: disable-next=unnecessary-lambda-assignment,unnecessary-lambda
|
||||
UUID_2_FIELD_SPEC = lambda x, y: UUID.parse_uuid_2(x, y) # noqa: E731
|
||||
|
||||
# fmt: on
|
||||
# pylint: enable=line-too-long
|
||||
@@ -227,7 +195,7 @@ class ATT_Error(ProtocolError):
|
||||
super().__init__(
|
||||
error_code,
|
||||
error_namespace='att',
|
||||
error_name=ATT_PDU.error_name(error_code),
|
||||
error_name=ErrorCode(error_code).name,
|
||||
)
|
||||
self.att_handle = att_handle
|
||||
self.message = message
|
||||
@@ -242,61 +210,45 @@ class ATT_Error(ProtocolError):
|
||||
# -----------------------------------------------------------------------------
|
||||
# Attribute Protocol
|
||||
# -----------------------------------------------------------------------------
|
||||
@dataclasses.dataclass
|
||||
class ATT_PDU:
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.3 ATTRIBUTE PDU
|
||||
'''
|
||||
|
||||
pdu_classes: dict[int, type[ATT_PDU]] = {}
|
||||
op_code = 0
|
||||
name: str
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(pdu):
|
||||
op_code = pdu[0]
|
||||
|
||||
cls = ATT_PDU.pdu_classes.get(op_code)
|
||||
if cls is None:
|
||||
instance = ATT_PDU(pdu)
|
||||
instance.name = ATT_PDU.pdu_name(op_code)
|
||||
instance.op_code = op_code
|
||||
return instance
|
||||
self = cls.__new__(cls)
|
||||
ATT_PDU.__init__(self, pdu)
|
||||
if hasattr(self, 'fields'):
|
||||
self.init_from_bytes(pdu, 1)
|
||||
return self
|
||||
|
||||
@staticmethod
|
||||
def pdu_name(op_code):
|
||||
return name_or_number(ATT_PDU_NAMES, op_code, 2)
|
||||
pdu_classes: ClassVar[dict[int, type[ATT_PDU]]] = {}
|
||||
fields: ClassVar[hci.Fields] = ()
|
||||
op_code: int = dataclasses.field(init=False)
|
||||
name: str = dataclasses.field(init=False)
|
||||
_payload: Optional[bytes] = dataclasses.field(default=None, init=False)
|
||||
|
||||
@classmethod
|
||||
def error_name(cls, error_code: int) -> str:
|
||||
return ErrorCode(error_code).name
|
||||
def from_bytes(cls, pdu: bytes) -> ATT_PDU:
|
||||
op_code = pdu[0]
|
||||
|
||||
@staticmethod
|
||||
def subclass(fields):
|
||||
def inner(cls):
|
||||
cls.name = cls.__name__.upper()
|
||||
cls.op_code = key_with_value(ATT_PDU_NAMES, cls.name)
|
||||
if cls.op_code is None:
|
||||
raise KeyError(f'PDU name {cls.name} not found in ATT_PDU_NAMES')
|
||||
cls.fields = fields
|
||||
subclass = ATT_PDU.pdu_classes.get(op_code)
|
||||
if subclass is None:
|
||||
instance = ATT_PDU()
|
||||
instance.op_code = op_code
|
||||
instance.payload = pdu[1:]
|
||||
instance.name = Opcode(op_code).name
|
||||
return instance
|
||||
instance = subclass(**HCI_Object.dict_from_bytes(pdu, 1, subclass.fields))
|
||||
instance.payload = pdu[1:]
|
||||
return instance
|
||||
|
||||
# Register a factory for this class
|
||||
ATT_PDU.pdu_classes[cls.op_code] = cls
|
||||
_PDU = TypeVar("_PDU", bound="ATT_PDU")
|
||||
|
||||
return cls
|
||||
@classmethod
|
||||
def subclass(cls, subclass: type[_PDU]) -> type[_PDU]:
|
||||
subclass.name = subclass.__name__.upper()
|
||||
subclass.op_code = Opcode[subclass.name]
|
||||
subclass.fields = HCI_Object.fields_from_dataclass(subclass)
|
||||
|
||||
return inner
|
||||
# Register a factory for this class
|
||||
ATT_PDU.pdu_classes[subclass.op_code] = subclass
|
||||
|
||||
def __init__(self, pdu=None, **kwargs):
|
||||
if hasattr(self, 'fields') and kwargs:
|
||||
HCI_Object.init_from_fields(self, self.fields, kwargs)
|
||||
if pdu is None:
|
||||
pdu = bytes([self.op_code]) + HCI_Object.dict_to_bytes(kwargs, self.fields)
|
||||
self.pdu = pdu
|
||||
return subclass
|
||||
|
||||
def init_from_bytes(self, pdu, offset):
|
||||
return HCI_Object.init_from_bytes(self, pdu, offset, self.fields)
|
||||
@@ -309,67 +261,91 @@ class ATT_PDU:
|
||||
def has_authentication_signature(self):
|
||||
return ((self.op_code >> 7) & 1) == 1
|
||||
|
||||
def __bytes__(self):
|
||||
return self.pdu
|
||||
@property
|
||||
def payload(self) -> bytes:
|
||||
if self._payload is None:
|
||||
self._payload = HCI_Object.dict_to_bytes(self.__dict__, self.fields)
|
||||
return self._payload
|
||||
|
||||
@payload.setter
|
||||
def payload(self, value: bytes):
|
||||
self._payload = value
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
return bytes([self.op_code]) + self.payload
|
||||
|
||||
def __str__(self):
|
||||
result = color(self.name, 'yellow')
|
||||
if fields := getattr(self, 'fields', None):
|
||||
result += ':\n' + HCI_Object.format_fields(self.__dict__, fields, ' ')
|
||||
else:
|
||||
if len(self.pdu) > 1:
|
||||
result += f': {self.pdu.hex()}'
|
||||
if self.payload:
|
||||
result += f': {self.payload.hex()}'
|
||||
return result
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass(
|
||||
[
|
||||
('request_opcode_in_error', {'size': 1, 'mapper': ATT_PDU.pdu_name}),
|
||||
('attribute_handle_in_error', HANDLE_FIELD_SPEC),
|
||||
('error_code', {'size': 1, 'mapper': ATT_PDU.error_name}),
|
||||
]
|
||||
)
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Error_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.1.1 Error Response
|
||||
'''
|
||||
|
||||
request_opcode_in_error: int = dataclasses.field(metadata=Opcode.type_metadata(1))
|
||||
attribute_handle_in_error: int = dataclasses.field(
|
||||
metadata=hci.metadata(HANDLE_FIELD_SPEC)
|
||||
)
|
||||
error_code: int = dataclasses.field(metadata=ErrorCode.type_metadata(1))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([('client_rx_mtu', 2)])
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Exchange_MTU_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.2.1 Exchange MTU Request
|
||||
'''
|
||||
|
||||
client_rx_mtu: int = dataclasses.field(metadata=hci.metadata(2))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([('server_rx_mtu', 2)])
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Exchange_MTU_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.2.2 Exchange MTU Response
|
||||
'''
|
||||
|
||||
server_rx_mtu: int = dataclasses.field(metadata=hci.metadata(2))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass(
|
||||
[('starting_handle', HANDLE_FIELD_SPEC), ('ending_handle', HANDLE_FIELD_SPEC)]
|
||||
)
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Find_Information_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.3.1 Find Information Request
|
||||
'''
|
||||
|
||||
starting_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
ending_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([('format', 1), ('information_data', '*')])
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Find_Information_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.3.2 Find Information Response
|
||||
'''
|
||||
|
||||
def parse_information_data(self):
|
||||
format: int = dataclasses.field(metadata=hci.metadata(1))
|
||||
information_data: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
information: list[tuple[int, bytes]] = dataclasses.field(init=False)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self.information = []
|
||||
offset = 0
|
||||
uuid_size = 2 if self.format == 1 else 16
|
||||
@@ -379,14 +355,6 @@ class ATT_Find_Information_Response(ATT_PDU):
|
||||
self.information.append((handle, uuid))
|
||||
offset += 2 + uuid_size
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.parse_information_data()
|
||||
|
||||
def init_from_bytes(self, pdu, offset):
|
||||
super().init_from_bytes(pdu, offset)
|
||||
self.parse_information_data()
|
||||
|
||||
def __str__(self):
|
||||
result = color(self.name, 'yellow')
|
||||
result += ':\n' + HCI_Object.format_fields(
|
||||
@@ -408,28 +376,31 @@ class ATT_Find_Information_Response(ATT_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass(
|
||||
[
|
||||
('starting_handle', HANDLE_FIELD_SPEC),
|
||||
('ending_handle', HANDLE_FIELD_SPEC),
|
||||
('attribute_type', UUID_2_FIELD_SPEC),
|
||||
('attribute_value', '*'),
|
||||
]
|
||||
)
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Find_By_Type_Value_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.3.3 Find By Type Value Request
|
||||
'''
|
||||
|
||||
starting_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
ending_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
attribute_type: UUID = dataclasses.field(metadata=hci.metadata(UUID.parse_uuid_2))
|
||||
attribute_value: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([('handles_information_list', '*')])
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Find_By_Type_Value_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.3.4 Find By Type Value Response
|
||||
'''
|
||||
|
||||
def parse_handles_information_list(self):
|
||||
handles_information_list: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
handles_information: list[tuple[int, int]] = dataclasses.field(init=False)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self.handles_information = []
|
||||
offset = 0
|
||||
while offset + 4 <= len(self.handles_information_list):
|
||||
@@ -439,14 +410,6 @@ class ATT_Find_By_Type_Value_Response(ATT_PDU):
|
||||
self.handles_information.append((found_attribute_handle, group_end_handle))
|
||||
offset += 4
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.parse_handles_information_list()
|
||||
|
||||
def init_from_bytes(self, pdu, offset):
|
||||
super().init_from_bytes(pdu, offset)
|
||||
self.parse_handles_information_list()
|
||||
|
||||
def __str__(self):
|
||||
result = color(self.name, 'yellow')
|
||||
result += ':\n' + HCI_Object.format_fields(
|
||||
@@ -470,27 +433,31 @@ class ATT_Find_By_Type_Value_Response(ATT_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass(
|
||||
[
|
||||
('starting_handle', HANDLE_FIELD_SPEC),
|
||||
('ending_handle', HANDLE_FIELD_SPEC),
|
||||
('attribute_type', UUID_2_16_FIELD_SPEC),
|
||||
]
|
||||
)
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Read_By_Type_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.1 Read By Type Request
|
||||
'''
|
||||
|
||||
starting_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
ending_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
attribute_type: UUID = dataclasses.field(metadata=hci.metadata(UUID.parse_uuid))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([('length', 1), ('attribute_data_list', '*')])
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Read_By_Type_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.2 Read By Type Response
|
||||
'''
|
||||
|
||||
def parse_attribute_data_list(self):
|
||||
length: int = dataclasses.field(metadata=hci.metadata(1))
|
||||
attribute_data_list: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
attributes: list[tuple[int, bytes]] = dataclasses.field(init=False)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self.attributes = []
|
||||
offset = 0
|
||||
while self.length != 0 and offset + self.length <= len(
|
||||
@@ -505,14 +472,6 @@ class ATT_Read_By_Type_Response(ATT_PDU):
|
||||
self.attributes.append((attribute_handle, attribute_value))
|
||||
offset += self.length
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.parse_attribute_data_list()
|
||||
|
||||
def init_from_bytes(self, pdu, offset):
|
||||
super().init_from_bytes(pdu, offset)
|
||||
self.parse_attribute_data_list()
|
||||
|
||||
def __str__(self):
|
||||
result = color(self.name, 'yellow')
|
||||
result += ':\n' + HCI_Object.format_fields(
|
||||
@@ -534,75 +493,100 @@ class ATT_Read_By_Type_Response(ATT_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([('attribute_handle', HANDLE_FIELD_SPEC)])
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Read_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.3 Read Request
|
||||
'''
|
||||
|
||||
attribute_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([('attribute_value', '*')])
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Read_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.4 Read Response
|
||||
'''
|
||||
|
||||
attribute_value: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([('attribute_handle', HANDLE_FIELD_SPEC), ('value_offset', 2)])
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Read_Blob_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.5 Read Blob Request
|
||||
'''
|
||||
|
||||
attribute_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
value_offset: int = dataclasses.field(metadata=hci.metadata(2))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([('part_attribute_value', '*')])
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Read_Blob_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.6 Read Blob Response
|
||||
'''
|
||||
|
||||
part_attribute_value: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([('set_of_handles', '*')])
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Read_Multiple_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.7 Read Multiple Request
|
||||
'''
|
||||
|
||||
set_of_handles: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([('set_of_values', '*')])
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Read_Multiple_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.8 Read Multiple Response
|
||||
'''
|
||||
|
||||
set_of_values: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass(
|
||||
[
|
||||
('starting_handle', HANDLE_FIELD_SPEC),
|
||||
('ending_handle', HANDLE_FIELD_SPEC),
|
||||
('attribute_group_type', UUID_2_16_FIELD_SPEC),
|
||||
]
|
||||
)
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Read_By_Group_Type_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.9 Read by Group Type Request
|
||||
'''
|
||||
|
||||
starting_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
ending_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
attribute_group_type: UUID = dataclasses.field(
|
||||
metadata=hci.metadata(UUID.parse_uuid)
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([('length', 1), ('attribute_data_list', '*')])
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Read_By_Group_Type_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.10 Read by Group Type Response
|
||||
'''
|
||||
|
||||
def parse_attribute_data_list(self):
|
||||
length: int = dataclasses.field(metadata=hci.metadata(1))
|
||||
attribute_data_list: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
attributes: list[tuple[int, int, bytes]] = dataclasses.field(init=False)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self.attributes = []
|
||||
offset = 0
|
||||
while self.length != 0 and offset + self.length <= len(
|
||||
@@ -619,14 +603,6 @@ class ATT_Read_By_Group_Type_Response(ATT_PDU):
|
||||
)
|
||||
offset += self.length
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.parse_attribute_data_list()
|
||||
|
||||
def init_from_bytes(self, pdu, offset):
|
||||
super().init_from_bytes(pdu, offset)
|
||||
self.parse_attribute_data_list()
|
||||
|
||||
def __str__(self):
|
||||
result = color(self.name, 'yellow')
|
||||
result += ':\n' + HCI_Object.format_fields(
|
||||
@@ -651,15 +627,20 @@ class ATT_Read_By_Group_Type_Response(ATT_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([('attribute_handle', HANDLE_FIELD_SPEC), ('attribute_value', '*')])
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Write_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.5.1 Write Request
|
||||
'''
|
||||
|
||||
attribute_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
attribute_value: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([])
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Write_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.5.2 Write Response
|
||||
@@ -667,65 +648,70 @@ class ATT_Write_Response(ATT_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([('attribute_handle', HANDLE_FIELD_SPEC), ('attribute_value', '*')])
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Write_Command(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.5.3 Write Command
|
||||
'''
|
||||
|
||||
attribute_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
attribute_value: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass(
|
||||
[
|
||||
('attribute_handle', HANDLE_FIELD_SPEC),
|
||||
('attribute_value', '*'),
|
||||
# ('authentication_signature', 'TODO')
|
||||
]
|
||||
)
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Signed_Write_Command(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.5.4 Signed Write Command
|
||||
'''
|
||||
|
||||
attribute_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
attribute_value: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
# TODO: authentication_signature
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass(
|
||||
[
|
||||
('attribute_handle', HANDLE_FIELD_SPEC),
|
||||
('value_offset', 2),
|
||||
('part_attribute_value', '*'),
|
||||
]
|
||||
)
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Prepare_Write_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.6.1 Prepare Write Request
|
||||
'''
|
||||
|
||||
attribute_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
value_offset: int = dataclasses.field(metadata=hci.metadata(2))
|
||||
part_attribute_value: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass(
|
||||
[
|
||||
('attribute_handle', HANDLE_FIELD_SPEC),
|
||||
('value_offset', 2),
|
||||
('part_attribute_value', '*'),
|
||||
]
|
||||
)
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Prepare_Write_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.6.2 Prepare Write Response
|
||||
'''
|
||||
|
||||
attribute_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
value_offset: int = dataclasses.field(metadata=hci.metadata(2))
|
||||
part_attribute_value: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([("flags", 1)])
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Execute_Write_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.6.3 Execute Write Request
|
||||
'''
|
||||
|
||||
flags: int = dataclasses.field(metadata=hci.metadata(1))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([])
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Execute_Write_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.6.4 Execute Write Response
|
||||
@@ -733,23 +719,32 @@ class ATT_Execute_Write_Response(ATT_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([('attribute_handle', HANDLE_FIELD_SPEC), ('attribute_value', '*')])
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Handle_Value_Notification(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.7.1 Handle Value Notification
|
||||
'''
|
||||
|
||||
attribute_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
attribute_value: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([('attribute_handle', HANDLE_FIELD_SPEC), ('attribute_value', '*')])
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Handle_Value_Indication(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.7.2 Handle Value Indication
|
||||
'''
|
||||
|
||||
attribute_handle: int = dataclasses.field(metadata=hci.metadata(HANDLE_FIELD_SPEC))
|
||||
attribute_value: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([])
|
||||
@ATT_PDU.subclass
|
||||
@dataclasses.dataclass
|
||||
class ATT_Handle_Value_Confirmation(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.7.3 Handle Value Confirmation
|
||||
|
||||
+5
-9
@@ -17,20 +17,16 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import abc
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import enum
|
||||
import logging
|
||||
import pathlib
|
||||
from typing import (
|
||||
AsyncGenerator,
|
||||
BinaryIO,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
import sys
|
||||
import wave
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from typing import TYPE_CHECKING, AsyncGenerator, BinaryIO
|
||||
|
||||
from bumble.colors import color
|
||||
|
||||
@@ -230,8 +226,8 @@ class SoundDeviceAudioOutput(ThreadedAudioOutput):
|
||||
|
||||
try:
|
||||
self._stream.write(pcm_samples)
|
||||
except Exception as error:
|
||||
print(f'Sound device error: {error}')
|
||||
except Exception:
|
||||
logger.exception('Sound device error')
|
||||
raise
|
||||
|
||||
def _close(self):
|
||||
|
||||
+2
-2
@@ -16,12 +16,12 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
import struct
|
||||
from typing import Union
|
||||
|
||||
from bumble import core
|
||||
from bumble import utils
|
||||
from bumble import core, utils
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
+6
-7
@@ -16,15 +16,14 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
from enum import IntEnum
|
||||
|
||||
import logging
|
||||
import struct
|
||||
from typing import Callable, cast, Optional
|
||||
from enum import IntEnum
|
||||
from typing import Callable, Optional, cast
|
||||
|
||||
from bumble import avc, core, l2cap
|
||||
from bumble.colors import color
|
||||
from bumble import avc
|
||||
from bumble import core
|
||||
from bumble import l2cap
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -137,8 +136,8 @@ class MessageAssembler:
|
||||
self.pid,
|
||||
self.payload,
|
||||
)
|
||||
except Exception as error:
|
||||
logger.exception(color(f"!!! exception in callback: {error}", "red"))
|
||||
except Exception:
|
||||
logger.exception(color("!!! exception in callback", "red"))
|
||||
|
||||
self.reset()
|
||||
|
||||
|
||||
+20
-23
@@ -16,31 +16,25 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
import logging
|
||||
import enum
|
||||
import logging
|
||||
import time
|
||||
import warnings
|
||||
from typing import (
|
||||
Any,
|
||||
Awaitable,
|
||||
Optional,
|
||||
Callable,
|
||||
AsyncGenerator,
|
||||
Awaitable,
|
||||
Callable,
|
||||
Iterable,
|
||||
Union,
|
||||
Optional,
|
||||
SupportsBytes,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
|
||||
|
||||
from bumble.core import (
|
||||
BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE,
|
||||
InvalidStateError,
|
||||
ProtocolError,
|
||||
InvalidArgumentError,
|
||||
name_or_number,
|
||||
)
|
||||
from bumble import device, l2cap, sdp, utils
|
||||
from bumble.a2dp import (
|
||||
A2DP_CODEC_TYPE_NAMES,
|
||||
A2DP_MPEG_2_4_AAC_CODEC_TYPE,
|
||||
@@ -51,10 +45,15 @@ from bumble.a2dp import (
|
||||
SbcMediaCodecInformation,
|
||||
VendorSpecificMediaCodecInformation,
|
||||
)
|
||||
from bumble.rtp import MediaPacket
|
||||
from bumble import sdp, device, l2cap, utils
|
||||
from bumble.colors import color
|
||||
|
||||
from bumble.core import (
|
||||
BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE,
|
||||
InvalidArgumentError,
|
||||
InvalidStateError,
|
||||
ProtocolError,
|
||||
name_or_number,
|
||||
)
|
||||
from bumble.rtp import MediaPacket
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -434,8 +433,8 @@ class MessageAssembler:
|
||||
)
|
||||
try:
|
||||
self.callback(self.transaction_label, message)
|
||||
except Exception as error:
|
||||
logger.exception(color(f'!!! exception in callback: {error}', 'red'))
|
||||
except Exception:
|
||||
logger.exception(color('!!! exception in callback', 'red'))
|
||||
|
||||
self.reset()
|
||||
|
||||
@@ -1400,10 +1399,8 @@ class Protocol(utils.EventEmitter):
|
||||
try:
|
||||
response = handler(message)
|
||||
self.send_message(transaction_label, response)
|
||||
except Exception as error:
|
||||
logger.warning(
|
||||
f'{color("!!! Exception in handler:", "red")} {error}'
|
||||
)
|
||||
except Exception:
|
||||
logger.exception(color("!!! Exception in handler:", "red"))
|
||||
else:
|
||||
logger.warning('unhandled command')
|
||||
else:
|
||||
|
||||
+33
-45
@@ -16,16 +16,16 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
import enum
|
||||
import logging
|
||||
import struct
|
||||
from dataclasses import dataclass
|
||||
from typing import (
|
||||
AsyncIterator,
|
||||
Awaitable,
|
||||
Callable,
|
||||
cast,
|
||||
Iterable,
|
||||
List,
|
||||
Optional,
|
||||
@@ -33,37 +33,23 @@ from typing import (
|
||||
SupportsBytes,
|
||||
TypeVar,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
|
||||
|
||||
from bumble import avc, avctp, core, l2cap, utils
|
||||
from bumble.colors import color
|
||||
from bumble.device import Device, Connection
|
||||
from bumble.device import Connection, Device
|
||||
from bumble.sdp import (
|
||||
SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
|
||||
SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
|
||||
SDP_PUBLIC_BROWSE_ROOT,
|
||||
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
|
||||
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,
|
||||
SDP_SUPPORTED_FEATURES_ATTRIBUTE_ID,
|
||||
DataElement,
|
||||
ServiceAttribute,
|
||||
)
|
||||
from bumble import utils
|
||||
from bumble.core import (
|
||||
InvalidArgumentError,
|
||||
ProtocolError,
|
||||
BT_L2CAP_PROTOCOL_ID,
|
||||
BT_AVCTP_PROTOCOL_ID,
|
||||
BT_AV_REMOTE_CONTROL_SERVICE,
|
||||
BT_AV_REMOTE_CONTROL_CONTROLLER_SERVICE,
|
||||
BT_AV_REMOTE_CONTROL_TARGET_SERVICE,
|
||||
)
|
||||
from bumble import l2cap
|
||||
from bumble import avc
|
||||
from bumble import avctp
|
||||
from bumble import utils
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -102,8 +88,8 @@ def make_controller_service_sdp_records(
|
||||
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(BT_AV_REMOTE_CONTROL_SERVICE),
|
||||
DataElement.uuid(BT_AV_REMOTE_CONTROL_CONTROLLER_SERVICE),
|
||||
DataElement.uuid(core.BT_AV_REMOTE_CONTROL_SERVICE),
|
||||
DataElement.uuid(core.BT_AV_REMOTE_CONTROL_CONTROLLER_SERVICE),
|
||||
]
|
||||
),
|
||||
),
|
||||
@@ -113,13 +99,13 @@ def make_controller_service_sdp_records(
|
||||
[
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(BT_L2CAP_PROTOCOL_ID),
|
||||
DataElement.uuid(core.BT_L2CAP_PROTOCOL_ID),
|
||||
DataElement.unsigned_integer_16(avctp.AVCTP_PSM),
|
||||
]
|
||||
),
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(BT_AVCTP_PROTOCOL_ID),
|
||||
DataElement.uuid(core.BT_AVCTP_PROTOCOL_ID),
|
||||
DataElement.unsigned_integer_16(avctp_version_int),
|
||||
]
|
||||
),
|
||||
@@ -132,7 +118,7 @@ def make_controller_service_sdp_records(
|
||||
[
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(BT_AV_REMOTE_CONTROL_SERVICE),
|
||||
DataElement.uuid(core.BT_AV_REMOTE_CONTROL_SERVICE),
|
||||
DataElement.unsigned_integer_16(avrcp_version_int),
|
||||
]
|
||||
),
|
||||
@@ -170,7 +156,7 @@ def make_target_service_sdp_records(
|
||||
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(BT_AV_REMOTE_CONTROL_TARGET_SERVICE),
|
||||
DataElement.uuid(core.BT_AV_REMOTE_CONTROL_TARGET_SERVICE),
|
||||
]
|
||||
),
|
||||
),
|
||||
@@ -180,13 +166,13 @@ def make_target_service_sdp_records(
|
||||
[
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(BT_L2CAP_PROTOCOL_ID),
|
||||
DataElement.uuid(core.BT_L2CAP_PROTOCOL_ID),
|
||||
DataElement.unsigned_integer_16(avctp.AVCTP_PSM),
|
||||
]
|
||||
),
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(BT_AVCTP_PROTOCOL_ID),
|
||||
DataElement.uuid(core.BT_AVCTP_PROTOCOL_ID),
|
||||
DataElement.unsigned_integer_16(avctp_version_int),
|
||||
]
|
||||
),
|
||||
@@ -199,7 +185,7 @@ def make_target_service_sdp_records(
|
||||
[
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(BT_AV_REMOTE_CONTROL_SERVICE),
|
||||
DataElement.uuid(core.BT_AV_REMOTE_CONTROL_SERVICE),
|
||||
DataElement.unsigned_integer_16(avrcp_version_int),
|
||||
]
|
||||
),
|
||||
@@ -276,8 +262,8 @@ class PduAssembler:
|
||||
assert self.pdu_id is not None
|
||||
try:
|
||||
self.callback(self.pdu_id, self.parameter)
|
||||
except Exception as error:
|
||||
logger.exception(color(f'!!! exception in callback: {error}', 'red'))
|
||||
except Exception:
|
||||
logger.exception(color('!!! exception in callback', 'red'))
|
||||
|
||||
self.reset()
|
||||
|
||||
@@ -1126,7 +1112,7 @@ class Protocol(utils.EventEmitter):
|
||||
|
||||
@staticmethod
|
||||
def _check_vendor_dependent_frame(
|
||||
frame: Union[avc.VendorDependentCommandFrame, avc.VendorDependentResponseFrame]
|
||||
frame: Union[avc.VendorDependentCommandFrame, avc.VendorDependentResponseFrame],
|
||||
) -> bool:
|
||||
if frame.company_id != AVRCP_BLUETOOTH_SIG_COMPANY_ID:
|
||||
logger.debug("unsupported company id, ignoring")
|
||||
@@ -1161,7 +1147,9 @@ class Protocol(utils.EventEmitter):
|
||||
A 'connection' event will be emitted when a connection is made, and a 'start'
|
||||
event will be emitted when the protocol is ready to be used on that connection.
|
||||
"""
|
||||
device.register_l2cap_server(avctp.AVCTP_PSM, self._on_avctp_connection)
|
||||
device.create_l2cap_server(
|
||||
l2cap.ClassicChannelSpec(avctp.AVCTP_PSM), self._on_avctp_connection
|
||||
)
|
||||
|
||||
async def connect(self, connection: Connection) -> None:
|
||||
"""
|
||||
@@ -1208,7 +1196,7 @@ class Protocol(utils.EventEmitter):
|
||||
def _delegate_command(
|
||||
self, transaction_label: int, command: Command, method: Awaitable
|
||||
) -> None:
|
||||
async def call():
|
||||
async def call() -> None:
|
||||
try:
|
||||
await method
|
||||
except Delegate.Error as error:
|
||||
@@ -1412,7 +1400,7 @@ class Protocol(utils.EventEmitter):
|
||||
def notify_track_changed(self, identifier: bytes) -> None:
|
||||
"""Notify the connected peer of a Track change."""
|
||||
if len(identifier) != 8:
|
||||
raise InvalidArgumentError("identifier must be 8 bytes")
|
||||
raise core.InvalidArgumentError("identifier must be 8 bytes")
|
||||
self.notify_event(TrackChangedEvent(identifier))
|
||||
|
||||
def notify_playback_position_changed(self, position: int) -> None:
|
||||
@@ -1679,7 +1667,7 @@ class Protocol(utils.EventEmitter):
|
||||
else:
|
||||
logger.debug("unexpected PDU ID")
|
||||
pending_command.response.set_exception(
|
||||
ProtocolError(
|
||||
core.ProtocolError(
|
||||
error_code=None,
|
||||
error_namespace="avrcp",
|
||||
details="unexpected PDU ID",
|
||||
@@ -1688,7 +1676,7 @@ class Protocol(utils.EventEmitter):
|
||||
else:
|
||||
logger.debug("unexpected response code")
|
||||
pending_command.response.set_exception(
|
||||
ProtocolError(
|
||||
core.ProtocolError(
|
||||
error_code=None,
|
||||
error_namespace="avrcp",
|
||||
details="unexpected response code",
|
||||
@@ -1866,12 +1854,12 @@ class Protocol(utils.EventEmitter):
|
||||
) -> None:
|
||||
logger.debug(f"<<< AVRCP command PDU: {command}")
|
||||
|
||||
async def get_supported_events():
|
||||
async def get_supported_events() -> None:
|
||||
if (
|
||||
command.capability_id
|
||||
!= GetCapabilitiesCommand.CapabilityId.EVENTS_SUPPORTED
|
||||
):
|
||||
raise Protocol.InvalidParameterError
|
||||
raise core.InvalidArgumentError()
|
||||
|
||||
supported_events = await self.delegate.get_supported_events()
|
||||
self.send_avrcp_response(
|
||||
@@ -1887,7 +1875,7 @@ class Protocol(utils.EventEmitter):
|
||||
) -> None:
|
||||
logger.debug(f"<<< AVRCP command PDU: {command}")
|
||||
|
||||
async def set_absolute_volume():
|
||||
async def set_absolute_volume() -> None:
|
||||
await self.delegate.set_absolute_volume(command.volume)
|
||||
effective_volume = await self.delegate.get_absolute_volume()
|
||||
self.send_avrcp_response(
|
||||
@@ -1903,7 +1891,7 @@ class Protocol(utils.EventEmitter):
|
||||
) -> None:
|
||||
logger.debug(f"<<< AVRCP command PDU: {command}")
|
||||
|
||||
async def register_notification():
|
||||
async def register_notification() -> None:
|
||||
# Check if the event is supported.
|
||||
supported_events = await self.delegate.get_supported_events()
|
||||
if command.event_id not in supported_events:
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from bumble import core
|
||||
|
||||
+177
-49
@@ -17,17 +17,17 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import itertools
|
||||
import logging
|
||||
import random
|
||||
import struct
|
||||
from bumble.colors import color
|
||||
from bumble.core import (
|
||||
PhysicalTransport,
|
||||
)
|
||||
from typing import TYPE_CHECKING, Any, Optional, Union
|
||||
|
||||
from bumble import hci
|
||||
from bumble.colors import color
|
||||
from bumble.core import PhysicalTransport
|
||||
from bumble.hci import (
|
||||
HCI_ACL_DATA_PACKET,
|
||||
HCI_COMMAND_DISALLOWED_ERROR,
|
||||
@@ -38,13 +38,12 @@ from bumble.hci import (
|
||||
HCI_EVENT_PACKET,
|
||||
HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR,
|
||||
HCI_LE_1M_PHY,
|
||||
HCI_SUCCESS,
|
||||
HCI_UNKNOWN_HCI_COMMAND_ERROR,
|
||||
HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR,
|
||||
HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR,
|
||||
HCI_SUCCESS,
|
||||
HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR,
|
||||
HCI_UNKNOWN_HCI_COMMAND_ERROR,
|
||||
HCI_VERSION_BLUETOOTH_CORE_5_0,
|
||||
Address,
|
||||
Role,
|
||||
HCI_AclDataPacket,
|
||||
HCI_AclDataPacketAssembler,
|
||||
HCI_Command_Complete_Event,
|
||||
@@ -53,7 +52,6 @@ from bumble.hci import (
|
||||
HCI_Connection_Request_Event,
|
||||
HCI_Disconnection_Complete_Event,
|
||||
HCI_Encryption_Change_Event,
|
||||
HCI_Synchronous_Connection_Complete_Event,
|
||||
HCI_LE_Advertising_Report_Event,
|
||||
HCI_LE_CIS_Established_Event,
|
||||
HCI_LE_CIS_Request_Event,
|
||||
@@ -62,8 +60,9 @@ from bumble.hci import (
|
||||
HCI_Number_Of_Completed_Packets_Event,
|
||||
HCI_Packet,
|
||||
HCI_Role_Change_Event,
|
||||
HCI_Synchronous_Connection_Complete_Event,
|
||||
Role,
|
||||
)
|
||||
from typing import Optional, Union, Any, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bumble.link import LocalLink
|
||||
@@ -108,7 +107,9 @@ class Connection:
|
||||
def on_hci_acl_data_packet(self, packet):
|
||||
self.assembler.feed_packet(packet)
|
||||
self.controller.send_hci_packet(
|
||||
HCI_Number_Of_Completed_Packets_Event([(self.handle, 1)])
|
||||
HCI_Number_Of_Completed_Packets_Event(
|
||||
connection_handles=[self.handle], num_completed_packets=[1]
|
||||
)
|
||||
)
|
||||
|
||||
def on_acl_pdu(self, data):
|
||||
@@ -368,6 +369,12 @@ class Controller:
|
||||
return connection
|
||||
return None
|
||||
|
||||
def find_peripheral_connection_by_handle(self, handle):
|
||||
for connection in self.peripheral_connections.values():
|
||||
if connection.handle == handle:
|
||||
return connection
|
||||
return None
|
||||
|
||||
def find_classic_connection_by_handle(self, handle):
|
||||
for connection in self.classic_connections.values():
|
||||
if connection.handle == handle:
|
||||
@@ -392,7 +399,7 @@ class Controller:
|
||||
peer_address=peer_address,
|
||||
link=self.link,
|
||||
transport=PhysicalTransport.LE,
|
||||
link_type=HCI_Connection_Complete_Event.ACL_LINK_TYPE,
|
||||
link_type=HCI_Connection_Complete_Event.LinkType.ACL,
|
||||
)
|
||||
self.peripheral_connections[peer_address] = connection
|
||||
logger.debug(f'New PERIPHERAL connection handle: 0x{connection_handle:04X}')
|
||||
@@ -412,7 +419,7 @@ class Controller:
|
||||
)
|
||||
)
|
||||
|
||||
def on_link_central_disconnected(self, peer_address, reason):
|
||||
def on_link_disconnected(self, peer_address, reason):
|
||||
'''
|
||||
Called when an active disconnection occurs from a peer
|
||||
'''
|
||||
@@ -429,6 +436,17 @@ class Controller:
|
||||
|
||||
# Remove the connection
|
||||
del self.peripheral_connections[peer_address]
|
||||
elif connection := self.central_connections.get(peer_address):
|
||||
self.send_hci_packet(
|
||||
HCI_Disconnection_Complete_Event(
|
||||
status=HCI_SUCCESS,
|
||||
connection_handle=connection.handle,
|
||||
reason=reason,
|
||||
)
|
||||
)
|
||||
|
||||
# Remove the connection
|
||||
del self.central_connections[peer_address]
|
||||
else:
|
||||
logger.warning(f'!!! No peripheral connection found for {peer_address}')
|
||||
|
||||
@@ -452,7 +470,7 @@ class Controller:
|
||||
peer_address=peer_address,
|
||||
link=self.link,
|
||||
transport=PhysicalTransport.LE,
|
||||
link_type=HCI_Connection_Complete_Event.ACL_LINK_TYPE,
|
||||
link_type=HCI_Connection_Complete_Event.LinkType.ACL,
|
||||
)
|
||||
self.central_connections[peer_address] = connection
|
||||
logger.debug(
|
||||
@@ -477,7 +495,7 @@ class Controller:
|
||||
)
|
||||
)
|
||||
|
||||
def on_link_peripheral_disconnection_complete(self, disconnection_command, status):
|
||||
def on_link_disconnection_complete(self, disconnection_command, status):
|
||||
'''
|
||||
Called when a disconnection has been completed
|
||||
'''
|
||||
@@ -497,26 +515,11 @@ class Controller:
|
||||
):
|
||||
logger.debug(f'CENTRAL Connection removed: {connection}')
|
||||
del self.central_connections[connection.peer_address]
|
||||
|
||||
def on_link_peripheral_disconnected(self, peer_address):
|
||||
'''
|
||||
Called when a connection to a peripheral is broken
|
||||
'''
|
||||
|
||||
# Send a disconnection complete event
|
||||
if connection := self.central_connections.get(peer_address):
|
||||
self.send_hci_packet(
|
||||
HCI_Disconnection_Complete_Event(
|
||||
status=HCI_SUCCESS,
|
||||
connection_handle=connection.handle,
|
||||
reason=HCI_CONNECTION_TIMEOUT_ERROR,
|
||||
)
|
||||
)
|
||||
|
||||
# Remove the connection
|
||||
del self.central_connections[peer_address]
|
||||
else:
|
||||
logger.warning(f'!!! No central connection found for {peer_address}')
|
||||
elif connection := self.find_peripheral_connection_by_handle(
|
||||
disconnection_command.connection_handle
|
||||
):
|
||||
logger.debug(f'PERIPHERAL Connection removed: {connection}')
|
||||
del self.peripheral_connections[connection.peer_address]
|
||||
|
||||
def on_link_encrypted(self, peer_address, _rand, _ediv, _ltk):
|
||||
# For now, just setup the encryption without asking the host
|
||||
@@ -542,15 +545,14 @@ class Controller:
|
||||
acl_packet = HCI_AclDataPacket(connection.handle, 2, 0, len(data), data)
|
||||
self.send_hci_packet(acl_packet)
|
||||
|
||||
def on_link_advertising_data(self, sender_address, data):
|
||||
def on_link_advertising_data(self, sender_address: Address, data: bytes):
|
||||
# Ignore if we're not scanning
|
||||
if self.le_scan_enable == 0:
|
||||
return
|
||||
|
||||
# Send a scan report
|
||||
report = HCI_LE_Advertising_Report_Event.Report(
|
||||
HCI_LE_Advertising_Report_Event.Report.FIELDS,
|
||||
event_type=HCI_LE_Advertising_Report_Event.ADV_IND,
|
||||
event_type=HCI_LE_Advertising_Report_Event.EventType.ADV_IND,
|
||||
address_type=sender_address.address_type,
|
||||
address=sender_address,
|
||||
data=data,
|
||||
@@ -560,8 +562,7 @@ class Controller:
|
||||
|
||||
# Simulate a scan response
|
||||
report = HCI_LE_Advertising_Report_Event.Report(
|
||||
HCI_LE_Advertising_Report_Event.Report.FIELDS,
|
||||
event_type=HCI_LE_Advertising_Report_Event.SCAN_RSP,
|
||||
event_type=HCI_LE_Advertising_Report_Event.EventType.SCAN_RSP,
|
||||
address_type=sender_address.address_type,
|
||||
address=sender_address,
|
||||
data=data,
|
||||
@@ -618,8 +619,8 @@ class Controller:
|
||||
cis_sync_delay=0,
|
||||
transport_latency_c_to_p=0,
|
||||
transport_latency_p_to_c=0,
|
||||
phy_c_to_p=0,
|
||||
phy_p_to_c=0,
|
||||
phy_c_to_p=1,
|
||||
phy_p_to_c=1,
|
||||
nse=0,
|
||||
bn_c_to_p=0,
|
||||
bn_p_to_c=0,
|
||||
@@ -695,7 +696,7 @@ class Controller:
|
||||
peer_address=peer_address,
|
||||
link=self.link,
|
||||
transport=PhysicalTransport.BR_EDR,
|
||||
link_type=HCI_Connection_Complete_Event.ACL_LINK_TYPE,
|
||||
link_type=HCI_Connection_Complete_Event.LinkType.ACL,
|
||||
)
|
||||
self.classic_connections[peer_address] = connection
|
||||
logger.debug(
|
||||
@@ -709,7 +710,7 @@ class Controller:
|
||||
connection_handle=connection_handle,
|
||||
bd_addr=peer_address,
|
||||
encryption_enabled=False,
|
||||
link_type=HCI_Connection_Complete_Event.ACL_LINK_TYPE,
|
||||
link_type=HCI_Connection_Complete_Event.LinkType.ACL,
|
||||
)
|
||||
)
|
||||
else:
|
||||
@@ -720,7 +721,7 @@ class Controller:
|
||||
connection_handle=0,
|
||||
bd_addr=peer_address,
|
||||
encryption_enabled=False,
|
||||
link_type=HCI_Connection_Complete_Event.ACL_LINK_TYPE,
|
||||
link_type=HCI_Connection_Complete_Event.LinkType.ACL,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -877,6 +878,14 @@ class Controller:
|
||||
else:
|
||||
# Remove the connection
|
||||
del self.central_connections[connection.peer_address]
|
||||
elif connection := self.find_peripheral_connection_by_handle(handle):
|
||||
if self.link:
|
||||
self.link.disconnect(
|
||||
self.random_address, connection.peer_address, command
|
||||
)
|
||||
else:
|
||||
# Remove the connection
|
||||
del self.peripheral_connections[connection.peer_address]
|
||||
elif connection := self.find_classic_connection_by_handle(handle):
|
||||
if self.link:
|
||||
self.link.classic_disconnect(
|
||||
@@ -945,7 +954,7 @@ class Controller:
|
||||
)
|
||||
)
|
||||
self.link.classic_sco_connect(
|
||||
self, connection.peer_address, HCI_Connection_Complete_Event.ESCO_LINK_TYPE
|
||||
self, connection.peer_address, HCI_Connection_Complete_Event.LinkType.ESCO
|
||||
)
|
||||
|
||||
def on_hci_enhanced_accept_synchronous_connection_request_command(self, command):
|
||||
@@ -974,10 +983,71 @@ class Controller:
|
||||
)
|
||||
)
|
||||
self.link.classic_accept_sco_connection(
|
||||
self, connection.peer_address, HCI_Connection_Complete_Event.ESCO_LINK_TYPE
|
||||
self, connection.peer_address, HCI_Connection_Complete_Event.LinkType.ESCO
|
||||
)
|
||||
|
||||
def on_hci_switch_role_command(self, command):
|
||||
def on_hci_sniff_mode_command(self, command: hci.HCI_Sniff_Mode_Command):
|
||||
'''
|
||||
See Bluetooth spec Vol 4, Part E - 7.2.2 Sniff Mode command
|
||||
'''
|
||||
if self.link is None:
|
||||
self.send_hci_packet(
|
||||
hci.HCI_Command_Status_Event(
|
||||
status=hci.HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR,
|
||||
num_hci_command_packets=1,
|
||||
command_opcode=command.op_code,
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
self.send_hci_packet(
|
||||
hci.HCI_Command_Status_Event(
|
||||
status=HCI_SUCCESS,
|
||||
num_hci_command_packets=1,
|
||||
command_opcode=command.op_code,
|
||||
)
|
||||
)
|
||||
self.send_hci_packet(
|
||||
hci.HCI_Mode_Change_Event(
|
||||
status=HCI_SUCCESS,
|
||||
connection_handle=command.connection_handle,
|
||||
current_mode=hci.HCI_Mode_Change_Event.Mode.SNIFF,
|
||||
interval=2,
|
||||
)
|
||||
)
|
||||
|
||||
def on_hci_exit_sniff_mode_command(self, command: hci.HCI_Exit_Sniff_Mode_Command):
|
||||
'''
|
||||
See Bluetooth spec Vol 4, Part E - 7.2.3 Exit Sniff Mode command
|
||||
'''
|
||||
|
||||
if self.link is None:
|
||||
self.send_hci_packet(
|
||||
hci.HCI_Command_Status_Event(
|
||||
status=hci.HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR,
|
||||
num_hci_command_packets=1,
|
||||
command_opcode=command.op_code,
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
self.send_hci_packet(
|
||||
hci.HCI_Command_Status_Event(
|
||||
status=HCI_SUCCESS,
|
||||
num_hci_command_packets=1,
|
||||
command_opcode=command.op_code,
|
||||
)
|
||||
)
|
||||
self.send_hci_packet(
|
||||
hci.HCI_Mode_Change_Event(
|
||||
status=HCI_SUCCESS,
|
||||
connection_handle=command.connection_handle,
|
||||
current_mode=hci.HCI_Mode_Change_Event.Mode.ACTIVE,
|
||||
interval=2,
|
||||
)
|
||||
)
|
||||
|
||||
def on_hci_switch_role_command(self, command: hci.HCI_Switch_Role_Command):
|
||||
'''
|
||||
See Bluetooth spec Vol 4, Part E - 7.2.8 Switch Role command
|
||||
'''
|
||||
@@ -1198,6 +1268,56 @@ class Controller:
|
||||
)
|
||||
return bytes([HCI_SUCCESS]) + bd_addr
|
||||
|
||||
def on_hci_le_set_default_subrate_command(
|
||||
self, command: hci.HCI_LE_Set_Default_Subrate_Command
|
||||
):
|
||||
'''
|
||||
See Bluetooth spec Vol 6, Part E - 7.8.123 LE Set Event Mask Command
|
||||
'''
|
||||
|
||||
if (
|
||||
command.subrate_max * (command.max_latency) > 500
|
||||
or command.subrate_max < command.subrate_min
|
||||
or command.continuation_number >= command.subrate_max
|
||||
):
|
||||
return bytes([HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR])
|
||||
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_le_subrate_request_command(
|
||||
self, command: hci.HCI_LE_Subrate_Request_Command
|
||||
):
|
||||
'''
|
||||
See Bluetooth spec Vol 6, Part E - 7.8.124 LE Subrate Request command
|
||||
'''
|
||||
if (
|
||||
command.subrate_max * (command.max_latency) > 500
|
||||
or command.continuation_number < command.continuation_number
|
||||
or command.subrate_max < command.subrate_min
|
||||
or command.continuation_number >= command.subrate_max
|
||||
):
|
||||
return bytes([HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR])
|
||||
|
||||
self.send_hci_packet(
|
||||
hci.HCI_Command_Status_Event(
|
||||
status=hci.HCI_SUCCESS,
|
||||
num_hci_command_packets=1,
|
||||
command_opcode=command.op_code,
|
||||
)
|
||||
)
|
||||
|
||||
self.send_hci_packet(
|
||||
hci.HCI_LE_Subrate_Change_Event(
|
||||
status=hci.HCI_SUCCESS,
|
||||
connection_handle=command.connection_handle,
|
||||
subrate_factor=2,
|
||||
peripheral_latency=2,
|
||||
continuation_number=command.continuation_number,
|
||||
supervision_timeout=command.supervision_timeout,
|
||||
)
|
||||
)
|
||||
return None
|
||||
|
||||
def on_hci_le_set_event_mask_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 4, Part E - 7.8.1 LE Set Event Mask Command
|
||||
@@ -1744,3 +1864,11 @@ class Controller:
|
||||
See Bluetooth spec Vol 4, Part E - 7.8.110 LE Remove ISO Data Path Command
|
||||
'''
|
||||
return struct.pack('<BH', HCI_SUCCESS, command.connection_handle)
|
||||
|
||||
def on_hci_le_set_host_feature_command(
|
||||
self, _command: hci.HCI_LE_Set_Host_Feature_Command
|
||||
):
|
||||
'''
|
||||
See Bluetooth spec Vol 4, Part E - 7.8.115 LE Set Host Feature command
|
||||
'''
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
+575
-239
File diff suppressed because it is too large
Load Diff
@@ -22,12 +22,12 @@ import operator
|
||||
import secrets
|
||||
|
||||
try:
|
||||
from bumble.crypto.cryptography import EccKey, e, aes_cmac
|
||||
from bumble.crypto.cryptography import EccKey, aes_cmac, e
|
||||
except ImportError:
|
||||
logging.getLogger(__name__).debug(
|
||||
"Unable to import cryptography, use built-in primitives."
|
||||
)
|
||||
from bumble.crypto.builtin import EccKey, e, aes_cmac # type: ignore[assignment]
|
||||
from bumble.crypto.builtin import EccKey, aes_cmac, e # type: ignore[assignment]
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -24,9 +24,9 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import dataclasses
|
||||
import functools
|
||||
import copy
|
||||
import secrets
|
||||
import struct
|
||||
from typing import Optional
|
||||
|
||||
@@ -16,11 +16,9 @@ from __future__ import annotations
|
||||
|
||||
import functools
|
||||
|
||||
from cryptography.hazmat.primitives import ciphers
|
||||
from cryptography.hazmat.primitives.ciphers import algorithms
|
||||
from cryptography.hazmat.primitives.ciphers import modes
|
||||
from cryptography.hazmat.primitives import ciphers, cmac
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
from cryptography.hazmat.primitives import cmac
|
||||
from cryptography.hazmat.primitives.ciphers import algorithms, modes
|
||||
|
||||
|
||||
def e(key: bytes, data: bytes) -> bytes:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+398
-232
File diff suppressed because it is too large
Load Diff
@@ -20,12 +20,13 @@ like loading firmware after a cold start.
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import pathlib
|
||||
import platform
|
||||
from typing import Iterable, Optional, TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Iterable, Optional
|
||||
|
||||
from bumble.drivers import rtk, intel
|
||||
from bumble.drivers import intel, rtk
|
||||
from bumble.drivers.common import Driver
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
+36
-40
@@ -20,6 +20,7 @@ Loosely based on the Fuchsia OS implementation.
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import collections
|
||||
import dataclasses
|
||||
@@ -28,12 +29,10 @@ import os
|
||||
import pathlib
|
||||
import platform
|
||||
import struct
|
||||
from typing import Any, Optional, TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
|
||||
from bumble import core
|
||||
from bumble import core, hci, utils
|
||||
from bumble.drivers import common
|
||||
from bumble import hci
|
||||
from bumble import utils
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bumble.host import Host
|
||||
@@ -90,54 +89,51 @@ HCI_INTEL_WRITE_BOOT_PARAMS_COMMAND = hci.hci_vendor_command_op_code(0x000E)
|
||||
hci.HCI_Command.register_commands(globals())
|
||||
|
||||
|
||||
@hci.HCI_Command.command(
|
||||
fields=[
|
||||
("param0", 1),
|
||||
],
|
||||
return_parameters_fields=[
|
||||
@hci.HCI_Command.command
|
||||
@dataclasses.dataclass
|
||||
class HCI_Intel_Read_Version_Command(hci.HCI_Command):
|
||||
param0: int = dataclasses.field(metadata=hci.metadata(1))
|
||||
|
||||
return_parameters_fields = [
|
||||
("status", hci.STATUS_SPEC),
|
||||
("tlv", "*"),
|
||||
],
|
||||
)
|
||||
class HCI_Intel_Read_Version_Command(hci.HCI_Command):
|
||||
pass
|
||||
]
|
||||
|
||||
|
||||
@hci.HCI_Command.command(
|
||||
fields=[("data_type", 1), ("data", "*")],
|
||||
return_parameters_fields=[
|
||||
("status", 1),
|
||||
],
|
||||
)
|
||||
@hci.HCI_Command.command
|
||||
@dataclasses.dataclass
|
||||
class Hci_Intel_Secure_Send_Command(hci.HCI_Command):
|
||||
pass
|
||||
data_type: int = dataclasses.field(metadata=hci.metadata(1))
|
||||
data: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
|
||||
return_parameters_fields = [
|
||||
("status", 1),
|
||||
]
|
||||
|
||||
|
||||
@hci.HCI_Command.command(
|
||||
fields=[
|
||||
("reset_type", 1),
|
||||
("patch_enable", 1),
|
||||
("ddc_reload", 1),
|
||||
("boot_option", 1),
|
||||
("boot_address", 4),
|
||||
],
|
||||
return_parameters_fields=[
|
||||
("data", "*"),
|
||||
],
|
||||
)
|
||||
@hci.HCI_Command.command
|
||||
@dataclasses.dataclass
|
||||
class HCI_Intel_Reset_Command(hci.HCI_Command):
|
||||
pass
|
||||
reset_type: int = dataclasses.field(metadata=hci.metadata(1))
|
||||
patch_enable: int = dataclasses.field(metadata=hci.metadata(1))
|
||||
ddc_reload: int = dataclasses.field(metadata=hci.metadata(1))
|
||||
boot_option: int = dataclasses.field(metadata=hci.metadata(1))
|
||||
boot_address: int = dataclasses.field(metadata=hci.metadata(4))
|
||||
|
||||
return_parameters_fields = [
|
||||
("data", "*"),
|
||||
]
|
||||
|
||||
|
||||
@hci.HCI_Command.command(
|
||||
fields=[("data", "*")],
|
||||
return_parameters_fields=[
|
||||
@hci.HCI_Command.command
|
||||
@dataclasses.dataclass
|
||||
class Hci_Intel_Write_Device_Config_Command(hci.HCI_Command):
|
||||
data: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
|
||||
return_parameters_fields = [
|
||||
("status", hci.STATUS_SPEC),
|
||||
("params", "*"),
|
||||
],
|
||||
)
|
||||
class Hci_Intel_Write_Device_Config_Command(hci.HCI_Command):
|
||||
pass
|
||||
]
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
+51
-40
@@ -17,10 +17,6 @@ Based on various online bits of information, including the Linux kernel.
|
||||
(see `drivers/bluetooth/btrtl.c`)
|
||||
"""
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from dataclasses import dataclass
|
||||
import asyncio
|
||||
import enum
|
||||
import logging
|
||||
@@ -31,16 +27,12 @@ import platform
|
||||
import struct
|
||||
import weakref
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from bumble import core
|
||||
from bumble.hci import (
|
||||
hci_vendor_command_op_code,
|
||||
STATUS_SPEC,
|
||||
HCI_SUCCESS,
|
||||
HCI_Command,
|
||||
HCI_Reset_Command,
|
||||
HCI_Read_Local_Version_Information_Command,
|
||||
)
|
||||
from bumble import core, hci
|
||||
from bumble.drivers import common
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -182,27 +174,29 @@ RTK_USB_PRODUCTS = {
|
||||
# -----------------------------------------------------------------------------
|
||||
# HCI Commands
|
||||
# -----------------------------------------------------------------------------
|
||||
HCI_RTK_READ_ROM_VERSION_COMMAND = hci_vendor_command_op_code(0x6D)
|
||||
HCI_RTK_DOWNLOAD_COMMAND = hci_vendor_command_op_code(0x20)
|
||||
HCI_RTK_DROP_FIRMWARE_COMMAND = hci_vendor_command_op_code(0x66)
|
||||
HCI_Command.register_commands(globals())
|
||||
HCI_RTK_READ_ROM_VERSION_COMMAND = hci.hci_vendor_command_op_code(0x6D)
|
||||
HCI_RTK_DOWNLOAD_COMMAND = hci.hci_vendor_command_op_code(0x20)
|
||||
HCI_RTK_DROP_FIRMWARE_COMMAND = hci.hci_vendor_command_op_code(0x66)
|
||||
hci.HCI_Command.register_commands(globals())
|
||||
|
||||
|
||||
@HCI_Command.command(return_parameters_fields=[("status", STATUS_SPEC), ("version", 1)])
|
||||
class HCI_RTK_Read_ROM_Version_Command(HCI_Command):
|
||||
pass
|
||||
@hci.HCI_Command.command
|
||||
@dataclass
|
||||
class HCI_RTK_Read_ROM_Version_Command(hci.HCI_Command):
|
||||
return_parameters_fields = [("status", hci.STATUS_SPEC), ("version", 1)]
|
||||
|
||||
|
||||
@HCI_Command.command(
|
||||
fields=[("index", 1), ("payload", RTK_FRAGMENT_LENGTH)],
|
||||
return_parameters_fields=[("status", STATUS_SPEC), ("index", 1)],
|
||||
)
|
||||
class HCI_RTK_Download_Command(HCI_Command):
|
||||
pass
|
||||
@hci.HCI_Command.command
|
||||
@dataclass
|
||||
class HCI_RTK_Download_Command(hci.HCI_Command):
|
||||
index: int = field(metadata=hci.metadata(1))
|
||||
payload: bytes = field(metadata=hci.metadata(RTK_FRAGMENT_LENGTH))
|
||||
return_parameters_fields = [("status", hci.STATUS_SPEC), ("index", 1)]
|
||||
|
||||
|
||||
@HCI_Command.command()
|
||||
class HCI_RTK_Drop_Firmware_Command(HCI_Command):
|
||||
@hci.HCI_Command.command
|
||||
@dataclass
|
||||
class HCI_RTK_Drop_Firmware_Command(hci.HCI_Command):
|
||||
pass
|
||||
|
||||
|
||||
@@ -494,21 +488,36 @@ class Driver(common.Driver):
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
async def get_loaded_firmware_version(host):
|
||||
response = await host.send_command(HCI_RTK_Read_ROM_Version_Command())
|
||||
|
||||
if response.return_parameters.status != hci.HCI_SUCCESS:
|
||||
return None
|
||||
|
||||
response = await host.send_command(
|
||||
hci.HCI_Read_Local_Version_Information_Command(), check_result=True
|
||||
)
|
||||
return (
|
||||
response.return_parameters.hci_subversion << 16
|
||||
| response.return_parameters.lmp_subversion
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def driver_info_for_host(cls, host):
|
||||
try:
|
||||
await host.send_command(
|
||||
HCI_Reset_Command(),
|
||||
hci.HCI_Reset_Command(),
|
||||
check_result=True,
|
||||
response_timeout=cls.POST_RESET_DELAY,
|
||||
)
|
||||
host.ready = True # Needed to let the host know the controller is ready.
|
||||
except asyncio.exceptions.TimeoutError:
|
||||
logger.warning("timeout waiting for hci reset, retrying")
|
||||
await host.send_command(HCI_Reset_Command(), check_result=True)
|
||||
await host.send_command(hci.HCI_Reset_Command(), check_result=True)
|
||||
host.ready = True
|
||||
|
||||
command = HCI_Read_Local_Version_Information_Command()
|
||||
command = hci.HCI_Read_Local_Version_Information_Command()
|
||||
response = await host.send_command(command, check_result=True)
|
||||
if response.command_opcode != command.op_code:
|
||||
logger.error("failed to probe local version information")
|
||||
@@ -595,9 +604,9 @@ class Driver(common.Driver):
|
||||
response = await self.host.send_command(
|
||||
HCI_RTK_Read_ROM_Version_Command(), check_result=True
|
||||
)
|
||||
if response.return_parameters.status != HCI_SUCCESS:
|
||||
if response.return_parameters.status != hci.HCI_SUCCESS:
|
||||
logger.warning("can't get ROM version")
|
||||
return
|
||||
return None
|
||||
rom_version = response.return_parameters.version
|
||||
logger.debug(f"ROM version before download: {rom_version:04X}")
|
||||
else:
|
||||
@@ -605,13 +614,14 @@ class Driver(common.Driver):
|
||||
|
||||
firmware = Firmware(self.firmware)
|
||||
logger.debug(f"firmware: project_id=0x{firmware.project_id:04X}")
|
||||
logger.debug(f"firmware: version=0x{firmware.version:04X}")
|
||||
for patch in firmware.patches:
|
||||
if patch[0] == rom_version + 1:
|
||||
logger.debug(f"using patch {patch[0]}")
|
||||
break
|
||||
else:
|
||||
logger.warning("no valid patch found for rom version {rom_version}")
|
||||
return
|
||||
return None
|
||||
|
||||
# Append the config if there is one.
|
||||
if self.config:
|
||||
@@ -633,9 +643,8 @@ class Driver(common.Driver):
|
||||
fragment = payload[fragment_offset : fragment_offset + RTK_FRAGMENT_LENGTH]
|
||||
logger.debug(f"downloading fragment {fragment_index}")
|
||||
await self.host.send_command(
|
||||
HCI_RTK_Download_Command(
|
||||
index=download_index, payload=fragment, check_result=True
|
||||
)
|
||||
HCI_RTK_Download_Command(index=download_index, payload=fragment),
|
||||
check_result=True,
|
||||
)
|
||||
|
||||
logger.debug("download complete!")
|
||||
@@ -644,11 +653,13 @@ class Driver(common.Driver):
|
||||
response = await self.host.send_command(
|
||||
HCI_RTK_Read_ROM_Version_Command(), check_result=True
|
||||
)
|
||||
if response.return_parameters.status != HCI_SUCCESS:
|
||||
if response.return_parameters.status != hci.HCI_SUCCESS:
|
||||
logger.warning("can't get ROM version")
|
||||
else:
|
||||
rom_version = response.return_parameters.version
|
||||
logger.debug(f"ROM version after download: {rom_version:04X}")
|
||||
logger.debug(f"ROM version after download: {rom_version:02X}")
|
||||
|
||||
return firmware.version
|
||||
|
||||
async def download_firmware(self):
|
||||
if self.driver_info.rom == RTK_ROM_LMP_8723A:
|
||||
@@ -667,7 +678,7 @@ class Driver(common.Driver):
|
||||
|
||||
async def init_controller(self):
|
||||
await self.download_firmware()
|
||||
await self.host.send_command(HCI_Reset_Command(), check_result=True)
|
||||
await self.host.send_command(hci.HCI_Reset_Command(), check_result=True)
|
||||
logger.info(f"loaded FW image {self.driver_info.fw_name}")
|
||||
|
||||
|
||||
|
||||
+4
-4
@@ -19,11 +19,11 @@ import logging
|
||||
import struct
|
||||
|
||||
from bumble.gatt import (
|
||||
Service,
|
||||
Characteristic,
|
||||
GATT_GENERIC_ACCESS_SERVICE,
|
||||
GATT_DEVICE_NAME_CHARACTERISTIC,
|
||||
GATT_APPEARANCE_CHARACTERISTIC,
|
||||
GATT_DEVICE_NAME_CHARACTERISTIC,
|
||||
GATT_GENERIC_ACCESS_SERVICE,
|
||||
Characteristic,
|
||||
Service,
|
||||
)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
+3
-2
@@ -23,15 +23,16 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
import functools
|
||||
import logging
|
||||
import struct
|
||||
from typing import Iterable, Optional, Sequence, TypeVar, Union
|
||||
|
||||
from bumble.colors import color
|
||||
from bumble.core import BaseBumbleError, UUID
|
||||
from bumble.att import Attribute, AttributeValue
|
||||
from bumble.colors import color
|
||||
from bumble.core import UUID, BaseBumbleError
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Typing
|
||||
|
||||
+4
-12
@@ -20,22 +20,14 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
import struct
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Generic,
|
||||
Iterable,
|
||||
Literal,
|
||||
Optional,
|
||||
TypeVar,
|
||||
)
|
||||
|
||||
import struct
|
||||
from typing import Any, Callable, Generic, Iterable, Literal, Optional, TypeVar
|
||||
|
||||
from bumble import utils
|
||||
from bumble.core import InvalidOperationError
|
||||
from bumble.gatt import Characteristic
|
||||
from bumble.gatt_client import CharacteristicProxy
|
||||
from bumble import utils
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Typing
|
||||
|
||||
+69
-81
@@ -24,60 +24,38 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import struct
|
||||
from datetime import datetime
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
Generic,
|
||||
Iterable,
|
||||
Optional,
|
||||
Union,
|
||||
TypeVar,
|
||||
TYPE_CHECKING,
|
||||
Union,
|
||||
)
|
||||
|
||||
|
||||
from bumble import att, core, utils
|
||||
from bumble.colors import color
|
||||
from bumble.hci import HCI_Constant
|
||||
from bumble.att import (
|
||||
ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
||||
ATT_ATTRIBUTE_NOT_LONG_ERROR,
|
||||
ATT_CID,
|
||||
ATT_DEFAULT_MTU,
|
||||
ATT_ERROR_RESPONSE,
|
||||
ATT_INVALID_OFFSET_ERROR,
|
||||
ATT_PDU,
|
||||
ATT_RESPONSES,
|
||||
ATT_Exchange_MTU_Request,
|
||||
ATT_Find_By_Type_Value_Request,
|
||||
ATT_Find_Information_Request,
|
||||
ATT_Handle_Value_Confirmation,
|
||||
ATT_Read_Blob_Request,
|
||||
ATT_Read_By_Group_Type_Request,
|
||||
ATT_Read_By_Type_Request,
|
||||
ATT_Read_Request,
|
||||
ATT_Write_Command,
|
||||
ATT_Write_Request,
|
||||
ATT_Error,
|
||||
)
|
||||
from bumble import utils
|
||||
from bumble import core
|
||||
from bumble.core import UUID, InvalidStateError
|
||||
from bumble.gatt import (
|
||||
GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
|
||||
GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
|
||||
GATT_INCLUDE_ATTRIBUTE_TYPE,
|
||||
GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
|
||||
GATT_REQUEST_TIMEOUT,
|
||||
GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE,
|
||||
GATT_INCLUDE_ATTRIBUTE_TYPE,
|
||||
Characteristic,
|
||||
ClientCharacteristicConfigurationBits,
|
||||
InvalidServiceError,
|
||||
TemplateService,
|
||||
)
|
||||
from bumble.hci import HCI_Constant
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Typing
|
||||
@@ -291,8 +269,8 @@ class Client:
|
||||
indication_subscribers: dict[
|
||||
int, set[Union[CharacteristicProxy, Callable[[bytes], Any]]]
|
||||
]
|
||||
pending_response: Optional[asyncio.futures.Future[ATT_PDU]]
|
||||
pending_request: Optional[ATT_PDU]
|
||||
pending_response: Optional[asyncio.futures.Future[att.ATT_PDU]]
|
||||
pending_request: Optional[att.ATT_PDU]
|
||||
|
||||
def __init__(self, connection: Connection) -> None:
|
||||
self.connection = connection
|
||||
@@ -308,15 +286,15 @@ class Client:
|
||||
connection.on(connection.EVENT_DISCONNECTION, self.on_disconnection)
|
||||
|
||||
def send_gatt_pdu(self, pdu: bytes) -> None:
|
||||
self.connection.send_l2cap_pdu(ATT_CID, pdu)
|
||||
self.connection.send_l2cap_pdu(att.ATT_CID, pdu)
|
||||
|
||||
async def send_command(self, command: ATT_PDU) -> None:
|
||||
async def send_command(self, command: att.ATT_PDU) -> None:
|
||||
logger.debug(
|
||||
f'GATT Command from client: [0x{self.connection.handle:04X}] {command}'
|
||||
)
|
||||
self.send_gatt_pdu(bytes(command))
|
||||
|
||||
async def send_request(self, request: ATT_PDU):
|
||||
async def send_request(self, request: att.ATT_PDU):
|
||||
logger.debug(
|
||||
f'GATT Request from client: [0x{self.connection.handle:04X}] {request}'
|
||||
)
|
||||
@@ -345,7 +323,9 @@ class Client:
|
||||
|
||||
return response
|
||||
|
||||
def send_confirmation(self, confirmation: ATT_Handle_Value_Confirmation) -> None:
|
||||
def send_confirmation(
|
||||
self, confirmation: att.ATT_Handle_Value_Confirmation
|
||||
) -> None:
|
||||
logger.debug(
|
||||
f'GATT Confirmation from client: [0x{self.connection.handle:04X}] '
|
||||
f'{confirmation}'
|
||||
@@ -354,8 +334,8 @@ class Client:
|
||||
|
||||
async def request_mtu(self, mtu: int) -> int:
|
||||
# Check the range
|
||||
if mtu < ATT_DEFAULT_MTU:
|
||||
raise core.InvalidArgumentError(f'MTU must be >= {ATT_DEFAULT_MTU}')
|
||||
if mtu < att.ATT_DEFAULT_MTU:
|
||||
raise core.InvalidArgumentError(f'MTU must be >= {att.ATT_DEFAULT_MTU}')
|
||||
if mtu > 0xFFFF:
|
||||
raise core.InvalidArgumentError('MTU must be <= 0xFFFF')
|
||||
|
||||
@@ -365,9 +345,11 @@ class Client:
|
||||
|
||||
# Send the request
|
||||
self.mtu_exchange_done = True
|
||||
response = await self.send_request(ATT_Exchange_MTU_Request(client_rx_mtu=mtu))
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
raise ATT_Error(error_code=response.error_code, message=response)
|
||||
response = await self.send_request(
|
||||
att.ATT_Exchange_MTU_Request(client_rx_mtu=mtu)
|
||||
)
|
||||
if response.op_code == att.Opcode.ATT_ERROR_RESPONSE:
|
||||
raise att.ATT_Error(error_code=response.error_code, message=response)
|
||||
|
||||
# Compute the final MTU
|
||||
self.connection.att_mtu = min(mtu, response.server_rx_mtu)
|
||||
@@ -432,7 +414,7 @@ class Client:
|
||||
services = []
|
||||
while starting_handle < 0xFFFF:
|
||||
response = await self.send_request(
|
||||
ATT_Read_By_Group_Type_Request(
|
||||
att.ATT_Read_By_Group_Type_Request(
|
||||
starting_handle=starting_handle,
|
||||
ending_handle=0xFFFF,
|
||||
attribute_group_type=GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
|
||||
@@ -443,14 +425,14 @@ class Client:
|
||||
return []
|
||||
|
||||
# Check if we reached the end of the iteration
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
if response.op_code == att.Opcode.ATT_ERROR_RESPONSE:
|
||||
if response.error_code != att.ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
# Unexpected end
|
||||
logger.warning(
|
||||
'!!! unexpected error while discovering services: '
|
||||
f'{HCI_Constant.error_name(response.error_code)}'
|
||||
)
|
||||
raise ATT_Error(
|
||||
raise att.ATT_Error(
|
||||
error_code=response.error_code,
|
||||
message='Unexpected error while discovering services',
|
||||
)
|
||||
@@ -509,7 +491,7 @@ class Client:
|
||||
services = []
|
||||
while starting_handle < 0xFFFF:
|
||||
response = await self.send_request(
|
||||
ATT_Find_By_Type_Value_Request(
|
||||
att.ATT_Find_By_Type_Value_Request(
|
||||
starting_handle=starting_handle,
|
||||
ending_handle=0xFFFF,
|
||||
attribute_type=GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
|
||||
@@ -521,8 +503,8 @@ class Client:
|
||||
return []
|
||||
|
||||
# Check if we reached the end of the iteration
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
if response.op_code == att.Opcode.ATT_ERROR_RESPONSE:
|
||||
if response.error_code != att.ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
# Unexpected end
|
||||
logger.warning(
|
||||
'!!! unexpected error while discovering services: '
|
||||
@@ -578,7 +560,7 @@ class Client:
|
||||
included_services: list[ServiceProxy] = []
|
||||
while starting_handle <= ending_handle:
|
||||
response = await self.send_request(
|
||||
ATT_Read_By_Type_Request(
|
||||
att.ATT_Read_By_Type_Request(
|
||||
starting_handle=starting_handle,
|
||||
ending_handle=ending_handle,
|
||||
attribute_type=GATT_INCLUDE_ATTRIBUTE_TYPE,
|
||||
@@ -589,14 +571,14 @@ class Client:
|
||||
return []
|
||||
|
||||
# Check if we reached the end of the iteration
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
if response.op_code == att.Opcode.ATT_ERROR_RESPONSE:
|
||||
if response.error_code != att.ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
# Unexpected end
|
||||
logger.warning(
|
||||
'!!! unexpected error while discovering included services: '
|
||||
f'{HCI_Constant.error_name(response.error_code)}'
|
||||
)
|
||||
raise ATT_Error(
|
||||
raise att.ATT_Error(
|
||||
error_code=response.error_code,
|
||||
message='Unexpected error while discovering included services',
|
||||
)
|
||||
@@ -652,7 +634,7 @@ class Client:
|
||||
characteristics: list[CharacteristicProxy[bytes]] = []
|
||||
while starting_handle <= ending_handle:
|
||||
response = await self.send_request(
|
||||
ATT_Read_By_Type_Request(
|
||||
att.ATT_Read_By_Type_Request(
|
||||
starting_handle=starting_handle,
|
||||
ending_handle=ending_handle,
|
||||
attribute_type=GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
|
||||
@@ -663,14 +645,14 @@ class Client:
|
||||
return []
|
||||
|
||||
# Check if we reached the end of the iteration
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
if response.op_code == att.Opcode.ATT_ERROR_RESPONSE:
|
||||
if response.error_code != att.ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
# Unexpected end
|
||||
logger.warning(
|
||||
'!!! unexpected error while discovering characteristics: '
|
||||
f'{HCI_Constant.error_name(response.error_code)}'
|
||||
)
|
||||
raise ATT_Error(
|
||||
raise att.ATT_Error(
|
||||
error_code=response.error_code,
|
||||
message='Unexpected error while discovering characteristics',
|
||||
)
|
||||
@@ -736,7 +718,7 @@ class Client:
|
||||
descriptors: list[DescriptorProxy] = []
|
||||
while starting_handle <= ending_handle:
|
||||
response = await self.send_request(
|
||||
ATT_Find_Information_Request(
|
||||
att.ATT_Find_Information_Request(
|
||||
starting_handle=starting_handle, ending_handle=ending_handle
|
||||
)
|
||||
)
|
||||
@@ -745,8 +727,8 @@ class Client:
|
||||
return []
|
||||
|
||||
# Check if we reached the end of the iteration
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
if response.op_code == att.Opcode.ATT_ERROR_RESPONSE:
|
||||
if response.error_code != att.ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
# Unexpected end
|
||||
logger.warning(
|
||||
'!!! unexpected error while discovering descriptors: '
|
||||
@@ -791,7 +773,7 @@ class Client:
|
||||
attributes = []
|
||||
while True:
|
||||
response = await self.send_request(
|
||||
ATT_Find_Information_Request(
|
||||
att.ATT_Find_Information_Request(
|
||||
starting_handle=starting_handle, ending_handle=ending_handle
|
||||
)
|
||||
)
|
||||
@@ -799,8 +781,8 @@ class Client:
|
||||
return []
|
||||
|
||||
# Check if we reached the end of the iteration
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
if response.op_code == att.Opcode.ATT_ERROR_RESPONSE:
|
||||
if response.error_code != att.ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
# Unexpected end
|
||||
logger.warning(
|
||||
'!!! unexpected error while discovering attributes: '
|
||||
@@ -954,12 +936,12 @@ class Client:
|
||||
# Send a request to read
|
||||
attribute_handle = attribute if isinstance(attribute, int) else attribute.handle
|
||||
response = await self.send_request(
|
||||
ATT_Read_Request(attribute_handle=attribute_handle)
|
||||
att.ATT_Read_Request(attribute_handle=attribute_handle)
|
||||
)
|
||||
if response is None:
|
||||
raise TimeoutError('read timeout')
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
raise ATT_Error(error_code=response.error_code, message=response)
|
||||
if response.op_code == att.Opcode.ATT_ERROR_RESPONSE:
|
||||
raise att.ATT_Error(error_code=response.error_code, message=response)
|
||||
|
||||
# If the value is the max size for the MTU, try to read more unless the caller
|
||||
# specifically asked not to do that
|
||||
@@ -969,19 +951,21 @@ class Client:
|
||||
offset = len(attribute_value)
|
||||
while True:
|
||||
response = await self.send_request(
|
||||
ATT_Read_Blob_Request(
|
||||
att.ATT_Read_Blob_Request(
|
||||
attribute_handle=attribute_handle, value_offset=offset
|
||||
)
|
||||
)
|
||||
if response is None:
|
||||
raise TimeoutError('read timeout')
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
if response.op_code == att.Opcode.ATT_ERROR_RESPONSE:
|
||||
if response.error_code in (
|
||||
ATT_ATTRIBUTE_NOT_LONG_ERROR,
|
||||
ATT_INVALID_OFFSET_ERROR,
|
||||
att.ATT_ATTRIBUTE_NOT_LONG_ERROR,
|
||||
att.ATT_INVALID_OFFSET_ERROR,
|
||||
):
|
||||
break
|
||||
raise ATT_Error(error_code=response.error_code, message=response)
|
||||
raise att.ATT_Error(
|
||||
error_code=response.error_code, message=response
|
||||
)
|
||||
|
||||
part = response.part_attribute_value
|
||||
attribute_value += part
|
||||
@@ -1012,7 +996,7 @@ class Client:
|
||||
characteristics_values = []
|
||||
while starting_handle <= ending_handle:
|
||||
response = await self.send_request(
|
||||
ATT_Read_By_Type_Request(
|
||||
att.ATT_Read_By_Type_Request(
|
||||
starting_handle=starting_handle,
|
||||
ending_handle=ending_handle,
|
||||
attribute_type=uuid,
|
||||
@@ -1023,8 +1007,8 @@ class Client:
|
||||
return []
|
||||
|
||||
# Check if we reached the end of the iteration
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
if response.op_code == att.Opcode.ATT_ERROR_RESPONSE:
|
||||
if response.error_code != att.ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
# Unexpected end
|
||||
logger.warning(
|
||||
'!!! unexpected error while reading characteristics: '
|
||||
@@ -1069,15 +1053,15 @@ class Client:
|
||||
attribute_handle = attribute if isinstance(attribute, int) else attribute.handle
|
||||
if with_response:
|
||||
response = await self.send_request(
|
||||
ATT_Write_Request(
|
||||
att.ATT_Write_Request(
|
||||
attribute_handle=attribute_handle, attribute_value=value
|
||||
)
|
||||
)
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
raise ATT_Error(error_code=response.error_code, message=response)
|
||||
if response.op_code == att.Opcode.ATT_ERROR_RESPONSE:
|
||||
raise att.ATT_Error(error_code=response.error_code, message=response)
|
||||
else:
|
||||
await self.send_command(
|
||||
ATT_Write_Command(
|
||||
att.ATT_Write_Command(
|
||||
attribute_handle=attribute_handle, attribute_value=value
|
||||
)
|
||||
)
|
||||
@@ -1086,11 +1070,11 @@ class Client:
|
||||
if self.pending_response and not self.pending_response.done():
|
||||
self.pending_response.cancel()
|
||||
|
||||
def on_gatt_pdu(self, att_pdu: ATT_PDU) -> None:
|
||||
def on_gatt_pdu(self, att_pdu: att.ATT_PDU) -> None:
|
||||
logger.debug(
|
||||
f'GATT Response to client: [0x{self.connection.handle:04X}] {att_pdu}'
|
||||
)
|
||||
if att_pdu.op_code in ATT_RESPONSES:
|
||||
if att_pdu.op_code in att.ATT_RESPONSES:
|
||||
if self.pending_request is None:
|
||||
# Not expected!
|
||||
logger.warning('!!! unexpected response, there is no pending request')
|
||||
@@ -1098,7 +1082,7 @@ class Client:
|
||||
|
||||
# The response should match the pending request unless it is
|
||||
# an error response
|
||||
if att_pdu.op_code != ATT_ERROR_RESPONSE:
|
||||
if att_pdu.op_code != att.Opcode.ATT_ERROR_RESPONSE:
|
||||
expected_response_name = self.pending_request.name.replace(
|
||||
'_REQUEST', '_RESPONSE'
|
||||
)
|
||||
@@ -1126,7 +1110,9 @@ class Client:
|
||||
+ str(att_pdu)
|
||||
)
|
||||
|
||||
def on_att_handle_value_notification(self, notification):
|
||||
def on_att_handle_value_notification(
|
||||
self, notification: att.ATT_Handle_Value_Notification
|
||||
):
|
||||
# Call all subscribers
|
||||
subscribers = self.notification_subscribers.get(
|
||||
notification.attribute_handle, set()
|
||||
@@ -1141,7 +1127,9 @@ class Client:
|
||||
else:
|
||||
subscriber.emit(subscriber.EVENT_UPDATE, notification.attribute_value)
|
||||
|
||||
def on_att_handle_value_indication(self, indication):
|
||||
def on_att_handle_value_indication(
|
||||
self, indication: att.ATT_Handle_Value_Indication
|
||||
):
|
||||
# Call all subscribers
|
||||
subscribers = self.indication_subscribers.get(
|
||||
indication.attribute_handle, set()
|
||||
@@ -1157,7 +1145,7 @@ class Client:
|
||||
subscriber.emit(subscriber.EVENT_UPDATE, indication.attribute_value)
|
||||
|
||||
# Confirm that we received the indication
|
||||
self.send_confirmation(ATT_Handle_Value_Confirmation())
|
||||
self.send_confirmation(att.ATT_Handle_Value_Confirmation())
|
||||
|
||||
def cache_value(self, attribute_handle: int, value: bytes) -> None:
|
||||
self.cached_values[attribute_handle] = (
|
||||
|
||||
+124
-124
@@ -13,7 +13,7 @@
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# GATT - Generic Attribute Profile
|
||||
# GATT - Generic att.Attribute Profile
|
||||
# Server
|
||||
#
|
||||
# See Bluetooth spec @ Vol 3, Part G
|
||||
@@ -24,46 +24,16 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
import struct
|
||||
from typing import (
|
||||
Iterable,
|
||||
Optional,
|
||||
TypeVar,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, Iterable, Optional, TypeVar
|
||||
|
||||
from bumble import att, utils
|
||||
from bumble.colors import color
|
||||
from bumble.core import UUID
|
||||
from bumble.att import (
|
||||
ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
||||
ATT_ATTRIBUTE_NOT_LONG_ERROR,
|
||||
ATT_CID,
|
||||
ATT_DEFAULT_MTU,
|
||||
ATT_INVALID_ATTRIBUTE_LENGTH_ERROR,
|
||||
ATT_INVALID_HANDLE_ERROR,
|
||||
ATT_INVALID_OFFSET_ERROR,
|
||||
ATT_REQUEST_NOT_SUPPORTED_ERROR,
|
||||
ATT_REQUESTS,
|
||||
ATT_PDU,
|
||||
ATT_UNLIKELY_ERROR_ERROR,
|
||||
ATT_UNSUPPORTED_GROUP_TYPE_ERROR,
|
||||
ATT_Error,
|
||||
ATT_Error_Response,
|
||||
ATT_Exchange_MTU_Response,
|
||||
ATT_Find_By_Type_Value_Response,
|
||||
ATT_Find_Information_Response,
|
||||
ATT_Handle_Value_Indication,
|
||||
ATT_Handle_Value_Notification,
|
||||
ATT_Read_Blob_Response,
|
||||
ATT_Read_By_Group_Type_Response,
|
||||
ATT_Read_By_Type_Response,
|
||||
ATT_Read_Response,
|
||||
ATT_Write_Response,
|
||||
Attribute,
|
||||
)
|
||||
from bumble.gatt import (
|
||||
GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
|
||||
GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
|
||||
@@ -74,14 +44,13 @@ from bumble.gatt import (
|
||||
Characteristic,
|
||||
CharacteristicDeclaration,
|
||||
CharacteristicValue,
|
||||
IncludedServiceDeclaration,
|
||||
Descriptor,
|
||||
IncludedServiceDeclaration,
|
||||
Service,
|
||||
)
|
||||
from bumble import utils
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bumble.device import Device, Connection
|
||||
from bumble.device import Connection, Device
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -99,9 +68,9 @@ GATT_SERVER_DEFAULT_MAX_MTU = 517
|
||||
# GATT Server
|
||||
# -----------------------------------------------------------------------------
|
||||
class Server(utils.EventEmitter):
|
||||
attributes: list[Attribute]
|
||||
attributes: list[att.Attribute]
|
||||
services: list[Service]
|
||||
attributes_by_handle: dict[int, Attribute]
|
||||
attributes_by_handle: dict[int, att.Attribute]
|
||||
subscribers: dict[int, dict[int, bytes]]
|
||||
indication_semaphores: defaultdict[int, asyncio.Semaphore]
|
||||
pending_confirmations: defaultdict[int, Optional[asyncio.futures.Future]]
|
||||
@@ -112,7 +81,7 @@ class Server(utils.EventEmitter):
|
||||
super().__init__()
|
||||
self.device = device
|
||||
self.services = []
|
||||
self.attributes = [] # Attributes, ordered by increasing handle values
|
||||
self.attributes = [] # att.Attributes, ordered by increasing handle values
|
||||
self.attributes_by_handle = {} # Map for fast attribute access by handle
|
||||
self.max_mtu = (
|
||||
GATT_SERVER_DEFAULT_MAX_MTU # The max MTU we're willing to negotiate
|
||||
@@ -127,12 +96,12 @@ class Server(utils.EventEmitter):
|
||||
return "\n".join(map(str, self.attributes))
|
||||
|
||||
def send_gatt_pdu(self, connection_handle: int, pdu: bytes) -> None:
|
||||
self.device.send_l2cap_pdu(connection_handle, ATT_CID, pdu)
|
||||
self.device.send_l2cap_pdu(connection_handle, att.ATT_CID, pdu)
|
||||
|
||||
def next_handle(self) -> int:
|
||||
return 1 + len(self.attributes)
|
||||
|
||||
def get_advertising_service_data(self) -> dict[Attribute, bytes]:
|
||||
def get_advertising_service_data(self) -> dict[att.Attribute, bytes]:
|
||||
return {
|
||||
attribute: data
|
||||
for attribute in self.attributes
|
||||
@@ -140,7 +109,7 @@ class Server(utils.EventEmitter):
|
||||
and (data := attribute.get_advertising_data())
|
||||
}
|
||||
|
||||
def get_attribute(self, handle: int) -> Optional[Attribute]:
|
||||
def get_attribute(self, handle: int) -> Optional[att.Attribute]:
|
||||
attribute = self.attributes_by_handle.get(handle)
|
||||
if attribute:
|
||||
return attribute
|
||||
@@ -231,7 +200,7 @@ class Server(utils.EventEmitter):
|
||||
None,
|
||||
)
|
||||
|
||||
def add_attribute(self, attribute: Attribute) -> None:
|
||||
def add_attribute(self, attribute: att.Attribute) -> None:
|
||||
# Assign a handle to this attribute
|
||||
attribute.handle = self.next_handle()
|
||||
attribute.end_group_handle = (
|
||||
@@ -286,7 +255,7 @@ class Server(utils.EventEmitter):
|
||||
# pylint: disable=line-too-long
|
||||
Descriptor(
|
||||
GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
|
||||
Attribute.READABLE | Attribute.WRITEABLE,
|
||||
att.Attribute.READABLE | att.Attribute.WRITEABLE,
|
||||
CharacteristicValue(
|
||||
read=lambda connection, characteristic=characteristic: self.read_cccd(
|
||||
connection, characteristic
|
||||
@@ -355,7 +324,7 @@ class Server(utils.EventEmitter):
|
||||
indicate_enabled,
|
||||
)
|
||||
|
||||
def send_response(self, connection: Connection, response: ATT_PDU) -> None:
|
||||
def send_response(self, connection: Connection, response: att.ATT_PDU) -> None:
|
||||
logger.debug(
|
||||
f'GATT Response from server: [0x{connection.handle:04X}] {response}'
|
||||
)
|
||||
@@ -364,7 +333,7 @@ class Server(utils.EventEmitter):
|
||||
async def notify_subscriber(
|
||||
self,
|
||||
connection: Connection,
|
||||
attribute: Attribute,
|
||||
attribute: att.Attribute,
|
||||
value: Optional[bytes] = None,
|
||||
force: bool = False,
|
||||
) -> None:
|
||||
@@ -396,7 +365,7 @@ class Server(utils.EventEmitter):
|
||||
value = value[: connection.att_mtu - 3]
|
||||
|
||||
# Notify
|
||||
notification = ATT_Handle_Value_Notification(
|
||||
notification = att.ATT_Handle_Value_Notification(
|
||||
attribute_handle=attribute.handle, attribute_value=value
|
||||
)
|
||||
logger.debug(
|
||||
@@ -407,7 +376,7 @@ class Server(utils.EventEmitter):
|
||||
async def indicate_subscriber(
|
||||
self,
|
||||
connection: Connection,
|
||||
attribute: Attribute,
|
||||
attribute: att.Attribute,
|
||||
value: Optional[bytes] = None,
|
||||
force: bool = False,
|
||||
) -> None:
|
||||
@@ -439,7 +408,7 @@ class Server(utils.EventEmitter):
|
||||
value = value[: connection.att_mtu - 3]
|
||||
|
||||
# Indicate
|
||||
indication = ATT_Handle_Value_Indication(
|
||||
indication = att.ATT_Handle_Value_Indication(
|
||||
attribute_handle=attribute.handle, attribute_value=value
|
||||
)
|
||||
logger.debug(
|
||||
@@ -467,7 +436,7 @@ class Server(utils.EventEmitter):
|
||||
async def _notify_or_indicate_subscribers(
|
||||
self,
|
||||
indicate: bool,
|
||||
attribute: Attribute,
|
||||
attribute: att.Attribute,
|
||||
value: Optional[bytes] = None,
|
||||
force: bool = False,
|
||||
) -> None:
|
||||
@@ -494,7 +463,7 @@ class Server(utils.EventEmitter):
|
||||
|
||||
async def notify_subscribers(
|
||||
self,
|
||||
attribute: Attribute,
|
||||
attribute: att.Attribute,
|
||||
value: Optional[bytes] = None,
|
||||
force: bool = False,
|
||||
):
|
||||
@@ -504,7 +473,7 @@ class Server(utils.EventEmitter):
|
||||
|
||||
async def indicate_subscribers(
|
||||
self,
|
||||
attribute: Attribute,
|
||||
attribute: att.Attribute,
|
||||
value: Optional[bytes] = None,
|
||||
force: bool = False,
|
||||
):
|
||||
@@ -518,33 +487,33 @@ class Server(utils.EventEmitter):
|
||||
if connection.handle in self.pending_confirmations:
|
||||
del self.pending_confirmations[connection.handle]
|
||||
|
||||
def on_gatt_pdu(self, connection: Connection, att_pdu: ATT_PDU) -> None:
|
||||
def on_gatt_pdu(self, connection: Connection, att_pdu: att.ATT_PDU) -> None:
|
||||
logger.debug(f'GATT Request to server: [0x{connection.handle:04X}] {att_pdu}')
|
||||
handler_name = f'on_{att_pdu.name.lower()}'
|
||||
handler = getattr(self, handler_name, None)
|
||||
if handler is not None:
|
||||
try:
|
||||
handler(connection, att_pdu)
|
||||
except ATT_Error as error:
|
||||
except att.ATT_Error as error:
|
||||
logger.debug(f'normal exception returned by handler: {error}')
|
||||
response = ATT_Error_Response(
|
||||
response = att.ATT_Error_Response(
|
||||
request_opcode_in_error=att_pdu.op_code,
|
||||
attribute_handle_in_error=error.att_handle,
|
||||
error_code=error.error_code,
|
||||
)
|
||||
self.send_response(connection, response)
|
||||
except Exception as error:
|
||||
logger.warning(f'{color("!!! Exception in handler:", "red")} {error}')
|
||||
response = ATT_Error_Response(
|
||||
except Exception:
|
||||
logger.exception(color("!!! Exception in handler:", "red"))
|
||||
response = att.ATT_Error_Response(
|
||||
request_opcode_in_error=att_pdu.op_code,
|
||||
attribute_handle_in_error=0x0000,
|
||||
error_code=ATT_UNLIKELY_ERROR_ERROR,
|
||||
error_code=att.ATT_UNLIKELY_ERROR_ERROR,
|
||||
)
|
||||
self.send_response(connection, response)
|
||||
raise error
|
||||
raise
|
||||
else:
|
||||
# No specific handler registered
|
||||
if att_pdu.op_code in ATT_REQUESTS:
|
||||
if att_pdu.op_code in att.ATT_REQUESTS:
|
||||
# Invoke the generic handler
|
||||
self.on_att_request(connection, att_pdu)
|
||||
else:
|
||||
@@ -560,7 +529,7 @@ class Server(utils.EventEmitter):
|
||||
#######################################################
|
||||
# ATT handlers
|
||||
#######################################################
|
||||
def on_att_request(self, connection: Connection, pdu: ATT_PDU) -> None:
|
||||
def on_att_request(self, connection: Connection, pdu: att.ATT_PDU) -> None:
|
||||
'''
|
||||
Handler for requests without a more specific handler
|
||||
'''
|
||||
@@ -570,23 +539,25 @@ class Server(utils.EventEmitter):
|
||||
)
|
||||
+ str(pdu)
|
||||
)
|
||||
response = ATT_Error_Response(
|
||||
response = att.ATT_Error_Response(
|
||||
request_opcode_in_error=pdu.op_code,
|
||||
attribute_handle_in_error=0x0000,
|
||||
error_code=ATT_REQUEST_NOT_SUPPORTED_ERROR,
|
||||
error_code=att.ATT_REQUEST_NOT_SUPPORTED_ERROR,
|
||||
)
|
||||
self.send_response(connection, response)
|
||||
|
||||
def on_att_exchange_mtu_request(self, connection, request):
|
||||
def on_att_exchange_mtu_request(
|
||||
self, connection: Connection, request: att.ATT_Exchange_MTU_Request
|
||||
):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.2.1 Exchange MTU Request
|
||||
'''
|
||||
self.send_response(
|
||||
connection, ATT_Exchange_MTU_Response(server_rx_mtu=self.max_mtu)
|
||||
connection, att.ATT_Exchange_MTU_Response(server_rx_mtu=self.max_mtu)
|
||||
)
|
||||
|
||||
# Compute the final MTU
|
||||
if request.client_rx_mtu >= ATT_DEFAULT_MTU:
|
||||
if request.client_rx_mtu >= att.ATT_DEFAULT_MTU:
|
||||
mtu = min(self.max_mtu, request.client_rx_mtu)
|
||||
|
||||
# Notify the device
|
||||
@@ -594,11 +565,14 @@ class Server(utils.EventEmitter):
|
||||
else:
|
||||
logger.warning('invalid client_rx_mtu received, MTU not changed')
|
||||
|
||||
def on_att_find_information_request(self, connection, request):
|
||||
def on_att_find_information_request(
|
||||
self, connection: Connection, request: att.ATT_Find_Information_Request
|
||||
):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.3.1 Find Information Request
|
||||
'''
|
||||
|
||||
response: att.ATT_PDU
|
||||
# Check the request parameters
|
||||
if (
|
||||
request.starting_handle == 0
|
||||
@@ -606,17 +580,17 @@ class Server(utils.EventEmitter):
|
||||
):
|
||||
self.send_response(
|
||||
connection,
|
||||
ATT_Error_Response(
|
||||
att.ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.starting_handle,
|
||||
error_code=ATT_INVALID_HANDLE_ERROR,
|
||||
error_code=att.ATT_INVALID_HANDLE_ERROR,
|
||||
),
|
||||
)
|
||||
return
|
||||
|
||||
# Build list of returned attributes
|
||||
pdu_space_available = connection.att_mtu - 2
|
||||
attributes = []
|
||||
attributes: list[att.Attribute] = []
|
||||
uuid_size = 0
|
||||
for attribute in (
|
||||
attribute
|
||||
@@ -646,21 +620,23 @@ class Server(utils.EventEmitter):
|
||||
struct.pack('<H', attribute.handle) + attribute.type.to_pdu_bytes()
|
||||
for attribute in attributes
|
||||
]
|
||||
response = ATT_Find_Information_Response(
|
||||
response = att.ATT_Find_Information_Response(
|
||||
format=1 if len(attributes[0].type.to_pdu_bytes()) == 2 else 2,
|
||||
information_data=b''.join(information_data_list),
|
||||
)
|
||||
else:
|
||||
response = ATT_Error_Response(
|
||||
response = att.ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.starting_handle,
|
||||
error_code=ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
||||
error_code=att.ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
||||
)
|
||||
|
||||
self.send_response(connection, response)
|
||||
|
||||
@utils.AsyncRunner.run_in_task()
|
||||
async def on_att_find_by_type_value_request(self, connection, request):
|
||||
async def on_att_find_by_type_value_request(
|
||||
self, connection: Connection, request: att.ATT_Find_By_Type_Value_Request
|
||||
):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.3.3 Find By Type Value Request
|
||||
'''
|
||||
@@ -668,6 +644,7 @@ class Server(utils.EventEmitter):
|
||||
# Build list of returned attributes
|
||||
pdu_space_available = connection.att_mtu - 2
|
||||
attributes = []
|
||||
response: att.ATT_PDU
|
||||
async for attribute in (
|
||||
attribute
|
||||
for attribute in self.attributes
|
||||
@@ -700,33 +677,35 @@ class Server(utils.EventEmitter):
|
||||
handles_information_list.append(
|
||||
struct.pack('<HH', attribute.handle, group_end_handle)
|
||||
)
|
||||
response = ATT_Find_By_Type_Value_Response(
|
||||
response = att.ATT_Find_By_Type_Value_Response(
|
||||
handles_information_list=b''.join(handles_information_list)
|
||||
)
|
||||
else:
|
||||
response = ATT_Error_Response(
|
||||
response = att.ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.starting_handle,
|
||||
error_code=ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
||||
error_code=att.ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
||||
)
|
||||
|
||||
self.send_response(connection, response)
|
||||
|
||||
@utils.AsyncRunner.run_in_task()
|
||||
async def on_att_read_by_type_request(self, connection, request):
|
||||
async def on_att_read_by_type_request(
|
||||
self, connection: Connection, request: att.ATT_Read_By_Type_Request
|
||||
):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.4.1 Read By Type Request
|
||||
'''
|
||||
|
||||
pdu_space_available = connection.att_mtu - 2
|
||||
|
||||
response = ATT_Error_Response(
|
||||
response: att.ATT_PDU = att.ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.starting_handle,
|
||||
error_code=ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
||||
error_code=att.ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
||||
)
|
||||
|
||||
attributes = []
|
||||
attributes: list[tuple[int, bytes]] = []
|
||||
for attribute in (
|
||||
attribute
|
||||
for attribute in self.attributes
|
||||
@@ -737,11 +716,11 @@ class Server(utils.EventEmitter):
|
||||
):
|
||||
try:
|
||||
attribute_value = await attribute.read_value(connection)
|
||||
except ATT_Error as error:
|
||||
except att.ATT_Error as error:
|
||||
# If the first attribute is unreadable, return an error
|
||||
# Otherwise return attributes up to this point
|
||||
if not attributes:
|
||||
response = ATT_Error_Response(
|
||||
response = att.ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=attribute.handle,
|
||||
error_code=error.error_code,
|
||||
@@ -770,7 +749,7 @@ class Server(utils.EventEmitter):
|
||||
attribute_data_list = [
|
||||
struct.pack('<H', handle) + value for handle, value in attributes
|
||||
]
|
||||
response = ATT_Read_By_Type_Response(
|
||||
response = att.ATT_Read_By_Type_Response(
|
||||
length=entry_size, attribute_data_list=b''.join(attribute_data_list)
|
||||
)
|
||||
else:
|
||||
@@ -779,95 +758,104 @@ class Server(utils.EventEmitter):
|
||||
self.send_response(connection, response)
|
||||
|
||||
@utils.AsyncRunner.run_in_task()
|
||||
async def on_att_read_request(self, connection, request):
|
||||
async def on_att_read_request(
|
||||
self, connection: Connection, request: att.ATT_Read_Request
|
||||
):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.4.3 Read Request
|
||||
'''
|
||||
|
||||
response: att.ATT_PDU
|
||||
if attribute := self.get_attribute(request.attribute_handle):
|
||||
try:
|
||||
value = await attribute.read_value(connection)
|
||||
except ATT_Error as error:
|
||||
response = ATT_Error_Response(
|
||||
except att.ATT_Error as error:
|
||||
response = att.ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.attribute_handle,
|
||||
error_code=error.error_code,
|
||||
)
|
||||
else:
|
||||
value_size = min(connection.att_mtu - 1, len(value))
|
||||
response = ATT_Read_Response(attribute_value=value[:value_size])
|
||||
response = att.ATT_Read_Response(attribute_value=value[:value_size])
|
||||
else:
|
||||
response = ATT_Error_Response(
|
||||
response = att.ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.attribute_handle,
|
||||
error_code=ATT_INVALID_HANDLE_ERROR,
|
||||
error_code=att.ATT_INVALID_HANDLE_ERROR,
|
||||
)
|
||||
self.send_response(connection, response)
|
||||
|
||||
@utils.AsyncRunner.run_in_task()
|
||||
async def on_att_read_blob_request(self, connection, request):
|
||||
async def on_att_read_blob_request(
|
||||
self, connection: Connection, request: att.ATT_Read_Blob_Request
|
||||
):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.4.5 Read Blob Request
|
||||
'''
|
||||
|
||||
response: att.ATT_PDU
|
||||
if attribute := self.get_attribute(request.attribute_handle):
|
||||
try:
|
||||
value = await attribute.read_value(connection)
|
||||
except ATT_Error as error:
|
||||
response = ATT_Error_Response(
|
||||
except att.ATT_Error as error:
|
||||
response = att.ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.attribute_handle,
|
||||
error_code=error.error_code,
|
||||
)
|
||||
else:
|
||||
if request.value_offset > len(value):
|
||||
response = ATT_Error_Response(
|
||||
response = att.ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.attribute_handle,
|
||||
error_code=ATT_INVALID_OFFSET_ERROR,
|
||||
error_code=att.ATT_INVALID_OFFSET_ERROR,
|
||||
)
|
||||
elif len(value) <= connection.att_mtu - 1:
|
||||
response = ATT_Error_Response(
|
||||
response = att.ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.attribute_handle,
|
||||
error_code=ATT_ATTRIBUTE_NOT_LONG_ERROR,
|
||||
error_code=att.ATT_ATTRIBUTE_NOT_LONG_ERROR,
|
||||
)
|
||||
else:
|
||||
part_size = min(
|
||||
connection.att_mtu - 1, len(value) - request.value_offset
|
||||
)
|
||||
response = ATT_Read_Blob_Response(
|
||||
response = att.ATT_Read_Blob_Response(
|
||||
part_attribute_value=value[
|
||||
request.value_offset : request.value_offset + part_size
|
||||
]
|
||||
)
|
||||
else:
|
||||
response = ATT_Error_Response(
|
||||
response = att.ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.attribute_handle,
|
||||
error_code=ATT_INVALID_HANDLE_ERROR,
|
||||
error_code=att.ATT_INVALID_HANDLE_ERROR,
|
||||
)
|
||||
self.send_response(connection, response)
|
||||
|
||||
@utils.AsyncRunner.run_in_task()
|
||||
async def on_att_read_by_group_type_request(self, connection, request):
|
||||
async def on_att_read_by_group_type_request(
|
||||
self, connection: Connection, request: att.ATT_Read_By_Group_Type_Request
|
||||
):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.4.9 Read by Group Type Request
|
||||
'''
|
||||
response: att.ATT_PDU
|
||||
if request.attribute_group_type not in (
|
||||
GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
|
||||
GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE,
|
||||
):
|
||||
response = ATT_Error_Response(
|
||||
response = att.ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.starting_handle,
|
||||
error_code=ATT_UNSUPPORTED_GROUP_TYPE_ERROR,
|
||||
error_code=att.ATT_UNSUPPORTED_GROUP_TYPE_ERROR,
|
||||
)
|
||||
self.send_response(connection, response)
|
||||
return
|
||||
|
||||
pdu_space_available = connection.att_mtu - 2
|
||||
attributes = []
|
||||
attributes: list[tuple[int, int, bytes]] = []
|
||||
for attribute in (
|
||||
attribute
|
||||
for attribute in self.attributes
|
||||
@@ -904,21 +892,23 @@ class Server(utils.EventEmitter):
|
||||
struct.pack('<HH', handle, end_group_handle) + value
|
||||
for handle, end_group_handle, value in attributes
|
||||
]
|
||||
response = ATT_Read_By_Group_Type_Response(
|
||||
response = att.ATT_Read_By_Group_Type_Response(
|
||||
length=len(attribute_data_list[0]),
|
||||
attribute_data_list=b''.join(attribute_data_list),
|
||||
)
|
||||
else:
|
||||
response = ATT_Error_Response(
|
||||
response = att.ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.starting_handle,
|
||||
error_code=ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
||||
error_code=att.ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
||||
)
|
||||
|
||||
self.send_response(connection, response)
|
||||
|
||||
@utils.AsyncRunner.run_in_task()
|
||||
async def on_att_write_request(self, connection, request):
|
||||
async def on_att_write_request(
|
||||
self, connection: Connection, request: att.ATT_Write_Request
|
||||
):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.5.1 Write Request
|
||||
'''
|
||||
@@ -928,10 +918,10 @@ class Server(utils.EventEmitter):
|
||||
if attribute is None:
|
||||
self.send_response(
|
||||
connection,
|
||||
ATT_Error_Response(
|
||||
att.ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.attribute_handle,
|
||||
error_code=ATT_INVALID_HANDLE_ERROR,
|
||||
error_code=att.ATT_INVALID_HANDLE_ERROR,
|
||||
),
|
||||
)
|
||||
return
|
||||
@@ -942,30 +932,33 @@ class Server(utils.EventEmitter):
|
||||
if len(request.attribute_value) > GATT_MAX_ATTRIBUTE_VALUE_SIZE:
|
||||
self.send_response(
|
||||
connection,
|
||||
ATT_Error_Response(
|
||||
att.ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.attribute_handle,
|
||||
error_code=ATT_INVALID_ATTRIBUTE_LENGTH_ERROR,
|
||||
error_code=att.ATT_INVALID_ATTRIBUTE_LENGTH_ERROR,
|
||||
),
|
||||
)
|
||||
return
|
||||
|
||||
response: att.ATT_PDU
|
||||
try:
|
||||
# Accept the value
|
||||
await attribute.write_value(connection, request.attribute_value)
|
||||
except ATT_Error as error:
|
||||
response = ATT_Error_Response(
|
||||
except att.ATT_Error as error:
|
||||
response = att.ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.attribute_handle,
|
||||
error_code=error.error_code,
|
||||
)
|
||||
else:
|
||||
# Done
|
||||
response = ATT_Write_Response()
|
||||
response = att.ATT_Write_Response()
|
||||
self.send_response(connection, response)
|
||||
|
||||
@utils.AsyncRunner.run_in_task()
|
||||
async def on_att_write_command(self, connection, request):
|
||||
async def on_att_write_command(
|
||||
self, connection: Connection, request: att.ATT_Write_Command
|
||||
):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.5.3 Write Command
|
||||
'''
|
||||
@@ -984,18 +977,25 @@ class Server(utils.EventEmitter):
|
||||
# Accept the value
|
||||
try:
|
||||
await attribute.write_value(connection, request.attribute_value)
|
||||
except Exception as error:
|
||||
logger.exception(f'!!! ignoring exception: {error}')
|
||||
except Exception:
|
||||
logger.exception('!!! ignoring exception')
|
||||
|
||||
def on_att_handle_value_confirmation(self, connection, _confirmation):
|
||||
def on_att_handle_value_confirmation(
|
||||
self,
|
||||
connection: Connection,
|
||||
confirmation: att.ATT_Handle_Value_Confirmation,
|
||||
):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.7.3 Handle Value Confirmation
|
||||
'''
|
||||
if self.pending_confirmations[connection.handle] is None:
|
||||
del confirmation # Unused.
|
||||
if (
|
||||
pending_confirmation := self.pending_confirmations[connection.handle]
|
||||
) is None:
|
||||
# Not expected!
|
||||
logger.warning(
|
||||
'!!! unexpected confirmation, there is no pending indication'
|
||||
)
|
||||
return
|
||||
|
||||
self.pending_confirmations[connection.handle].set_result(None)
|
||||
pending_confirmation.set_result(None)
|
||||
|
||||
+2560
-2868
File diff suppressed because it is too large
Load Diff
+22
-30
@@ -17,44 +17,36 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, MutableMapping
|
||||
import datetime
|
||||
from typing import cast, Any, Optional
|
||||
import logging
|
||||
from collections.abc import Callable, MutableMapping
|
||||
from typing import Any, Optional, cast
|
||||
|
||||
from bumble import avc
|
||||
from bumble import avctp
|
||||
from bumble import avdtp
|
||||
from bumble import avrcp
|
||||
from bumble import crypto
|
||||
from bumble import rfcomm
|
||||
from bumble import sdp
|
||||
from bumble.colors import color
|
||||
from bumble import avc, avctp, avdtp, avrcp, crypto, rfcomm, sdp
|
||||
from bumble.att import ATT_CID, ATT_PDU
|
||||
from bumble.smp import SMP_CID, SMP_Command
|
||||
from bumble.colors import color
|
||||
from bumble.core import name_or_number
|
||||
from bumble.l2cap import (
|
||||
L2CAP_PDU,
|
||||
L2CAP_CONNECTION_REQUEST,
|
||||
L2CAP_CONNECTION_RESPONSE,
|
||||
L2CAP_SIGNALING_CID,
|
||||
L2CAP_LE_SIGNALING_CID,
|
||||
L2CAP_Control_Frame,
|
||||
L2CAP_Connection_Request,
|
||||
L2CAP_Connection_Response,
|
||||
)
|
||||
from bumble.hci import (
|
||||
Address,
|
||||
HCI_EVENT_PACKET,
|
||||
HCI_ACL_DATA_PACKET,
|
||||
HCI_DISCONNECTION_COMPLETE_EVENT,
|
||||
HCI_AclDataPacketAssembler,
|
||||
HCI_Packet,
|
||||
HCI_Event,
|
||||
HCI_EVENT_PACKET,
|
||||
Address,
|
||||
HCI_AclDataPacket,
|
||||
HCI_AclDataPacketAssembler,
|
||||
HCI_Disconnection_Complete_Event,
|
||||
HCI_Event,
|
||||
HCI_Packet,
|
||||
)
|
||||
|
||||
from bumble.l2cap import (
|
||||
L2CAP_LE_SIGNALING_CID,
|
||||
L2CAP_PDU,
|
||||
L2CAP_SIGNALING_CID,
|
||||
CommandCode,
|
||||
L2CAP_Connection_Request,
|
||||
L2CAP_Connection_Response,
|
||||
L2CAP_Control_Frame,
|
||||
)
|
||||
from bumble.smp import SMP_CID, SMP_Command
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -106,14 +98,14 @@ class PacketTracer:
|
||||
self.analyzer.emit(control_frame)
|
||||
|
||||
# Check if this signals a new channel
|
||||
if control_frame.code == L2CAP_CONNECTION_REQUEST:
|
||||
if control_frame.code == CommandCode.L2CAP_CONNECTION_REQUEST:
|
||||
connection_request = cast(L2CAP_Connection_Request, control_frame)
|
||||
self.psms[connection_request.source_cid] = connection_request.psm
|
||||
elif control_frame.code == L2CAP_CONNECTION_RESPONSE:
|
||||
elif control_frame.code == CommandCode.L2CAP_CONNECTION_RESPONSE:
|
||||
connection_response = cast(L2CAP_Connection_Response, control_frame)
|
||||
if (
|
||||
connection_response.result
|
||||
== L2CAP_Connection_Response.CONNECTION_SUCCESSFUL
|
||||
== L2CAP_Connection_Response.Result.CONNECTION_SUCCESSFUL
|
||||
):
|
||||
if self.peer and (
|
||||
psm := self.peer.psms.get(connection_response.source_cid)
|
||||
|
||||
+10
-21
@@ -17,45 +17,34 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import collections
|
||||
import collections.abc
|
||||
import logging
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import enum
|
||||
import traceback
|
||||
import logging
|
||||
import re
|
||||
from typing import (
|
||||
Union,
|
||||
Any,
|
||||
Optional,
|
||||
ClassVar,
|
||||
Iterable,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
import traceback
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, Iterable, Optional, Union
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from bumble import at
|
||||
from bumble import device
|
||||
from bumble import rfcomm
|
||||
from bumble import sdp
|
||||
from bumble import utils
|
||||
from bumble import at, device, rfcomm, sdp, utils
|
||||
from bumble.colors import color
|
||||
from bumble.core import (
|
||||
ProtocolError,
|
||||
BT_GENERIC_AUDIO_SERVICE,
|
||||
BT_HANDSFREE_SERVICE,
|
||||
BT_HANDSFREE_AUDIO_GATEWAY_SERVICE,
|
||||
BT_HANDSFREE_SERVICE,
|
||||
BT_L2CAP_PROTOCOL_ID,
|
||||
BT_RFCOMM_PROTOCOL_ID,
|
||||
ProtocolError,
|
||||
)
|
||||
from bumble.hci import (
|
||||
HCI_Enhanced_Setup_Synchronous_Connection_Command,
|
||||
CodingFormat,
|
||||
CodecID,
|
||||
CodingFormat,
|
||||
HCI_Enhanced_Setup_Synchronous_Connection_Command,
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
+23
-17
@@ -16,22 +16,20 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
import enum
|
||||
import struct
|
||||
|
||||
import enum
|
||||
import logging
|
||||
import struct
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional, Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable, Optional
|
||||
|
||||
from typing_extensions import override
|
||||
|
||||
from bumble import l2cap
|
||||
from bumble import device
|
||||
from bumble import utils
|
||||
from bumble import device, l2cap, utils
|
||||
from bumble.core import InvalidStateError, ProtocolError
|
||||
from bumble.hci import Address
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -219,33 +217,41 @@ class HID(ABC, utils.EventEmitter):
|
||||
self.role = role
|
||||
|
||||
# Register ourselves with the L2CAP channel manager
|
||||
device.register_l2cap_server(HID_CONTROL_PSM, self.on_l2cap_connection)
|
||||
device.register_l2cap_server(HID_INTERRUPT_PSM, self.on_l2cap_connection)
|
||||
device.create_l2cap_server(
|
||||
l2cap.ClassicChannelSpec(HID_CONTROL_PSM), self.on_l2cap_connection
|
||||
)
|
||||
device.create_l2cap_server(
|
||||
l2cap.ClassicChannelSpec(HID_INTERRUPT_PSM), self.on_l2cap_connection
|
||||
)
|
||||
|
||||
device.on(device.EVENT_CONNECTION, self.on_device_connection)
|
||||
|
||||
async def connect_control_channel(self) -> None:
|
||||
if not self.connection:
|
||||
raise InvalidStateError("Connection is not established!")
|
||||
# Create a new L2CAP connection - control channel
|
||||
try:
|
||||
channel = await self.device.l2cap_channel_manager.connect(
|
||||
self.connection, HID_CONTROL_PSM
|
||||
channel = await self.connection.create_l2cap_channel(
|
||||
l2cap.ClassicChannelSpec(HID_CONTROL_PSM)
|
||||
)
|
||||
channel.sink = self.on_ctrl_pdu
|
||||
self.l2cap_ctrl_channel = channel
|
||||
except ProtocolError:
|
||||
logging.exception(f'L2CAP connection failed.')
|
||||
logging.exception('L2CAP connection failed.')
|
||||
raise
|
||||
|
||||
async def connect_interrupt_channel(self) -> None:
|
||||
if not self.connection:
|
||||
raise InvalidStateError("Connection is not established!")
|
||||
# Create a new L2CAP connection - interrupt channel
|
||||
try:
|
||||
channel = await self.device.l2cap_channel_manager.connect(
|
||||
self.connection, HID_INTERRUPT_PSM
|
||||
channel = await self.connection.create_l2cap_channel(
|
||||
l2cap.ClassicChannelSpec(HID_CONTROL_PSM)
|
||||
)
|
||||
channel.sink = self.on_intr_pdu
|
||||
self.l2cap_intr_channel = channel
|
||||
except ProtocolError:
|
||||
logging.exception(f'L2CAP connection failed.')
|
||||
logging.exception('L2CAP connection failed.')
|
||||
raise
|
||||
|
||||
async def disconnect_interrupt_channel(self) -> None:
|
||||
|
||||
+102
-48
@@ -16,34 +16,19 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import collections
|
||||
import dataclasses
|
||||
import logging
|
||||
import struct
|
||||
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Optional, cast
|
||||
|
||||
from typing import (
|
||||
Any,
|
||||
Awaitable,
|
||||
Callable,
|
||||
Optional,
|
||||
cast,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
|
||||
from bumble import drivers, hci, utils
|
||||
from bumble.colors import color
|
||||
from bumble.core import ConnectionParameters, ConnectionPHY, PhysicalTransport
|
||||
from bumble.l2cap import L2CAP_PDU
|
||||
from bumble.snoop import Snooper
|
||||
from bumble import drivers
|
||||
from bumble import hci
|
||||
from bumble.core import (
|
||||
PhysicalTransport,
|
||||
PhysicalTransport,
|
||||
ConnectionPHY,
|
||||
ConnectionParameters,
|
||||
)
|
||||
from bumble import utils
|
||||
from bumble.transport.common import TransportLostError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -72,6 +57,11 @@ class DataPacketQueue(utils.EventEmitter):
|
||||
|
||||
max_packet_size: int
|
||||
|
||||
class PerConnectionState:
|
||||
def __init__(self) -> None:
|
||||
self.in_flight = 0
|
||||
self.drained = asyncio.Event()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
max_packet_size: int,
|
||||
@@ -82,9 +72,12 @@ class DataPacketQueue(utils.EventEmitter):
|
||||
self.max_packet_size = max_packet_size
|
||||
self.max_in_flight = max_in_flight
|
||||
self._in_flight = 0 # Total number of packets in flight across all connections
|
||||
self._in_flight_per_connection: dict[int, int] = collections.defaultdict(
|
||||
int
|
||||
) # Number of packets in flight per connection
|
||||
self._connection_state: dict[int, DataPacketQueue.PerConnectionState] = (
|
||||
collections.defaultdict(DataPacketQueue.PerConnectionState)
|
||||
)
|
||||
self._drained_per_connection: dict[int, asyncio.Event] = (
|
||||
collections.defaultdict(asyncio.Event)
|
||||
)
|
||||
self._send = send
|
||||
self._packets: collections.deque[tuple[hci.HCI_Packet, int]] = (
|
||||
collections.deque()
|
||||
@@ -136,36 +129,40 @@ class DataPacketQueue(utils.EventEmitter):
|
||||
self._completed += flushed_count
|
||||
self._packets = collections.deque(packets_to_keep)
|
||||
|
||||
if connection_handle in self._in_flight_per_connection:
|
||||
in_flight = self._in_flight_per_connection[connection_handle]
|
||||
if connection_state := self._connection_state.pop(connection_handle, None):
|
||||
in_flight = connection_state.in_flight
|
||||
self._completed += in_flight
|
||||
self._in_flight -= in_flight
|
||||
del self._in_flight_per_connection[connection_handle]
|
||||
connection_state.drained.set()
|
||||
|
||||
def _check_queue(self) -> None:
|
||||
while self._packets and self._in_flight < self.max_in_flight:
|
||||
packet, connection_handle = self._packets.pop()
|
||||
self._send(packet)
|
||||
self._in_flight += 1
|
||||
self._in_flight_per_connection[connection_handle] += 1
|
||||
connection_state = self._connection_state[connection_handle]
|
||||
connection_state.in_flight += 1
|
||||
connection_state.drained.clear()
|
||||
|
||||
def on_packets_completed(self, packet_count: int, connection_handle: int) -> None:
|
||||
"""Mark one or more packets associated with a connection as completed."""
|
||||
if connection_handle not in self._in_flight_per_connection:
|
||||
if connection_handle not in self._connection_state:
|
||||
logger.warning(
|
||||
f'received completion for unknown connection {connection_handle}'
|
||||
)
|
||||
return
|
||||
|
||||
in_flight_for_connection = self._in_flight_per_connection[connection_handle]
|
||||
if packet_count <= in_flight_for_connection:
|
||||
self._in_flight_per_connection[connection_handle] -= packet_count
|
||||
connection_state = self._connection_state[connection_handle]
|
||||
if packet_count <= connection_state.in_flight:
|
||||
connection_state.in_flight -= packet_count
|
||||
else:
|
||||
logger.warning(
|
||||
f'{packet_count} completed for {connection_handle} '
|
||||
f'but only {in_flight_for_connection} in flight'
|
||||
f'but only {connection_state.in_flight} in flight'
|
||||
)
|
||||
self._in_flight_per_connection[connection_handle] = 0
|
||||
connection_state.in_flight = 0
|
||||
if connection_state.in_flight == 0:
|
||||
connection_state.drained.set()
|
||||
|
||||
if packet_count <= self._in_flight:
|
||||
self._in_flight -= packet_count
|
||||
@@ -180,6 +177,13 @@ class DataPacketQueue(utils.EventEmitter):
|
||||
self._check_queue()
|
||||
self.emit('flow')
|
||||
|
||||
async def drain(self, connection_handle: int) -> None:
|
||||
"""Wait until there are no pending packets for a connection."""
|
||||
if not (connection_state := self._connection_state.get(connection_handle)):
|
||||
raise ValueError('no such connection')
|
||||
|
||||
await connection_state.drained.wait()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Connection:
|
||||
@@ -689,11 +693,9 @@ class Host(utils.EventEmitter):
|
||||
raise hci.HCI_Error(status)
|
||||
|
||||
return response
|
||||
except Exception as error:
|
||||
logger.exception(
|
||||
f'{color("!!! Exception while sending command:", "red")} {error}'
|
||||
)
|
||||
raise error
|
||||
except Exception:
|
||||
logger.exception(color("!!! Exception while sending command:", "red"))
|
||||
raise
|
||||
finally:
|
||||
self.pending_command = None
|
||||
self.pending_response = None
|
||||
@@ -835,8 +837,8 @@ class Host(utils.EventEmitter):
|
||||
def on_packet(self, packet: bytes) -> None:
|
||||
try:
|
||||
hci_packet = hci.HCI_Packet.from_bytes(packet)
|
||||
except Exception as error:
|
||||
logger.warning(f'!!! error parsing packet from bytes: {error}')
|
||||
except Exception:
|
||||
logger.exception('!!! error parsing packet from bytes')
|
||||
return
|
||||
|
||||
if self.ready or (
|
||||
@@ -1126,11 +1128,19 @@ class Host(utils.EventEmitter):
|
||||
else:
|
||||
self.emit('connection_phy_update_failure', connection.handle, event.status)
|
||||
|
||||
def on_hci_le_advertising_report_event(self, event):
|
||||
def on_hci_le_advertising_report_event(
|
||||
self,
|
||||
event: (
|
||||
hci.HCI_LE_Advertising_Report_Event
|
||||
| hci.HCI_LE_Extended_Advertising_Report_Event
|
||||
),
|
||||
):
|
||||
for report in event.reports:
|
||||
self.emit('advertising_report', report)
|
||||
|
||||
def on_hci_le_extended_advertising_report_event(self, event):
|
||||
def on_hci_le_extended_advertising_report_event(
|
||||
self, event: hci.HCI_LE_Extended_Advertising_Report_Event
|
||||
):
|
||||
self.on_hci_le_advertising_report_event(event)
|
||||
|
||||
def on_hci_le_advertising_set_terminated_event(self, event):
|
||||
@@ -1261,7 +1271,24 @@ class Host(utils.EventEmitter):
|
||||
self.cis_links[event.connection_handle] = IsoLink(
|
||||
handle=event.connection_handle, packet_queue=self.iso_packet_queue
|
||||
)
|
||||
self.emit('cis_establishment', event.connection_handle)
|
||||
self.emit(
|
||||
'cis_establishment',
|
||||
event.connection_handle,
|
||||
event.cig_sync_delay,
|
||||
event.cis_sync_delay,
|
||||
event.transport_latency_c_to_p,
|
||||
event.transport_latency_p_to_c,
|
||||
event.phy_c_to_p,
|
||||
event.phy_p_to_c,
|
||||
event.nse,
|
||||
event.bn_c_to_p,
|
||||
event.bn_p_to_c,
|
||||
event.ft_c_to_p,
|
||||
event.ft_p_to_c,
|
||||
event.max_pdu_c_to_p,
|
||||
event.max_pdu_p_to_c,
|
||||
event.iso_interval,
|
||||
)
|
||||
else:
|
||||
self.emit(
|
||||
'cis_establishment_failure', event.connection_handle, event.status
|
||||
@@ -1349,6 +1376,15 @@ class Host(utils.EventEmitter):
|
||||
def on_hci_synchronous_connection_changed_event(self, event):
|
||||
pass
|
||||
|
||||
def on_hci_mode_change_event(self, event: hci.HCI_Mode_Change_Event):
|
||||
self.emit(
|
||||
'mode_change',
|
||||
event.connection_handle,
|
||||
event.status,
|
||||
event.current_mode,
|
||||
event.interval,
|
||||
)
|
||||
|
||||
def on_hci_role_change_event(self, event):
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
logger.debug(
|
||||
@@ -1364,6 +1400,10 @@ class Host(utils.EventEmitter):
|
||||
self.emit('role_change_failure', event.bd_addr, event.status)
|
||||
|
||||
def on_hci_le_data_length_change_event(self, event):
|
||||
if (connection := self.connections.get(event.connection_handle)) is None:
|
||||
logger.warning('!!! DATA LENGTH CHANGE: unknown handle')
|
||||
return
|
||||
|
||||
self.emit(
|
||||
'connection_data_length_change',
|
||||
event.connection_handle,
|
||||
@@ -1384,7 +1424,7 @@ class Host(utils.EventEmitter):
|
||||
event.status,
|
||||
)
|
||||
|
||||
def on_hci_encryption_change_event(self, event):
|
||||
def on_hci_encryption_change_event(self, event: hci.HCI_Encryption_Change_Event):
|
||||
# Notify the client
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
self.emit(
|
||||
@@ -1398,7 +1438,9 @@ class Host(utils.EventEmitter):
|
||||
'connection_encryption_failure', event.connection_handle, event.status
|
||||
)
|
||||
|
||||
def on_hci_encryption_change_v2_event(self, event):
|
||||
def on_hci_encryption_change_v2_event(
|
||||
self, event: hci.HCI_Encryption_Change_V2_Event
|
||||
):
|
||||
# Notify the client
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
self.emit(
|
||||
@@ -1519,13 +1561,15 @@ class Host(utils.EventEmitter):
|
||||
self.emit('inquiry_complete')
|
||||
|
||||
def on_hci_inquiry_result_with_rssi_event(self, event):
|
||||
for response in event.responses:
|
||||
for bd_addr, class_of_device, rssi in zip(
|
||||
event.bd_addr, event.class_of_device, event.rssi
|
||||
):
|
||||
self.emit(
|
||||
'inquiry_result',
|
||||
response.bd_addr,
|
||||
response.class_of_device,
|
||||
bd_addr,
|
||||
class_of_device,
|
||||
b'',
|
||||
response.rssi,
|
||||
rssi,
|
||||
)
|
||||
|
||||
def on_hci_extended_inquiry_result_event(self, event):
|
||||
@@ -1585,5 +1629,15 @@ class Host(utils.EventEmitter):
|
||||
def on_hci_le_cs_subevent_result_continue_event(self, event):
|
||||
self.emit('cs_subevent_result_continue', event)
|
||||
|
||||
def on_hci_le_subrate_change_event(self, event: hci.HCI_LE_Subrate_Change_Event):
|
||||
self.emit(
|
||||
'le_subrate_change',
|
||||
event.connection_handle,
|
||||
event.subrate_factor,
|
||||
event.peripheral_latency,
|
||||
event.continuation_number,
|
||||
event.supervision_timeout,
|
||||
)
|
||||
|
||||
def on_hci_vendor_event(self, event):
|
||||
self.emit('vendor_event', event)
|
||||
|
||||
+5
-3
@@ -21,16 +21,18 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import json
|
||||
from typing import TYPE_CHECKING, Optional, Any
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from bumble.colors import color
|
||||
from bumble import hci
|
||||
from bumble.colors import color
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bumble.device import Device
|
||||
|
||||
+415
-458
File diff suppressed because it is too large
Load Diff
+26
-291
@@ -12,32 +12,25 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import asyncio
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import logging
|
||||
import asyncio
|
||||
from functools import partial
|
||||
|
||||
from bumble.core import (
|
||||
PhysicalTransport,
|
||||
InvalidStateError,
|
||||
)
|
||||
from bumble.colors import color
|
||||
from bumble.hci import (
|
||||
Address,
|
||||
Role,
|
||||
HCI_SUCCESS,
|
||||
HCI_CONNECTION_ACCEPT_TIMEOUT_ERROR,
|
||||
HCI_CONNECTION_TIMEOUT_ERROR,
|
||||
HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR,
|
||||
HCI_PAGE_TIMEOUT_ERROR,
|
||||
HCI_Connection_Complete_Event,
|
||||
)
|
||||
from bumble import controller
|
||||
|
||||
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
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -115,10 +108,10 @@ class LocalLink:
|
||||
|
||||
def send_acl_data(self, sender_controller, destination_address, transport, data):
|
||||
# Send the data to the first controller with a matching address
|
||||
if transport == PhysicalTransport.LE:
|
||||
if transport == core.PhysicalTransport.LE:
|
||||
destination_controller = self.find_controller(destination_address)
|
||||
source_address = sender_controller.random_address
|
||||
elif transport == PhysicalTransport.BR_EDR:
|
||||
elif transport == core.PhysicalTransport.BR_EDR:
|
||||
destination_controller = self.find_classic_controller(destination_address)
|
||||
source_address = sender_controller.public_address
|
||||
else:
|
||||
@@ -165,29 +158,29 @@ class LocalLink:
|
||||
asyncio.get_running_loop().call_soon(self.on_connection_complete)
|
||||
|
||||
def on_disconnection_complete(
|
||||
self, central_address, peripheral_address, disconnect_command
|
||||
self, initiating_address, target_address, disconnect_command
|
||||
):
|
||||
# Find the controller that initiated the disconnection
|
||||
if not (central_controller := self.find_controller(central_address)):
|
||||
if not (initiating_controller := self.find_controller(initiating_address)):
|
||||
logger.warning('!!! Initiating controller not found')
|
||||
return
|
||||
|
||||
# Disconnect from the first controller with a matching address
|
||||
if peripheral_controller := self.find_controller(peripheral_address):
|
||||
peripheral_controller.on_link_central_disconnected(
|
||||
central_address, disconnect_command.reason
|
||||
if target_controller := self.find_controller(target_address):
|
||||
target_controller.on_link_disconnected(
|
||||
initiating_address, disconnect_command.reason
|
||||
)
|
||||
|
||||
central_controller.on_link_peripheral_disconnection_complete(
|
||||
initiating_controller.on_link_disconnection_complete(
|
||||
disconnect_command, HCI_SUCCESS
|
||||
)
|
||||
|
||||
def disconnect(self, central_address, peripheral_address, disconnect_command):
|
||||
def disconnect(self, initiating_address, target_address, disconnect_command):
|
||||
logger.debug(
|
||||
f'$$$ DISCONNECTION {central_address} -> '
|
||||
f'{peripheral_address}: reason = {disconnect_command.reason}'
|
||||
f'$$$ DISCONNECTION {initiating_address} -> '
|
||||
f'{target_address}: reason = {disconnect_command.reason}'
|
||||
)
|
||||
args = [central_address, peripheral_address, disconnect_command]
|
||||
args = [initiating_address, target_address, disconnect_command]
|
||||
asyncio.get_running_loop().call_soon(self.on_disconnection_complete, *args)
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
@@ -274,7 +267,7 @@ class LocalLink:
|
||||
|
||||
responder_controller.on_classic_connection_request(
|
||||
initiator_controller.public_address,
|
||||
HCI_Connection_Complete_Event.ACL_LINK_TYPE,
|
||||
HCI_Connection_Complete_Event.LinkType.ACL,
|
||||
)
|
||||
|
||||
def classic_accept_connection(
|
||||
@@ -384,261 +377,3 @@ class LocalLink:
|
||||
responder_controller.on_classic_sco_connection_complete(
|
||||
initiator_controller.public_address, HCI_SUCCESS, link_type
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class RemoteLink:
|
||||
'''
|
||||
A Link implementation that communicates with other virtual controllers via a
|
||||
WebSocket relay
|
||||
'''
|
||||
|
||||
def __init__(self, uri):
|
||||
self.controller = None
|
||||
self.uri = uri
|
||||
self.execution_queue = asyncio.Queue()
|
||||
self.websocket = asyncio.get_running_loop().create_future()
|
||||
self.rpc_result = None
|
||||
self.pending_connection = None
|
||||
self.central_connections = set() # List of addresses that we have connected to
|
||||
self.peripheral_connections = (
|
||||
set()
|
||||
) # List of addresses that have connected to us
|
||||
|
||||
# Connect and run asynchronously
|
||||
asyncio.create_task(self.run_connection())
|
||||
asyncio.create_task(self.run_executor_loop())
|
||||
|
||||
def add_controller(self, controller):
|
||||
if self.controller:
|
||||
raise InvalidStateError('controller already set')
|
||||
self.controller = controller
|
||||
|
||||
def remove_controller(self, controller):
|
||||
if self.controller != controller:
|
||||
raise InvalidStateError('controller mismatch')
|
||||
self.controller = None
|
||||
|
||||
def get_pending_connection(self):
|
||||
return self.pending_connection
|
||||
|
||||
def get_pending_classic_connection(self):
|
||||
return self.pending_classic_connection
|
||||
|
||||
async def wait_until_connected(self):
|
||||
await self.websocket
|
||||
|
||||
def execute(self, async_function):
|
||||
self.execution_queue.put_nowait(async_function())
|
||||
|
||||
async def run_executor_loop(self):
|
||||
logger.debug('executor loop starting')
|
||||
while True:
|
||||
item = await self.execution_queue.get()
|
||||
try:
|
||||
await item
|
||||
except Exception as error:
|
||||
logger.warning(
|
||||
f'{color("!!! Exception in async handler:", "red")} {error}'
|
||||
)
|
||||
|
||||
async def run_connection(self):
|
||||
import websockets # lazy import
|
||||
|
||||
# Connect to the relay
|
||||
logger.debug(f'connecting to {self.uri}')
|
||||
# pylint: disable-next=no-member
|
||||
websocket = await websockets.connect(self.uri)
|
||||
self.websocket.set_result(websocket)
|
||||
logger.debug(f'connected to {self.uri}')
|
||||
|
||||
while True:
|
||||
message = await websocket.recv()
|
||||
logger.debug(f'received message: {message}')
|
||||
keyword, *payload = message.split(':', 1)
|
||||
|
||||
handler_name = f'on_{keyword}_received'
|
||||
handler = getattr(self, handler_name, None)
|
||||
if handler:
|
||||
await handler(payload[0] if payload else None)
|
||||
|
||||
def close(self):
|
||||
if self.websocket.done():
|
||||
logger.debug('closing websocket')
|
||||
websocket = self.websocket.result()
|
||||
asyncio.create_task(websocket.close())
|
||||
|
||||
async def on_result_received(self, result):
|
||||
if self.rpc_result:
|
||||
self.rpc_result.set_result(result)
|
||||
|
||||
async def on_left_received(self, address):
|
||||
if address in self.central_connections:
|
||||
self.controller.on_link_peripheral_disconnected(Address(address))
|
||||
self.central_connections.remove(address)
|
||||
|
||||
if address in self.peripheral_connections:
|
||||
self.controller.on_link_central_disconnected(
|
||||
address, HCI_CONNECTION_TIMEOUT_ERROR
|
||||
)
|
||||
self.peripheral_connections.remove(address)
|
||||
|
||||
async def on_unreachable_received(self, target):
|
||||
await self.on_left_received(target)
|
||||
|
||||
async def on_message_received(self, message):
|
||||
sender, *payload = message.split('/', 1)
|
||||
if payload:
|
||||
keyword, *payload = payload[0].split(':', 1)
|
||||
handler_name = f'on_{keyword}_message_received'
|
||||
handler = getattr(self, handler_name, None)
|
||||
if handler:
|
||||
await handler(sender, payload[0] if payload else None)
|
||||
|
||||
async def on_advertisement_message_received(self, sender, advertisement):
|
||||
try:
|
||||
self.controller.on_link_advertising_data(
|
||||
Address(sender), bytes.fromhex(advertisement)
|
||||
)
|
||||
except Exception:
|
||||
logger.exception('exception')
|
||||
|
||||
async def on_acl_message_received(self, sender, acl_data):
|
||||
try:
|
||||
self.controller.on_link_acl_data(Address(sender), bytes.fromhex(acl_data))
|
||||
except Exception:
|
||||
logger.exception('exception')
|
||||
|
||||
async def on_connect_message_received(self, sender, _):
|
||||
# Remember the connection
|
||||
self.peripheral_connections.add(sender)
|
||||
|
||||
# Notify the controller
|
||||
logger.debug(f'connection from central {sender}')
|
||||
self.controller.on_link_central_connected(Address(sender))
|
||||
|
||||
# Accept the connection by responding to it
|
||||
await self.send_targeted_message(sender, 'connected')
|
||||
|
||||
async def on_connected_message_received(self, sender, _):
|
||||
if not self.pending_connection:
|
||||
logger.warning('received a connection ack, but no connection is pending')
|
||||
return
|
||||
|
||||
# Remember the connection
|
||||
self.central_connections.add(sender)
|
||||
|
||||
# Notify the controller
|
||||
logger.debug(f'connected to peripheral {self.pending_connection.peer_address}')
|
||||
self.controller.on_link_peripheral_connection_complete(
|
||||
self.pending_connection, HCI_SUCCESS
|
||||
)
|
||||
|
||||
async def on_disconnect_message_received(self, sender, message):
|
||||
# Notify the controller
|
||||
params = parse_parameters(message)
|
||||
reason = int(params.get('reason', str(HCI_CONNECTION_TIMEOUT_ERROR)))
|
||||
self.controller.on_link_central_disconnected(Address(sender), reason)
|
||||
|
||||
# Forget the connection
|
||||
if sender in self.peripheral_connections:
|
||||
self.peripheral_connections.remove(sender)
|
||||
|
||||
async def on_encrypted_message_received(self, sender, _):
|
||||
# TODO parse params to get real args
|
||||
self.controller.on_link_encrypted(Address(sender), bytes(8), 0, bytes(16))
|
||||
|
||||
async def send_rpc_command(self, command):
|
||||
# Ensure we have a connection
|
||||
websocket = await self.websocket
|
||||
|
||||
# Create a future value to hold the eventual result
|
||||
assert self.rpc_result is None
|
||||
self.rpc_result = asyncio.get_running_loop().create_future()
|
||||
|
||||
# Send the command
|
||||
await websocket.send(command)
|
||||
|
||||
# Wait for the result
|
||||
rpc_result = await self.rpc_result
|
||||
self.rpc_result = None
|
||||
logger.debug(f'rpc_result: {rpc_result}')
|
||||
|
||||
# TODO: parse the result
|
||||
|
||||
async def send_targeted_message(self, target, message):
|
||||
# Ensure we have a connection
|
||||
websocket = await self.websocket
|
||||
|
||||
# Send the message
|
||||
await websocket.send(f'@{target} {message}')
|
||||
|
||||
async def notify_address_changed(self):
|
||||
await self.send_rpc_command(f'/set-address {self.controller.random_address}')
|
||||
|
||||
def on_address_changed(self, controller):
|
||||
logger.info(f'address changed for {controller}: {controller.random_address}')
|
||||
|
||||
# Notify the relay of the change
|
||||
self.execute(self.notify_address_changed)
|
||||
|
||||
async def send_advertising_data_to_relay(self, data):
|
||||
await self.send_targeted_message('*', f'advertisement:{data.hex()}')
|
||||
|
||||
def send_advertising_data(self, _, data):
|
||||
self.execute(partial(self.send_advertising_data_to_relay, data))
|
||||
|
||||
async def send_acl_data_to_relay(self, peer_address, data):
|
||||
await self.send_targeted_message(peer_address, f'acl:{data.hex()}')
|
||||
|
||||
def send_acl_data(self, _, peer_address, _transport, data):
|
||||
# TODO: handle different transport
|
||||
self.execute(partial(self.send_acl_data_to_relay, peer_address, data))
|
||||
|
||||
async def send_connection_request_to_relay(self, peer_address):
|
||||
await self.send_targeted_message(peer_address, 'connect')
|
||||
|
||||
def connect(self, _, le_create_connection_command):
|
||||
if self.pending_connection:
|
||||
logger.warning('connection already pending')
|
||||
return
|
||||
self.pending_connection = le_create_connection_command
|
||||
self.execute(
|
||||
partial(
|
||||
self.send_connection_request_to_relay,
|
||||
str(le_create_connection_command.peer_address),
|
||||
)
|
||||
)
|
||||
|
||||
def on_disconnection_complete(self, disconnect_command):
|
||||
self.controller.on_link_peripheral_disconnection_complete(
|
||||
disconnect_command, HCI_SUCCESS
|
||||
)
|
||||
|
||||
def disconnect(self, central_address, peripheral_address, disconnect_command):
|
||||
logger.debug(
|
||||
f'disconnect {central_address} -> '
|
||||
f'{peripheral_address}: reason = {disconnect_command.reason}'
|
||||
)
|
||||
self.execute(
|
||||
partial(
|
||||
self.send_targeted_message,
|
||||
peripheral_address,
|
||||
f'disconnect:reason={disconnect_command.reason}',
|
||||
)
|
||||
)
|
||||
asyncio.get_running_loop().call_soon(
|
||||
self.on_disconnection_complete, disconnect_command
|
||||
)
|
||||
|
||||
def on_connection_encrypted(self, _, peripheral_address, rand, ediv, ltk):
|
||||
asyncio.get_running_loop().call_soon(
|
||||
self.controller.on_link_encrypted, peripheral_address, rand, ediv, ltk
|
||||
)
|
||||
self.execute(
|
||||
partial(
|
||||
self.send_targeted_message,
|
||||
peripheral_address,
|
||||
f'encrypted:ltk={ltk.hex()}',
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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],
|
||||
)
|
||||
+18
-23
@@ -16,33 +16,28 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
from dataclasses import dataclass
|
||||
import secrets
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from bumble.hci import (
|
||||
Address,
|
||||
HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
|
||||
HCI_DISPLAY_ONLY_IO_CAPABILITY,
|
||||
HCI_DISPLAY_YES_NO_IO_CAPABILITY,
|
||||
HCI_KEYBOARD_ONLY_IO_CAPABILITY,
|
||||
)
|
||||
from bumble import hci
|
||||
from bumble.core import AdvertisingData, LeRole
|
||||
from bumble.smp import (
|
||||
SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
|
||||
SMP_KEYBOARD_ONLY_IO_CAPABILITY,
|
||||
SMP_DISPLAY_ONLY_IO_CAPABILITY,
|
||||
SMP_DISPLAY_YES_NO_IO_CAPABILITY,
|
||||
SMP_KEYBOARD_DISPLAY_IO_CAPABILITY,
|
||||
SMP_ENC_KEY_DISTRIBUTION_FLAG,
|
||||
SMP_ID_KEY_DISTRIBUTION_FLAG,
|
||||
SMP_SIGN_KEY_DISTRIBUTION_FLAG,
|
||||
SMP_KEYBOARD_DISPLAY_IO_CAPABILITY,
|
||||
SMP_KEYBOARD_ONLY_IO_CAPABILITY,
|
||||
SMP_LINK_KEY_DISTRIBUTION_FLAG,
|
||||
SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
|
||||
SMP_SIGN_KEY_DISTRIBUTION_FLAG,
|
||||
OobContext,
|
||||
OobLegacyContext,
|
||||
OobSharedData,
|
||||
)
|
||||
from bumble.core import AdvertisingData, LeRole
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -50,7 +45,7 @@ from bumble.core import AdvertisingData, LeRole
|
||||
class OobData:
|
||||
"""OOB data that can be sent from one device to another."""
|
||||
|
||||
address: Optional[Address] = None
|
||||
address: Optional[hci.Address] = None
|
||||
role: Optional[LeRole] = None
|
||||
shared_data: Optional[OobSharedData] = None
|
||||
legacy_context: Optional[OobLegacyContext] = None
|
||||
@@ -62,7 +57,7 @@ class OobData:
|
||||
shared_data_r: Optional[bytes] = None
|
||||
for ad_type, ad_data in ad.ad_structures:
|
||||
if ad_type == AdvertisingData.LE_BLUETOOTH_DEVICE_ADDRESS:
|
||||
instance.address = Address(ad_data)
|
||||
instance.address = hci.Address(ad_data)
|
||||
elif ad_type == AdvertisingData.LE_ROLE:
|
||||
instance.role = LeRole(ad_data[0])
|
||||
elif ad_type == AdvertisingData.LE_SECURE_CONNECTIONS_CONFIRMATION_VALUE:
|
||||
@@ -130,11 +125,11 @@ class PairingDelegate:
|
||||
# Default mapping from abstract to Classic I/O capabilities.
|
||||
# Subclasses may override this if they prefer a different mapping.
|
||||
CLASSIC_IO_CAPABILITIES_MAP = {
|
||||
NO_OUTPUT_NO_INPUT: HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
|
||||
KEYBOARD_INPUT_ONLY: HCI_KEYBOARD_ONLY_IO_CAPABILITY,
|
||||
DISPLAY_OUTPUT_ONLY: HCI_DISPLAY_ONLY_IO_CAPABILITY,
|
||||
DISPLAY_OUTPUT_AND_YES_NO_INPUT: HCI_DISPLAY_YES_NO_IO_CAPABILITY,
|
||||
DISPLAY_OUTPUT_AND_KEYBOARD_INPUT: HCI_DISPLAY_YES_NO_IO_CAPABILITY,
|
||||
NO_OUTPUT_NO_INPUT: hci.IoCapability.NO_INPUT_NO_OUTPUT,
|
||||
KEYBOARD_INPUT_ONLY: hci.IoCapability.KEYBOARD_ONLY,
|
||||
DISPLAY_OUTPUT_ONLY: hci.IoCapability.DISPLAY_ONLY,
|
||||
DISPLAY_OUTPUT_AND_YES_NO_INPUT: hci.IoCapability.DISPLAY_YES_NO,
|
||||
DISPLAY_OUTPUT_AND_KEYBOARD_INPUT: hci.IoCapability.DISPLAY_YES_NO,
|
||||
}
|
||||
|
||||
io_capability: IoCapability
|
||||
@@ -160,7 +155,7 @@ class PairingDelegate:
|
||||
|
||||
# pylint: disable=line-too-long
|
||||
return self.CLASSIC_IO_CAPABILITIES_MAP.get(
|
||||
self.io_capability, HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY
|
||||
self.io_capability, hci.IoCapability.NO_INPUT_NO_OUTPUT
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -237,8 +232,8 @@ class PairingConfig:
|
||||
"""Configuration for the Pairing protocol."""
|
||||
|
||||
class AddressType(enum.IntEnum):
|
||||
PUBLIC = Address.PUBLIC_DEVICE_ADDRESS
|
||||
RANDOM = Address.RANDOM_DEVICE_ADDRESS
|
||||
PUBLIC = hci.Address.PUBLIC_DEVICE_ADDRESS
|
||||
RANDOM = hci.Address.RANDOM_DEVICE_ADDRESS
|
||||
|
||||
@dataclass
|
||||
class OobConfig:
|
||||
|
||||
@@ -19,21 +19,22 @@ This module implement the Pandora Bluetooth test APIs for the Bumble stack.
|
||||
|
||||
__version__ = "0.0.1"
|
||||
|
||||
from typing import Callable, List, Optional
|
||||
|
||||
import grpc
|
||||
import grpc.aio
|
||||
|
||||
from bumble.pandora.config import Config
|
||||
from bumble.pandora.device import PandoraDevice
|
||||
from bumble.pandora.host import HostService
|
||||
from bumble.pandora.l2cap import L2CAPService
|
||||
from bumble.pandora.security import SecurityService, SecurityStorageService
|
||||
from pandora.host_grpc_aio import add_HostServicer_to_server
|
||||
from pandora.l2cap_grpc_aio import add_L2CAPServicer_to_server
|
||||
from pandora.security_grpc_aio import (
|
||||
add_SecurityServicer_to_server,
|
||||
add_SecurityStorageServicer_to_server,
|
||||
)
|
||||
from typing import Callable, List, Optional
|
||||
|
||||
from bumble.pandora.config import Config
|
||||
from bumble.pandora.device import PandoraDevice
|
||||
from bumble.pandora.host import HostService
|
||||
from bumble.pandora.l2cap import L2CAPService
|
||||
from bumble.pandora.security import SecurityService, SecurityStorageService
|
||||
|
||||
# public symbols
|
||||
__all__ = [
|
||||
@@ -49,7 +50,7 @@ _SERVICERS_HOOKS: list[Callable[[PandoraDevice, Config, grpc.aio.Server], None]]
|
||||
|
||||
|
||||
def register_servicer_hook(
|
||||
hook: Callable[[PandoraDevice, Config, grpc.aio.Server], None]
|
||||
hook: Callable[[PandoraDevice, Config, grpc.aio.Server], None],
|
||||
) -> None:
|
||||
_SERVICERS_HOOKS.append(hook)
|
||||
|
||||
|
||||
@@ -13,10 +13,12 @@
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
from bumble.pairing import PairingConfig, PairingDelegate
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from bumble.pairing import PairingConfig, PairingDelegate
|
||||
|
||||
|
||||
@dataclass
|
||||
class Config:
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
"""Generic & dependency free Bumble (reference) device."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Optional
|
||||
|
||||
from bumble import transport
|
||||
from bumble.core import (
|
||||
BT_GENERIC_AUDIO_SERVICE,
|
||||
@@ -32,8 +35,6 @@ from bumble.sdp import (
|
||||
DataElement,
|
||||
ServiceAttribute,
|
||||
)
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
# Default rootcanal HCI TCP address
|
||||
ROOTCANAL_HCI_ADDRESS = "localhost:6402"
|
||||
|
||||
+38
-36
@@ -13,51 +13,23 @@
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import bumble.device
|
||||
import grpc
|
||||
import grpc.aio
|
||||
import logging
|
||||
import struct
|
||||
from typing import AsyncGenerator, Optional, cast
|
||||
|
||||
import bumble.utils
|
||||
from bumble.pandora import utils
|
||||
from bumble.pandora.config import Config
|
||||
from bumble.core import (
|
||||
PhysicalTransport,
|
||||
UUID,
|
||||
AdvertisingData,
|
||||
Appearance,
|
||||
ConnectionError,
|
||||
)
|
||||
from bumble.device import (
|
||||
DEVICE_DEFAULT_SCAN_INTERVAL,
|
||||
DEVICE_DEFAULT_SCAN_WINDOW,
|
||||
Advertisement,
|
||||
AdvertisingParameters,
|
||||
AdvertisingEventProperties,
|
||||
AdvertisingType,
|
||||
Device,
|
||||
)
|
||||
from bumble.gatt import Service
|
||||
from bumble.hci import (
|
||||
HCI_CONNECTION_ALREADY_EXISTS_ERROR,
|
||||
HCI_PAGE_TIMEOUT_ERROR,
|
||||
HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR,
|
||||
Address,
|
||||
Phy,
|
||||
Role,
|
||||
OwnAddressType,
|
||||
)
|
||||
import grpc
|
||||
import grpc.aio
|
||||
from google.protobuf import any_pb2 # pytype: disable=pyi-error
|
||||
from google.protobuf import empty_pb2 # pytype: disable=pyi-error
|
||||
from pandora.host_grpc_aio import HostServicer
|
||||
from pandora import host_pb2
|
||||
from pandora.host_grpc_aio import HostServicer
|
||||
from pandora.host_pb2 import (
|
||||
DISCOVERABLE_GENERAL,
|
||||
DISCOVERABLE_LIMITED,
|
||||
NOT_CONNECTABLE,
|
||||
NOT_DISCOVERABLE,
|
||||
DISCOVERABLE_LIMITED,
|
||||
DISCOVERABLE_GENERAL,
|
||||
PRIMARY_1M,
|
||||
PRIMARY_CODED,
|
||||
SECONDARY_1M,
|
||||
@@ -85,7 +57,37 @@ from pandora.host_pb2 import (
|
||||
WaitConnectionResponse,
|
||||
WaitDisconnectionRequest,
|
||||
)
|
||||
from typing import AsyncGenerator, Optional, cast
|
||||
|
||||
import bumble.device
|
||||
import bumble.utils
|
||||
from bumble.core import (
|
||||
UUID,
|
||||
AdvertisingData,
|
||||
Appearance,
|
||||
ConnectionError,
|
||||
PhysicalTransport,
|
||||
)
|
||||
from bumble.device import (
|
||||
DEVICE_DEFAULT_SCAN_INTERVAL,
|
||||
DEVICE_DEFAULT_SCAN_WINDOW,
|
||||
Advertisement,
|
||||
AdvertisingEventProperties,
|
||||
AdvertisingParameters,
|
||||
AdvertisingType,
|
||||
Device,
|
||||
)
|
||||
from bumble.gatt import Service
|
||||
from bumble.hci import (
|
||||
HCI_CONNECTION_ALREADY_EXISTS_ERROR,
|
||||
HCI_PAGE_TIMEOUT_ERROR,
|
||||
HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR,
|
||||
Address,
|
||||
OwnAddressType,
|
||||
Phy,
|
||||
Role,
|
||||
)
|
||||
from bumble.pandora import utils
|
||||
from bumble.pandora.config import Config
|
||||
|
||||
PRIMARY_PHY_MAP: dict[int, PrimaryPhy] = {
|
||||
# Default value reported by Bumble for legacy Advertising reports.
|
||||
|
||||
+22
-21
@@ -12,31 +12,21 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import grpc
|
||||
import json
|
||||
import logging
|
||||
from asyncio import Future
|
||||
from asyncio import Queue as AsyncQueue
|
||||
from dataclasses import dataclass
|
||||
from typing import AsyncGenerator, Optional, Union
|
||||
|
||||
from asyncio import Queue as AsyncQueue, Future
|
||||
|
||||
from bumble.pandora import utils
|
||||
from bumble.pandora.config import Config
|
||||
from bumble.core import OutOfResourcesError, InvalidArgumentError
|
||||
from bumble.device import Device
|
||||
from bumble.l2cap import (
|
||||
ClassicChannel,
|
||||
ClassicChannelServer,
|
||||
ClassicChannelSpec,
|
||||
LeCreditBasedChannel,
|
||||
LeCreditBasedChannelServer,
|
||||
LeCreditBasedChannelSpec,
|
||||
)
|
||||
import grpc
|
||||
from google.protobuf import any_pb2, empty_pb2 # pytype: disable=pyi-error
|
||||
from pandora.l2cap_grpc_aio import L2CAPServicer # pytype: disable=pyi-error
|
||||
from pandora.l2cap_pb2 import ( # pytype: disable=pyi-error
|
||||
COMMAND_NOT_UNDERSTOOD,
|
||||
INVALID_CID_IN_REQUEST,
|
||||
Channel as PandoraChannel,
|
||||
from pandora.l2cap_pb2 import COMMAND_NOT_UNDERSTOOD, INVALID_CID_IN_REQUEST
|
||||
from pandora.l2cap_pb2 import Channel as PandoraChannel # pytype: disable=pyi-error
|
||||
from pandora.l2cap_pb2 import (
|
||||
ConnectRequest,
|
||||
ConnectResponse,
|
||||
CreditBasedChannelRequest,
|
||||
@@ -51,8 +41,19 @@ from pandora.l2cap_pb2 import ( # pytype: disable=pyi-error
|
||||
WaitDisconnectionRequest,
|
||||
WaitDisconnectionResponse,
|
||||
)
|
||||
from typing import AsyncGenerator, Optional, Union
|
||||
from dataclasses import dataclass
|
||||
|
||||
from bumble.core import InvalidArgumentError, OutOfResourcesError
|
||||
from bumble.device import Device
|
||||
from bumble.l2cap import (
|
||||
ClassicChannel,
|
||||
ClassicChannelServer,
|
||||
ClassicChannelSpec,
|
||||
LeCreditBasedChannel,
|
||||
LeCreditBasedChannelServer,
|
||||
LeCreditBasedChannelSpec,
|
||||
)
|
||||
from bumble.pandora import utils
|
||||
from bumble.pandora.config import Config
|
||||
|
||||
L2capChannel = Union[ClassicChannel, LeCreditBasedChannel]
|
||||
|
||||
|
||||
+19
-19
@@ -13,24 +13,14 @@
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
from collections.abc import Awaitable
|
||||
import grpc
|
||||
import logging
|
||||
from collections.abc import Awaitable
|
||||
from typing import Any, AsyncGenerator, AsyncIterator, Callable, Optional, Union
|
||||
|
||||
from bumble.pandora import utils
|
||||
from bumble.pandora.config import Config
|
||||
from bumble import hci
|
||||
from bumble.core import (
|
||||
PhysicalTransport,
|
||||
ProtocolError,
|
||||
InvalidArgumentError,
|
||||
)
|
||||
import bumble.utils
|
||||
from bumble.device import Connection as BumbleConnection, Device
|
||||
from bumble.hci import HCI_Error, Role
|
||||
from bumble.pairing import PairingConfig, PairingDelegate as BasePairingDelegate
|
||||
import grpc
|
||||
from google.protobuf import any_pb2 # pytype: disable=pyi-error
|
||||
from google.protobuf import empty_pb2 # pytype: disable=pyi-error
|
||||
from google.protobuf import wrappers_pb2 # pytype: disable=pyi-error
|
||||
@@ -57,7 +47,17 @@ from pandora.security_pb2 import (
|
||||
WaitSecurityRequest,
|
||||
WaitSecurityResponse,
|
||||
)
|
||||
from typing import Any, AsyncGenerator, AsyncIterator, Callable, Optional, Union
|
||||
|
||||
import bumble.utils
|
||||
from bumble import hci
|
||||
from bumble.core import InvalidArgumentError, PhysicalTransport, ProtocolError
|
||||
from bumble.device import Connection as BumbleConnection
|
||||
from bumble.device import Device
|
||||
from bumble.hci import HCI_Error, Role
|
||||
from bumble.pairing import PairingConfig
|
||||
from bumble.pairing import PairingDelegate as BasePairingDelegate
|
||||
from bumble.pandora import utils
|
||||
from bumble.pandora.config import Config
|
||||
|
||||
|
||||
class PairingDelegate(BasePairingDelegate):
|
||||
@@ -244,16 +244,16 @@ class SecurityService(SecurityServicer):
|
||||
and connection.authenticated
|
||||
and link_key_type
|
||||
in (
|
||||
hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_192_TYPE,
|
||||
hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE,
|
||||
hci.LinkKeyType.AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_192,
|
||||
hci.LinkKeyType.AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256,
|
||||
)
|
||||
)
|
||||
if level == LEVEL4:
|
||||
return (
|
||||
connection.encryption == hci.HCI_Encryption_Change_Event.AES_CCM
|
||||
connection.encryption == hci.HCI_Encryption_Change_Event.Enabled.AES_CCM
|
||||
and connection.authenticated
|
||||
and link_key_type
|
||||
== hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE
|
||||
== hci.LinkKeyType.AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256
|
||||
)
|
||||
raise InvalidArgumentError(f"Unexpected level {level}")
|
||||
|
||||
|
||||
@@ -13,16 +13,18 @@
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import functools
|
||||
import grpc
|
||||
import inspect
|
||||
import logging
|
||||
from typing import Any, Generator, MutableMapping, Optional
|
||||
|
||||
import grpc
|
||||
from google.protobuf.message import Message # pytype: disable=pyi-error
|
||||
|
||||
from bumble.device import Device
|
||||
from bumble.hci import Address, AddressType
|
||||
from google.protobuf.message import Message # pytype: disable=pyi-error
|
||||
from typing import Any, Generator, MutableMapping, Optional
|
||||
|
||||
ADDRESS_TYPES: dict[str, AddressType] = {
|
||||
"public": Address.PUBLIC_DEVICE_ADDRESS,
|
||||
|
||||
+11
-11
@@ -18,26 +18,27 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import struct
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from bumble.device import Connection
|
||||
from bumble import utils
|
||||
from bumble.att import ATT_Error
|
||||
from bumble.device import Connection
|
||||
from bumble.gatt import (
|
||||
GATT_AUDIO_INPUT_CONTROL_POINT_CHARACTERISTIC,
|
||||
GATT_AUDIO_INPUT_CONTROL_SERVICE,
|
||||
GATT_AUDIO_INPUT_DESCRIPTION_CHARACTERISTIC,
|
||||
GATT_AUDIO_INPUT_STATE_CHARACTERISTIC,
|
||||
GATT_AUDIO_INPUT_STATUS_CHARACTERISTIC,
|
||||
GATT_AUDIO_INPUT_TYPE_CHARACTERISTIC,
|
||||
GATT_GAIN_SETTINGS_ATTRIBUTE_CHARACTERISTIC,
|
||||
Attribute,
|
||||
Characteristic,
|
||||
TemplateService,
|
||||
CharacteristicValue,
|
||||
GATT_AUDIO_INPUT_CONTROL_SERVICE,
|
||||
GATT_AUDIO_INPUT_STATE_CHARACTERISTIC,
|
||||
GATT_GAIN_SETTINGS_ATTRIBUTE_CHARACTERISTIC,
|
||||
GATT_AUDIO_INPUT_TYPE_CHARACTERISTIC,
|
||||
GATT_AUDIO_INPUT_STATUS_CHARACTERISTIC,
|
||||
GATT_AUDIO_INPUT_CONTROL_POINT_CHARACTERISTIC,
|
||||
GATT_AUDIO_INPUT_DESCRIPTION_CHARACTERISTIC,
|
||||
TemplateService,
|
||||
)
|
||||
from bumble.gatt_adapters import (
|
||||
CharacteristicProxy,
|
||||
@@ -48,7 +49,6 @@ from bumble.gatt_adapters import (
|
||||
UTF8CharacteristicProxyAdapter,
|
||||
)
|
||||
from bumble.gatt_client import ProfileServiceProxy, ServiceProxy
|
||||
from bumble import utils
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
|
||||
@@ -0,0 +1,403 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
Apple Media Service (AMS).
|
||||
"""
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import enum
|
||||
import logging
|
||||
from typing import Iterable, Optional, Union
|
||||
|
||||
from bumble import utils
|
||||
from bumble.device import Peer
|
||||
from bumble.gatt import (
|
||||
GATT_AMS_ENTITY_ATTRIBUTE_CHARACTERISTIC,
|
||||
GATT_AMS_ENTITY_UPDATE_CHARACTERISTIC,
|
||||
GATT_AMS_REMOTE_COMMAND_CHARACTERISTIC,
|
||||
GATT_AMS_SERVICE,
|
||||
Characteristic,
|
||||
TemplateService,
|
||||
)
|
||||
from bumble.gatt_client import CharacteristicProxy, ProfileServiceProxy, ServiceProxy
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Protocol
|
||||
# -----------------------------------------------------------------------------
|
||||
class RemoteCommandId(utils.OpenIntEnum):
|
||||
PLAY = 0
|
||||
PAUSE = 1
|
||||
TOGGLE_PLAY_PAUSE = 2
|
||||
NEXT_TRACK = 3
|
||||
PREVIOUS_TRACK = 4
|
||||
VOLUME_UP = 5
|
||||
VOLUME_DOWN = 6
|
||||
ADVANCE_REPEAT_MODE = 7
|
||||
ADVANCE_SHUFFLE_MODE = 8
|
||||
SKIP_FORWARD = 9
|
||||
SKIP_BACKWARD = 10
|
||||
LIKE_TRACK = 11
|
||||
DISLIKE_TRACK = 12
|
||||
BOOKMARK_TRACK = 13
|
||||
|
||||
|
||||
class EntityId(utils.OpenIntEnum):
|
||||
PLAYER = 0
|
||||
QUEUE = 1
|
||||
TRACK = 2
|
||||
|
||||
|
||||
class ActionId(utils.OpenIntEnum):
|
||||
POSITIVE = 0
|
||||
NEGATIVE = 1
|
||||
|
||||
|
||||
class EntityUpdateFlags(enum.IntFlag):
|
||||
TRUNCATED = 1
|
||||
|
||||
|
||||
class PlayerAttributeId(utils.OpenIntEnum):
|
||||
NAME = 0
|
||||
PLAYBACK_INFO = 1
|
||||
VOLUME = 2
|
||||
|
||||
|
||||
class QueueAttributeId(utils.OpenIntEnum):
|
||||
INDEX = 0
|
||||
COUNT = 1
|
||||
SHUFFLE_MODE = 2
|
||||
REPEAT_MODE = 3
|
||||
|
||||
|
||||
class ShuffleMode(utils.OpenIntEnum):
|
||||
OFF = 0
|
||||
ONE = 1
|
||||
ALL = 2
|
||||
|
||||
|
||||
class RepeatMode(utils.OpenIntEnum):
|
||||
OFF = 0
|
||||
ONE = 1
|
||||
ALL = 2
|
||||
|
||||
|
||||
class TrackAttributeId(utils.OpenIntEnum):
|
||||
ARTIST = 0
|
||||
ALBUM = 1
|
||||
TITLE = 2
|
||||
DURATION = 3
|
||||
|
||||
|
||||
class PlaybackState(utils.OpenIntEnum):
|
||||
PAUSED = 0
|
||||
PLAYING = 1
|
||||
REWINDING = 2
|
||||
FAST_FORWARDING = 3
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class PlaybackInfo:
|
||||
playback_state: PlaybackState = PlaybackState.PAUSED
|
||||
playback_rate: float = 1.0
|
||||
elapsed_time: float = 0.0
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# GATT Server-side
|
||||
# -----------------------------------------------------------------------------
|
||||
class Ams(TemplateService):
|
||||
UUID = GATT_AMS_SERVICE
|
||||
|
||||
remote_command_characteristic: Characteristic
|
||||
entity_update_characteristic: Characteristic
|
||||
entity_attribute_characteristic: Characteristic
|
||||
|
||||
def __init__(self) -> None:
|
||||
# TODO not the final implementation
|
||||
self.remote_command_characteristic = Characteristic(
|
||||
GATT_AMS_REMOTE_COMMAND_CHARACTERISTIC,
|
||||
Characteristic.Properties.NOTIFY
|
||||
| Characteristic.Properties.WRITE_WITHOUT_RESPONSE,
|
||||
Characteristic.Permissions.WRITEABLE,
|
||||
)
|
||||
|
||||
# TODO not the final implementation
|
||||
self.entity_update_characteristic = Characteristic(
|
||||
GATT_AMS_ENTITY_UPDATE_CHARACTERISTIC,
|
||||
Characteristic.Properties.NOTIFY | Characteristic.Properties.WRITE,
|
||||
Characteristic.Permissions.WRITEABLE,
|
||||
)
|
||||
|
||||
# TODO not the final implementation
|
||||
self.entity_attribute_characteristic = Characteristic(
|
||||
GATT_AMS_ENTITY_ATTRIBUTE_CHARACTERISTIC,
|
||||
Characteristic.Properties.READ
|
||||
| Characteristic.Properties.WRITE_WITHOUT_RESPONSE,
|
||||
Characteristic.Permissions.WRITEABLE | Characteristic.Permissions.READABLE,
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
[
|
||||
self.remote_command_characteristic,
|
||||
self.entity_update_characteristic,
|
||||
self.entity_attribute_characteristic,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# GATT Client-side
|
||||
# -----------------------------------------------------------------------------
|
||||
class AmsProxy(ProfileServiceProxy):
|
||||
SERVICE_CLASS = Ams
|
||||
|
||||
# NOTE: these don't use adapters, because the format for write and notifications
|
||||
# are different.
|
||||
remote_command: CharacteristicProxy[bytes]
|
||||
entity_update: CharacteristicProxy[bytes]
|
||||
entity_attribute: CharacteristicProxy[bytes]
|
||||
|
||||
def __init__(self, service_proxy: ServiceProxy):
|
||||
self.remote_command = service_proxy.get_required_characteristic_by_uuid(
|
||||
GATT_AMS_REMOTE_COMMAND_CHARACTERISTIC
|
||||
)
|
||||
|
||||
self.entity_update = service_proxy.get_required_characteristic_by_uuid(
|
||||
GATT_AMS_ENTITY_UPDATE_CHARACTERISTIC
|
||||
)
|
||||
|
||||
self.entity_attribute = service_proxy.get_required_characteristic_by_uuid(
|
||||
GATT_AMS_ENTITY_ATTRIBUTE_CHARACTERISTIC
|
||||
)
|
||||
|
||||
|
||||
class AmsClient(utils.EventEmitter):
|
||||
EVENT_SUPPORTED_COMMANDS = "supported_commands"
|
||||
EVENT_PLAYER_NAME = "player_name"
|
||||
EVENT_PLAYER_PLAYBACK_INFO = "player_playback_info"
|
||||
EVENT_PLAYER_VOLUME = "player_volume"
|
||||
EVENT_QUEUE_COUNT = "queue_count"
|
||||
EVENT_QUEUE_INDEX = "queue_index"
|
||||
EVENT_QUEUE_SHUFFLE_MODE = "queue_shuffle_mode"
|
||||
EVENT_QUEUE_REPEAT_MODE = "queue_repeat_mode"
|
||||
EVENT_TRACK_ARTIST = "track_artist"
|
||||
EVENT_TRACK_ALBUM = "track_album"
|
||||
EVENT_TRACK_TITLE = "track_title"
|
||||
EVENT_TRACK_DURATION = "track_duration"
|
||||
|
||||
supported_commands: set[RemoteCommandId]
|
||||
player_name: str = ""
|
||||
player_playback_info: PlaybackInfo = PlaybackInfo(PlaybackState.PAUSED, 0.0, 0.0)
|
||||
player_volume: float = 1.0
|
||||
queue_count: int = 0
|
||||
queue_index: int = 0
|
||||
queue_shuffle_mode: ShuffleMode = ShuffleMode.OFF
|
||||
queue_repeat_mode: RepeatMode = RepeatMode.OFF
|
||||
track_artist: str = ""
|
||||
track_album: str = ""
|
||||
track_title: str = ""
|
||||
track_duration: float = 0.0
|
||||
|
||||
def __init__(self, ams_proxy: AmsProxy) -> None:
|
||||
super().__init__()
|
||||
self._ams_proxy = ams_proxy
|
||||
self._started = False
|
||||
self._read_attribute_semaphore = asyncio.Semaphore()
|
||||
self.supported_commands = set()
|
||||
|
||||
@classmethod
|
||||
async def for_peer(cls, peer: Peer) -> Optional[AmsClient]:
|
||||
ams_proxy = await peer.discover_service_and_create_proxy(AmsProxy)
|
||||
if ams_proxy is None:
|
||||
return None
|
||||
return cls(ams_proxy)
|
||||
|
||||
async def start(self) -> None:
|
||||
logger.debug("subscribing to remote command characteristic")
|
||||
await self._ams_proxy.remote_command.subscribe(
|
||||
self._on_remote_command_notification
|
||||
)
|
||||
|
||||
logger.debug("subscribing to entity update characteristic")
|
||||
await self._ams_proxy.entity_update.subscribe(
|
||||
lambda data: utils.AsyncRunner.spawn(
|
||||
self._on_entity_update_notification(data)
|
||||
)
|
||||
)
|
||||
|
||||
self._started = True
|
||||
|
||||
async def stop(self) -> None:
|
||||
await self._ams_proxy.remote_command.unsubscribe(
|
||||
self._on_remote_command_notification
|
||||
)
|
||||
await self._ams_proxy.entity_update.unsubscribe(
|
||||
self._on_entity_update_notification
|
||||
)
|
||||
self._started = False
|
||||
|
||||
async def observe(
|
||||
self,
|
||||
entity: EntityId,
|
||||
attributes: Iterable[
|
||||
Union[PlayerAttributeId, QueueAttributeId, TrackAttributeId]
|
||||
],
|
||||
) -> None:
|
||||
await self._ams_proxy.entity_update.write_value(
|
||||
bytes([entity] + list(attributes)), with_response=True
|
||||
)
|
||||
|
||||
async def command(self, command: RemoteCommandId) -> None:
|
||||
await self._ams_proxy.remote_command.write_value(
|
||||
bytes([command]), with_response=True
|
||||
)
|
||||
|
||||
async def play(self) -> None:
|
||||
await self.command(RemoteCommandId.PLAY)
|
||||
|
||||
async def pause(self) -> None:
|
||||
await self.command(RemoteCommandId.PAUSE)
|
||||
|
||||
async def toggle_play_pause(self) -> None:
|
||||
await self.command(RemoteCommandId.TOGGLE_PLAY_PAUSE)
|
||||
|
||||
async def next_track(self) -> None:
|
||||
await self.command(RemoteCommandId.NEXT_TRACK)
|
||||
|
||||
async def previous_track(self) -> None:
|
||||
await self.command(RemoteCommandId.PREVIOUS_TRACK)
|
||||
|
||||
async def volume_up(self) -> None:
|
||||
await self.command(RemoteCommandId.VOLUME_UP)
|
||||
|
||||
async def volume_down(self) -> None:
|
||||
await self.command(RemoteCommandId.VOLUME_DOWN)
|
||||
|
||||
async def advance_repeat_mode(self) -> None:
|
||||
await self.command(RemoteCommandId.ADVANCE_REPEAT_MODE)
|
||||
|
||||
async def advance_shuffle_mode(self) -> None:
|
||||
await self.command(RemoteCommandId.ADVANCE_SHUFFLE_MODE)
|
||||
|
||||
async def skip_forward(self) -> None:
|
||||
await self.command(RemoteCommandId.SKIP_FORWARD)
|
||||
|
||||
async def skip_backward(self) -> None:
|
||||
await self.command(RemoteCommandId.SKIP_BACKWARD)
|
||||
|
||||
async def like_track(self) -> None:
|
||||
await self.command(RemoteCommandId.LIKE_TRACK)
|
||||
|
||||
async def dislike_track(self) -> None:
|
||||
await self.command(RemoteCommandId.DISLIKE_TRACK)
|
||||
|
||||
async def bookmark_track(self) -> None:
|
||||
await self.command(RemoteCommandId.BOOKMARK_TRACK)
|
||||
|
||||
def _on_remote_command_notification(self, data: bytes) -> None:
|
||||
supported_commands = [RemoteCommandId(command) for command in data]
|
||||
logger.debug(
|
||||
f"supported commands: {[command.name for command in supported_commands]}"
|
||||
)
|
||||
for command in supported_commands:
|
||||
self.supported_commands.add(command)
|
||||
|
||||
self.emit(self.EVENT_SUPPORTED_COMMANDS)
|
||||
|
||||
async def _on_entity_update_notification(self, data: bytes) -> None:
|
||||
entity = EntityId(data[0])
|
||||
flags = EntityUpdateFlags(data[2])
|
||||
value = data[3:]
|
||||
|
||||
if flags & EntityUpdateFlags.TRUNCATED:
|
||||
logger.debug("truncated attribute, fetching full value")
|
||||
|
||||
# Write the entity and attribute we're interested in
|
||||
# (protected by a semaphore, so that we only read one attribute at a time)
|
||||
async with self._read_attribute_semaphore:
|
||||
await self._ams_proxy.entity_attribute.write_value(
|
||||
data[:2], with_response=True
|
||||
)
|
||||
value = await self._ams_proxy.entity_attribute.read_value()
|
||||
|
||||
if entity == EntityId.PLAYER:
|
||||
player_attribute = PlayerAttributeId(data[1])
|
||||
if player_attribute == PlayerAttributeId.NAME:
|
||||
self.player_name = value.decode()
|
||||
self.emit(self.EVENT_PLAYER_NAME)
|
||||
elif player_attribute == PlayerAttributeId.PLAYBACK_INFO:
|
||||
playback_state_str, playback_rate_str, elapsed_time_str = (
|
||||
value.decode().split(",")
|
||||
)
|
||||
self.player_playback_info = PlaybackInfo(
|
||||
PlaybackState(int(playback_state_str)),
|
||||
float(playback_rate_str),
|
||||
float(elapsed_time_str),
|
||||
)
|
||||
self.emit(self.EVENT_PLAYER_PLAYBACK_INFO)
|
||||
elif player_attribute == PlayerAttributeId.VOLUME:
|
||||
self.player_volume = float(value.decode())
|
||||
self.emit(self.EVENT_PLAYER_VOLUME)
|
||||
else:
|
||||
logger.warning(f"received unknown player attribute {player_attribute}")
|
||||
|
||||
elif entity == EntityId.QUEUE:
|
||||
queue_attribute = QueueAttributeId(data[1])
|
||||
if queue_attribute == QueueAttributeId.COUNT:
|
||||
self.queue_count = int(value)
|
||||
self.emit(self.EVENT_QUEUE_COUNT)
|
||||
elif queue_attribute == QueueAttributeId.INDEX:
|
||||
self.queue_index = int(value)
|
||||
self.emit(self.EVENT_QUEUE_INDEX)
|
||||
elif queue_attribute == QueueAttributeId.REPEAT_MODE:
|
||||
self.queue_repeat_mode = RepeatMode(int(value))
|
||||
self.emit(self.EVENT_QUEUE_REPEAT_MODE)
|
||||
elif queue_attribute == QueueAttributeId.SHUFFLE_MODE:
|
||||
self.queue_shuffle_mode = ShuffleMode(int(value))
|
||||
self.emit(self.EVENT_QUEUE_SHUFFLE_MODE)
|
||||
else:
|
||||
logger.warning(f"received unknown queue attribute {queue_attribute}")
|
||||
|
||||
elif entity == EntityId.TRACK:
|
||||
track_attribute = TrackAttributeId(data[1])
|
||||
if track_attribute == TrackAttributeId.ARTIST:
|
||||
self.track_artist = value.decode()
|
||||
self.emit(self.EVENT_TRACK_ARTIST)
|
||||
elif track_attribute == TrackAttributeId.ALBUM:
|
||||
self.track_album = value.decode()
|
||||
self.emit(self.EVENT_TRACK_ALBUM)
|
||||
elif track_attribute == TrackAttributeId.TITLE:
|
||||
self.track_title = value.decode()
|
||||
self.emit(self.EVENT_TRACK_TITLE)
|
||||
elif track_attribute == TrackAttributeId.DURATION:
|
||||
self.track_duration = float(value.decode())
|
||||
self.emit(self.EVENT_TRACK_DURATION)
|
||||
else:
|
||||
logger.warning(f"received unknown track attribute {track_attribute}")
|
||||
|
||||
else:
|
||||
logger.warning(f"received unknown attribute ID {data[1]}")
|
||||
@@ -20,6 +20,7 @@ Apple Notification Center Service (ANCS).
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import datetime
|
||||
@@ -28,21 +29,19 @@ import logging
|
||||
import struct
|
||||
from typing import Optional, Sequence, Union
|
||||
|
||||
|
||||
from bumble import utils
|
||||
from bumble.att import ATT_Error
|
||||
from bumble.device import Peer
|
||||
from bumble.gatt import (
|
||||
Characteristic,
|
||||
GATT_ANCS_SERVICE,
|
||||
GATT_ANCS_NOTIFICATION_SOURCE_CHARACTERISTIC,
|
||||
GATT_ANCS_CONTROL_POINT_CHARACTERISTIC,
|
||||
GATT_ANCS_DATA_SOURCE_CHARACTERISTIC,
|
||||
GATT_ANCS_NOTIFICATION_SOURCE_CHARACTERISTIC,
|
||||
GATT_ANCS_SERVICE,
|
||||
Characteristic,
|
||||
TemplateService,
|
||||
)
|
||||
from bumble.gatt_client import CharacteristicProxy, ProfileServiceProxy, ServiceProxy
|
||||
from bumble.gatt_adapters import SerializableCharacteristicProxyAdapter
|
||||
from bumble import utils
|
||||
|
||||
from bumble.gatt_client import CharacteristicProxy, ProfileServiceProxy, ServiceProxy
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
|
||||
+19
-20
@@ -18,22 +18,17 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
import enum
|
||||
import functools
|
||||
import logging
|
||||
import struct
|
||||
from typing import Any, Optional, Union, TypeVar
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Optional, TypeVar, Union
|
||||
|
||||
from bumble import utils
|
||||
from bumble import colors
|
||||
from bumble.profiles.bap import CodecSpecificConfiguration
|
||||
from bumble import colors, device, gatt, gatt_client, hci, utils
|
||||
from bumble.profiles import le_audio
|
||||
from bumble import device
|
||||
from bumble import gatt
|
||||
from bumble import gatt_client
|
||||
from bumble import hci
|
||||
from bumble.profiles.bap import CodecSpecificConfiguration
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -343,22 +338,16 @@ class AseStateMachine(gatt.Characteristic):
|
||||
self.service.device.EVENT_CIS_ESTABLISHMENT, self.on_cis_establishment
|
||||
)
|
||||
|
||||
def on_cis_request(
|
||||
self,
|
||||
acl_connection: device.Connection,
|
||||
cis_handle: int,
|
||||
cig_id: int,
|
||||
cis_id: int,
|
||||
) -> None:
|
||||
def on_cis_request(self, cis_link: device.CisLink) -> None:
|
||||
if (
|
||||
cig_id == self.cig_id
|
||||
and cis_id == self.cis_id
|
||||
cis_link.cig_id == self.cig_id
|
||||
and cis_link.cis_id == self.cis_id
|
||||
and self.state == self.State.ENABLING
|
||||
):
|
||||
utils.cancel_on_event(
|
||||
acl_connection,
|
||||
cis_link.acl_connection,
|
||||
'flush',
|
||||
self.service.device.accept_cis_request(cis_handle),
|
||||
self.service.device.accept_cis_request(cis_link),
|
||||
)
|
||||
|
||||
def on_cis_establishment(self, cis_link: device.CisLink) -> None:
|
||||
@@ -458,6 +447,16 @@ class AseStateMachine(gatt.Characteristic):
|
||||
|
||||
self.metadata = le_audio.Metadata.from_bytes(metadata)
|
||||
self.state = self.State.ENABLING
|
||||
# CIS could be established before enable.
|
||||
if cis_link := next(
|
||||
(
|
||||
cis_link
|
||||
for cis_link in self.service.device.cis_links.values()
|
||||
if cis_link.cig_id == self.cig_id and cis_link.cis_id == self.cis_id
|
||||
),
|
||||
None,
|
||||
):
|
||||
self.on_cis_establishment(cis_link)
|
||||
|
||||
return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
|
||||
|
||||
|
||||
+8
-12
@@ -17,16 +17,13 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import enum
|
||||
import struct
|
||||
import logging
|
||||
from typing import Optional, Callable, Union, Any
|
||||
import struct
|
||||
from typing import Any, Callable, Optional, Union
|
||||
|
||||
from bumble import l2cap
|
||||
from bumble import utils
|
||||
from bumble import gatt
|
||||
from bumble import gatt_client
|
||||
from bumble import data_types, gatt, gatt_client, l2cap, utils
|
||||
from bumble.core import AdvertisingData
|
||||
from bumble.device import Device, Connection
|
||||
from bumble.device import Connection, Device
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -188,12 +185,11 @@ class AshaService(gatt.TemplateService):
|
||||
return bytes(
|
||||
AdvertisingData(
|
||||
[
|
||||
(
|
||||
AdvertisingData.SERVICE_DATA_16_BIT_UUID,
|
||||
bytes(gatt.GATT_ASHA_SERVICE)
|
||||
+ bytes([self.protocol_version, self.capability])
|
||||
data_types.ServiceData16BitUUID(
|
||||
gatt.GATT_ASHA_SERVICE,
|
||||
bytes([self.protocol_version, self.capability])
|
||||
+ self.hisyncid[:4],
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
+11
-23
@@ -18,21 +18,18 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
import dataclasses
|
||||
import enum
|
||||
import struct
|
||||
import functools
|
||||
import logging
|
||||
import struct
|
||||
from collections.abc import Sequence
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from bumble import core
|
||||
from bumble import hci
|
||||
from bumble import gatt
|
||||
from bumble import utils
|
||||
from bumble import core, data_types, gatt, hci, utils
|
||||
from bumble.profiles import le_audio
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -260,11 +257,10 @@ class UnicastServerAdvertisingData:
|
||||
return bytes(
|
||||
core.AdvertisingData(
|
||||
[
|
||||
(
|
||||
core.AdvertisingData.SERVICE_DATA_16_BIT_UUID,
|
||||
data_types.ServiceData16BitUUID(
|
||||
gatt.GATT_AUDIO_STREAM_CONTROL_SERVICE,
|
||||
struct.pack(
|
||||
'<2sBIB',
|
||||
bytes(gatt.GATT_AUDIO_STREAM_CONTROL_SERVICE),
|
||||
'<BIB',
|
||||
self.announcement_type,
|
||||
self.available_audio_contexts,
|
||||
len(self.metadata),
|
||||
@@ -493,12 +489,8 @@ class BroadcastAudioAnnouncement:
|
||||
return bytes(
|
||||
core.AdvertisingData(
|
||||
[
|
||||
(
|
||||
core.AdvertisingData.SERVICE_DATA_16_BIT_UUID,
|
||||
(
|
||||
bytes(gatt.GATT_BROADCAST_AUDIO_ANNOUNCEMENT_SERVICE)
|
||||
+ bytes(self)
|
||||
),
|
||||
data_types.ServiceData16BitUUID(
|
||||
gatt.GATT_BROADCAST_AUDIO_ANNOUNCEMENT_SERVICE, bytes(self)
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -610,12 +602,8 @@ class BasicAudioAnnouncement:
|
||||
return bytes(
|
||||
core.AdvertisingData(
|
||||
[
|
||||
(
|
||||
core.AdvertisingData.SERVICE_DATA_16_BIT_UUID,
|
||||
(
|
||||
bytes(gatt.GATT_BASIC_AUDIO_ANNOUNCEMENT_SERVICE)
|
||||
+ bytes(self)
|
||||
),
|
||||
data_types.ServiceData16BitUUID(
|
||||
gatt.GATT_BASIC_AUDIO_ANNOUNCEMENT_SERVICE, bytes(self)
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
@@ -17,18 +17,13 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import logging
|
||||
import struct
|
||||
from typing import ClassVar, Optional, Sequence
|
||||
|
||||
from bumble import core
|
||||
from bumble import device
|
||||
from bumble import gatt
|
||||
from bumble import gatt_adapters
|
||||
from bumble import gatt_client
|
||||
from bumble import hci
|
||||
from bumble import utils
|
||||
from bumble import core, device, gatt, gatt_adapters, gatt_client, hci, utils
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
|
||||
@@ -18,19 +18,18 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
from typing import Optional
|
||||
|
||||
from bumble.gatt_client import ProfileServiceProxy
|
||||
from bumble.gatt import (
|
||||
GATT_BATTERY_SERVICE,
|
||||
GATT_BATTERY_LEVEL_CHARACTERISTIC,
|
||||
TemplateService,
|
||||
GATT_BATTERY_SERVICE,
|
||||
Characteristic,
|
||||
CharacteristicValue,
|
||||
TemplateService,
|
||||
)
|
||||
from bumble.gatt_client import CharacteristicProxy
|
||||
from bumble.gatt_adapters import (
|
||||
PackedCharacteristicAdapter,
|
||||
PackedCharacteristicProxyAdapter,
|
||||
)
|
||||
from bumble.gatt_client import CharacteristicProxy, ProfileServiceProxy
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -18,8 +18,7 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
from bumble import gatt
|
||||
from bumble import gatt_client
|
||||
from bumble import gatt, gatt_client
|
||||
from bumble.profiles import csip
|
||||
|
||||
|
||||
|
||||
@@ -17,16 +17,12 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
import struct
|
||||
from typing import Optional
|
||||
|
||||
from bumble import core
|
||||
from bumble import crypto
|
||||
from bumble import device
|
||||
from bumble import gatt
|
||||
from bumble import gatt_client
|
||||
|
||||
from bumble import core, crypto, device, gatt, gatt_client
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
|
||||
@@ -25,12 +25,12 @@ from bumble.gatt import (
|
||||
GATT_HARDWARE_REVISION_STRING_CHARACTERISTIC,
|
||||
GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC,
|
||||
GATT_MODEL_NUMBER_STRING_CHARACTERISTIC,
|
||||
GATT_REGULATORY_CERTIFICATION_DATA_LIST_CHARACTERISTIC,
|
||||
GATT_SERIAL_NUMBER_STRING_CHARACTERISTIC,
|
||||
GATT_SOFTWARE_REVISION_STRING_CHARACTERISTIC,
|
||||
GATT_SYSTEM_ID_CHARACTERISTIC,
|
||||
GATT_REGULATORY_CERTIFICATION_DATA_LIST_CHARACTERISTIC,
|
||||
TemplateService,
|
||||
Characteristic,
|
||||
TemplateService,
|
||||
)
|
||||
from bumble.gatt_adapters import (
|
||||
DelegatedCharacteristicProxyAdapter,
|
||||
|
||||
@@ -23,11 +23,11 @@ from typing import Optional, Union
|
||||
|
||||
from bumble.core import Appearance
|
||||
from bumble.gatt import (
|
||||
TemplateService,
|
||||
Characteristic,
|
||||
GATT_GENERIC_ACCESS_SERVICE,
|
||||
GATT_DEVICE_NAME_CHARACTERISTIC,
|
||||
GATT_APPEARANCE_CHARACTERISTIC,
|
||||
GATT_DEVICE_NAME_CHARACTERISTIC,
|
||||
GATT_GENERIC_ACCESS_SERVICE,
|
||||
Characteristic,
|
||||
TemplateService,
|
||||
)
|
||||
from bumble.gatt_adapters import (
|
||||
DelegatedCharacteristicProxyAdapter,
|
||||
|
||||
@@ -17,10 +17,7 @@ from __future__ import annotations
|
||||
import struct
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bumble import att
|
||||
from bumble import gatt
|
||||
from bumble import gatt_client
|
||||
from bumble import crypto
|
||||
from bumble import att, crypto, gatt, gatt_client
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bumble import device
|
||||
|
||||
@@ -18,21 +18,21 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import struct
|
||||
from enum import IntFlag
|
||||
from typing import Optional
|
||||
|
||||
from bumble.gatt import (
|
||||
TemplateService,
|
||||
Characteristic,
|
||||
GATT_BGR_FEATURES_CHARACTERISTIC,
|
||||
GATT_BGS_FEATURES_CHARACTERISTIC,
|
||||
GATT_GAMING_AUDIO_SERVICE,
|
||||
GATT_GMAP_ROLE_CHARACTERISTIC,
|
||||
GATT_UGG_FEATURES_CHARACTERISTIC,
|
||||
GATT_UGT_FEATURES_CHARACTERISTIC,
|
||||
GATT_BGS_FEATURES_CHARACTERISTIC,
|
||||
GATT_BGR_FEATURES_CHARACTERISTIC,
|
||||
Characteristic,
|
||||
TemplateService,
|
||||
)
|
||||
from bumble.gatt_adapters import DelegatedCharacteristicProxyAdapter
|
||||
from bumble.gatt_client import CharacteristicProxy, ProfileServiceProxy, ServiceProxy
|
||||
from enum import IntFlag
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -16,16 +16,16 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import functools
|
||||
from dataclasses import dataclass, field
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
from bumble import att, gatt, gatt_adapters, gatt_client
|
||||
from bumble import att, gatt, gatt_adapters, gatt_client, utils
|
||||
from bumble.core import InvalidArgumentError, InvalidStateError
|
||||
from bumble.device import Device, Connection
|
||||
from bumble import utils
|
||||
from bumble.device import Connection, Device
|
||||
from bumble.hci import Address
|
||||
|
||||
|
||||
|
||||
@@ -17,20 +17,21 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
from enum import IntEnum
|
||||
|
||||
import struct
|
||||
from enum import IntEnum
|
||||
from typing import Optional
|
||||
|
||||
from bumble import core
|
||||
from bumble.att import ATT_Error
|
||||
from bumble.gatt import (
|
||||
GATT_HEART_RATE_SERVICE,
|
||||
GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC,
|
||||
GATT_BODY_SENSOR_LOCATION_CHARACTERISTIC,
|
||||
GATT_HEART_RATE_CONTROL_POINT_CHARACTERISTIC,
|
||||
TemplateService,
|
||||
GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC,
|
||||
GATT_HEART_RATE_SERVICE,
|
||||
Characteristic,
|
||||
CharacteristicValue,
|
||||
TemplateService,
|
||||
)
|
||||
from bumble.gatt_adapters import (
|
||||
DelegatedCharacteristicAdapter,
|
||||
|
||||
@@ -16,14 +16,16 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import enum
|
||||
import struct
|
||||
from typing import Any
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from bumble.profiles import bap
|
||||
from bumble import utils
|
||||
from bumble.profiles import bap
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -22,16 +22,12 @@ import asyncio
|
||||
import dataclasses
|
||||
import enum
|
||||
import struct
|
||||
from typing import TYPE_CHECKING, ClassVar, Optional
|
||||
|
||||
from bumble import core
|
||||
from bumble import device
|
||||
from bumble import gatt
|
||||
from bumble import gatt_client
|
||||
from bumble import utils
|
||||
|
||||
from typing import Optional, ClassVar, TYPE_CHECKING
|
||||
from typing_extensions import Self
|
||||
|
||||
from bumble import core, device, gatt, gatt_client, utils
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -17,18 +17,15 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import logging
|
||||
import struct
|
||||
from typing import Optional, Sequence, Union
|
||||
|
||||
from bumble.profiles.bap import AudioLocation, CodecSpecificCapabilities, ContextType
|
||||
from bumble import gatt, gatt_adapters, gatt_client, hci
|
||||
from bumble.profiles import le_audio
|
||||
from bumble import gatt
|
||||
from bumble import gatt_adapters
|
||||
from bumble import gatt_client
|
||||
from bumble import hci
|
||||
|
||||
from bumble.profiles.bap import AudioLocation, CodecSpecificCapabilities, ContextType
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
|
||||
@@ -16,8 +16,10 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import enum
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from bumble.profiles import le_audio
|
||||
|
||||
@@ -22,15 +22,14 @@ import logging
|
||||
import struct
|
||||
|
||||
from bumble.gatt import (
|
||||
TemplateService,
|
||||
Characteristic,
|
||||
GATT_TELEPHONY_AND_MEDIA_AUDIO_SERVICE,
|
||||
GATT_TMAP_ROLE_CHARACTERISTIC,
|
||||
Characteristic,
|
||||
TemplateService,
|
||||
)
|
||||
from bumble.gatt_adapters import DelegatedCharacteristicProxyAdapter
|
||||
from bumble.gatt_client import CharacteristicProxy, ProfileServiceProxy, ServiceProxy
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -17,18 +17,12 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import enum
|
||||
|
||||
from typing import Sequence
|
||||
|
||||
from bumble import att
|
||||
from bumble import utils
|
||||
from bumble import device
|
||||
from bumble import gatt
|
||||
from bumble import gatt_adapters
|
||||
from bumble import gatt_client
|
||||
|
||||
from bumble import att, device, gatt, gatt_adapters, gatt_client, utils
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
|
||||
@@ -20,17 +20,18 @@ import struct
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from bumble.device import Connection
|
||||
from bumble import utils
|
||||
from bumble.att import ATT_Error
|
||||
from bumble.device import Connection
|
||||
from bumble.gatt import (
|
||||
Characteristic,
|
||||
TemplateService,
|
||||
CharacteristicValue,
|
||||
GATT_AUDIO_LOCATION_CHARACTERISTIC,
|
||||
GATT_AUDIO_OUTPUT_DESCRIPTION_CHARACTERISTIC,
|
||||
GATT_VOLUME_OFFSET_CONTROL_POINT_CHARACTERISTIC,
|
||||
GATT_VOLUME_OFFSET_CONTROL_SERVICE,
|
||||
GATT_VOLUME_OFFSET_STATE_CHARACTERISTIC,
|
||||
GATT_AUDIO_LOCATION_CHARACTERISTIC,
|
||||
GATT_VOLUME_OFFSET_CONTROL_POINT_CHARACTERISTIC,
|
||||
GATT_AUDIO_OUTPUT_DESCRIPTION_CHARACTERISTIC,
|
||||
Characteristic,
|
||||
CharacteristicValue,
|
||||
TemplateService,
|
||||
)
|
||||
from bumble.gatt_adapters import (
|
||||
DelegatedCharacteristicProxyAdapter,
|
||||
@@ -38,7 +39,6 @@ from bumble.gatt_adapters import (
|
||||
UTF8CharacteristicProxyAdapter,
|
||||
)
|
||||
from bumble.gatt_client import ProfileServiceProxy, ServiceProxy
|
||||
from bumble import utils
|
||||
from bumble.profiles.bap import AudioLocation
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
+11
-14
@@ -17,33 +17,30 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import asyncio
|
||||
import collections
|
||||
import dataclasses
|
||||
import enum
|
||||
from typing import Callable, Optional, Union, TYPE_CHECKING
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Callable, Optional, Union
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
|
||||
from bumble import core
|
||||
from bumble import l2cap
|
||||
from bumble import sdp
|
||||
from bumble import utils
|
||||
from bumble import core, l2cap, sdp, utils
|
||||
from bumble.colors import color
|
||||
from bumble.core import (
|
||||
UUID,
|
||||
BT_RFCOMM_PROTOCOL_ID,
|
||||
PhysicalTransport,
|
||||
BT_L2CAP_PROTOCOL_ID,
|
||||
BT_RFCOMM_PROTOCOL_ID,
|
||||
UUID,
|
||||
InvalidArgumentError,
|
||||
InvalidStateError,
|
||||
InvalidPacketError,
|
||||
InvalidStateError,
|
||||
PhysicalTransport,
|
||||
ProtocolError,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bumble.device import Device, Connection
|
||||
from bumble.device import Connection, Device
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -1047,8 +1044,8 @@ class Client:
|
||||
self.l2cap_channel = await self.connection.create_l2cap_channel(
|
||||
spec=l2cap.ClassicChannelSpec(psm=RFCOMM_PSM, mtu=self.l2cap_mtu)
|
||||
)
|
||||
except ProtocolError as error:
|
||||
logger.warning(f'L2CAP connection failed: {error}')
|
||||
except ProtocolError:
|
||||
logger.exception('L2CAP connection failed')
|
||||
raise
|
||||
|
||||
assert self.l2cap_channel is not None
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
|
||||
|
||||
|
||||
+10
-8
@@ -16,24 +16,26 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import struct
|
||||
from typing import Iterable, NewType, Optional, Union, Sequence, TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Iterable, NewType, Optional, Sequence, Union
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from bumble import core, l2cap
|
||||
from bumble.colors import color
|
||||
from bumble.core import (
|
||||
InvalidStateError,
|
||||
InvalidArgumentError,
|
||||
InvalidPacketError,
|
||||
InvalidStateError,
|
||||
ProtocolError,
|
||||
)
|
||||
from bumble.hci import HCI_Object, name_or_number, key_with_value
|
||||
from bumble.hci import HCI_Object, key_with_value, name_or_number
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bumble.device import Device, Connection
|
||||
from bumble.device import Connection, Device
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -1084,8 +1086,8 @@ class Server:
|
||||
def on_pdu(self, pdu):
|
||||
try:
|
||||
sdp_pdu = SDP_PDU.from_bytes(pdu)
|
||||
except Exception as error:
|
||||
logger.warning(color(f'failed to parse SDP Request PDU: {error}', 'red'))
|
||||
except Exception:
|
||||
logger.exception(color('failed to parse SDP Request PDU', 'red'))
|
||||
self.send_response(
|
||||
SDP_ErrorResponse(
|
||||
transaction_id=0, error_code=SDP_INVALID_REQUEST_SYNTAX_ERROR
|
||||
@@ -1100,8 +1102,8 @@ class Server:
|
||||
if handler:
|
||||
try:
|
||||
handler(sdp_pdu)
|
||||
except Exception as error:
|
||||
logger.exception(f'{color("!!! Exception in handler:", "red")} {error}')
|
||||
except Exception:
|
||||
logger.exception(color("!!! Exception in handler:", "red"))
|
||||
self.send_response(
|
||||
SDP_ErrorResponse(
|
||||
transaction_id=sdp_pdu.transaction_id,
|
||||
|
||||
+154
-160
@@ -23,38 +23,41 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
import logging
|
||||
|
||||
import asyncio
|
||||
import enum
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Awaitable,
|
||||
Callable,
|
||||
ClassVar,
|
||||
Optional,
|
||||
TypeVar,
|
||||
cast,
|
||||
)
|
||||
|
||||
|
||||
from bumble import crypto, utils
|
||||
from bumble.colors import color
|
||||
from bumble.hci import (
|
||||
Address,
|
||||
Role,
|
||||
HCI_LE_Enable_Encryption_Command,
|
||||
HCI_Object,
|
||||
key_with_value,
|
||||
)
|
||||
from bumble.core import (
|
||||
PhysicalTransport,
|
||||
AdvertisingData,
|
||||
InvalidArgumentError,
|
||||
PhysicalTransport,
|
||||
ProtocolError,
|
||||
name_or_number,
|
||||
)
|
||||
from bumble.hci import (
|
||||
Address,
|
||||
Fields,
|
||||
HCI_LE_Enable_Encryption_Command,
|
||||
HCI_Object,
|
||||
Role,
|
||||
key_with_value,
|
||||
metadata,
|
||||
)
|
||||
from bumble.keys import PairingKeys
|
||||
from bumble import crypto
|
||||
from bumble import utils
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bumble.device import Connection, Device
|
||||
@@ -200,31 +203,32 @@ def error_name(error_code: int) -> str:
|
||||
# -----------------------------------------------------------------------------
|
||||
# Classes
|
||||
# -----------------------------------------------------------------------------
|
||||
@dataclass
|
||||
class SMP_Command:
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part H - 3 SECURITY MANAGER PROTOCOL
|
||||
'''
|
||||
|
||||
smp_classes: dict[int, type[SMP_Command]] = {}
|
||||
fields: Any
|
||||
code = 0
|
||||
name = ''
|
||||
smp_classes: ClassVar[dict[int, type[SMP_Command]]] = {}
|
||||
fields: ClassVar[Fields]
|
||||
code: int = field(default=0, init=False)
|
||||
name: str = field(default='', init=False)
|
||||
_payload: Optional[bytes] = field(default=None, init=False)
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(pdu: bytes) -> "SMP_Command":
|
||||
@classmethod
|
||||
def from_bytes(cls, pdu: bytes) -> "SMP_Command":
|
||||
code = pdu[0]
|
||||
|
||||
cls = SMP_Command.smp_classes.get(code)
|
||||
if cls is None:
|
||||
instance = SMP_Command(pdu)
|
||||
subclass = SMP_Command.smp_classes.get(code)
|
||||
if subclass is None:
|
||||
instance = SMP_Command()
|
||||
instance.name = SMP_Command.command_name(code)
|
||||
instance.code = code
|
||||
instance.payload = pdu
|
||||
return instance
|
||||
self = cls.__new__(cls)
|
||||
SMP_Command.__init__(self, pdu)
|
||||
if hasattr(self, 'fields'):
|
||||
self.init_from_bytes(pdu, 1)
|
||||
return self
|
||||
instance = subclass(**HCI_Object.dict_from_bytes(pdu, 1, subclass.fields))
|
||||
instance.payload = pdu[1:]
|
||||
return instance
|
||||
|
||||
@staticmethod
|
||||
def command_name(code: int) -> str:
|
||||
@@ -264,36 +268,35 @@ class SMP_Command:
|
||||
def keypress_notification_type_name(notification_type: int) -> str:
|
||||
return name_or_number(SMP_KEYPRESS_NOTIFICATION_TYPE_NAMES, notification_type)
|
||||
|
||||
@staticmethod
|
||||
def subclass(fields):
|
||||
def inner(cls):
|
||||
cls.name = cls.__name__.upper()
|
||||
cls.code = key_with_value(SMP_COMMAND_NAMES, cls.name)
|
||||
if cls.code is None:
|
||||
raise KeyError(
|
||||
f'Command name {cls.name} not found in SMP_COMMAND_NAMES'
|
||||
)
|
||||
cls.fields = fields
|
||||
_Command = TypeVar("_Command", bound="SMP_Command")
|
||||
|
||||
# Register a factory for this class
|
||||
SMP_Command.smp_classes[cls.code] = cls
|
||||
@classmethod
|
||||
def subclass(cls, subclass: type[_Command]) -> type[_Command]:
|
||||
subclass.name = subclass.__name__.upper()
|
||||
subclass.code = key_with_value(SMP_COMMAND_NAMES, subclass.name)
|
||||
if subclass.code is None:
|
||||
raise KeyError(
|
||||
f'Command name {subclass.name} not found in SMP_COMMAND_NAMES'
|
||||
)
|
||||
subclass.fields = HCI_Object.fields_from_dataclass(subclass)
|
||||
|
||||
return cls
|
||||
# Register a factory for this class
|
||||
SMP_Command.smp_classes[subclass.code] = subclass
|
||||
|
||||
return inner
|
||||
return subclass
|
||||
|
||||
def __init__(self, pdu: Optional[bytes] = None, **kwargs: Any) -> None:
|
||||
if hasattr(self, 'fields') and kwargs:
|
||||
HCI_Object.init_from_fields(self, self.fields, kwargs)
|
||||
if pdu is None:
|
||||
pdu = bytes([self.code]) + HCI_Object.dict_to_bytes(kwargs, self.fields)
|
||||
self.pdu = pdu
|
||||
@property
|
||||
def payload(self) -> bytes:
|
||||
if self._payload is None:
|
||||
self._payload = HCI_Object.dict_to_bytes(self.__dict__, self.fields)
|
||||
return self._payload
|
||||
|
||||
def init_from_bytes(self, pdu: bytes, offset: int) -> None:
|
||||
return HCI_Object.init_from_bytes(self, pdu, offset, self.fields)
|
||||
@payload.setter
|
||||
def payload(self, value: bytes) -> None:
|
||||
self._payload = value
|
||||
|
||||
def __bytes__(self):
|
||||
return self.pdu
|
||||
return bytes([self.code]) + self.payload
|
||||
|
||||
def __str__(self):
|
||||
result = color(self.name, 'yellow')
|
||||
@@ -306,206 +309,192 @@ class SMP_Command:
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@SMP_Command.subclass(
|
||||
[
|
||||
('io_capability', {'size': 1, 'mapper': SMP_Command.io_capability_name}),
|
||||
('oob_data_flag', 1),
|
||||
('auth_req', {'size': 1, 'mapper': SMP_Command.auth_req_str}),
|
||||
('maximum_encryption_key_size', 1),
|
||||
(
|
||||
'initiator_key_distribution',
|
||||
{'size': 1, 'mapper': SMP_Command.key_distribution_str},
|
||||
),
|
||||
(
|
||||
'responder_key_distribution',
|
||||
{'size': 1, 'mapper': SMP_Command.key_distribution_str},
|
||||
),
|
||||
]
|
||||
)
|
||||
@SMP_Command.subclass
|
||||
@dataclass
|
||||
class SMP_Pairing_Request_Command(SMP_Command):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part H - 3.5.1 Pairing Request
|
||||
'''
|
||||
|
||||
io_capability: int
|
||||
oob_data_flag: int
|
||||
auth_req: int
|
||||
maximum_encryption_key_size: int
|
||||
initiator_key_distribution: int
|
||||
responder_key_distribution: int
|
||||
io_capability: int = field(
|
||||
metadata=metadata({'size': 1, 'mapper': SMP_Command.io_capability_name})
|
||||
)
|
||||
oob_data_flag: int = field(metadata=metadata(1))
|
||||
auth_req: int = field(
|
||||
metadata=metadata({'size': 1, 'mapper': SMP_Command.auth_req_str})
|
||||
)
|
||||
maximum_encryption_key_size: int = field(metadata=metadata(1))
|
||||
initiator_key_distribution: int = field(
|
||||
metadata=metadata({'size': 1, 'mapper': SMP_Command.key_distribution_str})
|
||||
)
|
||||
responder_key_distribution: int = field(
|
||||
metadata=metadata({'size': 1, 'mapper': SMP_Command.key_distribution_str})
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@SMP_Command.subclass(
|
||||
[
|
||||
('io_capability', {'size': 1, 'mapper': SMP_Command.io_capability_name}),
|
||||
('oob_data_flag', 1),
|
||||
('auth_req', {'size': 1, 'mapper': SMP_Command.auth_req_str}),
|
||||
('maximum_encryption_key_size', 1),
|
||||
(
|
||||
'initiator_key_distribution',
|
||||
{'size': 1, 'mapper': SMP_Command.key_distribution_str},
|
||||
),
|
||||
(
|
||||
'responder_key_distribution',
|
||||
{'size': 1, 'mapper': SMP_Command.key_distribution_str},
|
||||
),
|
||||
]
|
||||
)
|
||||
@SMP_Command.subclass
|
||||
@dataclass
|
||||
class SMP_Pairing_Response_Command(SMP_Command):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part H - 3.5.2 Pairing Response
|
||||
'''
|
||||
|
||||
io_capability: int
|
||||
oob_data_flag: int
|
||||
auth_req: int
|
||||
maximum_encryption_key_size: int
|
||||
initiator_key_distribution: int
|
||||
responder_key_distribution: int
|
||||
io_capability: int = field(
|
||||
metadata=metadata({'size': 1, 'mapper': SMP_Command.io_capability_name})
|
||||
)
|
||||
oob_data_flag: int = field(metadata=metadata(1))
|
||||
auth_req: int = field(
|
||||
metadata=metadata({'size': 1, 'mapper': SMP_Command.auth_req_str})
|
||||
)
|
||||
maximum_encryption_key_size: int = field(metadata=metadata(1))
|
||||
initiator_key_distribution: int = field(
|
||||
metadata=metadata({'size': 1, 'mapper': SMP_Command.key_distribution_str})
|
||||
)
|
||||
responder_key_distribution: int = field(
|
||||
metadata=metadata({'size': 1, 'mapper': SMP_Command.key_distribution_str})
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@SMP_Command.subclass([('confirm_value', 16)])
|
||||
@SMP_Command.subclass
|
||||
@dataclass
|
||||
class SMP_Pairing_Confirm_Command(SMP_Command):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part H - 3.5.3 Pairing Confirm
|
||||
'''
|
||||
|
||||
confirm_value: bytes
|
||||
confirm_value: bytes = field(metadata=metadata(16))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@SMP_Command.subclass([('random_value', 16)])
|
||||
@SMP_Command.subclass
|
||||
@dataclass
|
||||
class SMP_Pairing_Random_Command(SMP_Command):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part H - 3.5.4 Pairing Random
|
||||
'''
|
||||
|
||||
random_value: bytes
|
||||
random_value: bytes = field(metadata=metadata(16))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@SMP_Command.subclass([('reason', {'size': 1, 'mapper': error_name})])
|
||||
@SMP_Command.subclass
|
||||
@dataclass
|
||||
class SMP_Pairing_Failed_Command(SMP_Command):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part H - 3.5.5 Pairing Failed
|
||||
'''
|
||||
|
||||
reason: int
|
||||
reason: int = field(metadata=metadata({'size': 1, 'mapper': error_name}))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@SMP_Command.subclass([('public_key_x', 32), ('public_key_y', 32)])
|
||||
@SMP_Command.subclass
|
||||
@dataclass
|
||||
class SMP_Pairing_Public_Key_Command(SMP_Command):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part H - 3.5.6 Pairing Public Key
|
||||
'''
|
||||
|
||||
public_key_x: bytes
|
||||
public_key_y: bytes
|
||||
public_key_x: bytes = field(metadata=metadata(32))
|
||||
public_key_y: bytes = field(metadata=metadata(32))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@SMP_Command.subclass(
|
||||
[
|
||||
('dhkey_check', 16),
|
||||
]
|
||||
)
|
||||
@SMP_Command.subclass
|
||||
@dataclass
|
||||
class SMP_Pairing_DHKey_Check_Command(SMP_Command):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part H - 3.5.7 Pairing DHKey Check
|
||||
'''
|
||||
|
||||
dhkey_check: bytes
|
||||
dhkey_check: bytes = field(metadata=metadata(16))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@SMP_Command.subclass(
|
||||
[
|
||||
(
|
||||
'notification_type',
|
||||
{'size': 1, 'mapper': SMP_Command.keypress_notification_type_name},
|
||||
),
|
||||
]
|
||||
)
|
||||
@SMP_Command.subclass
|
||||
@dataclass
|
||||
class SMP_Pairing_Keypress_Notification_Command(SMP_Command):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part H - 3.5.8 Keypress Notification
|
||||
'''
|
||||
|
||||
notification_type: int
|
||||
notification_type: int = field(
|
||||
metadata=metadata(
|
||||
{'size': 1, 'mapper': SMP_Command.keypress_notification_type_name}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@SMP_Command.subclass([('long_term_key', 16)])
|
||||
@SMP_Command.subclass
|
||||
@dataclass
|
||||
class SMP_Encryption_Information_Command(SMP_Command):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part H - 3.6.2 Encryption Information
|
||||
'''
|
||||
|
||||
long_term_key: bytes
|
||||
long_term_key: bytes = field(metadata=metadata(16))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@SMP_Command.subclass([('ediv', 2), ('rand', 8)])
|
||||
@SMP_Command.subclass
|
||||
@dataclass
|
||||
class SMP_Master_Identification_Command(SMP_Command):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part H - 3.6.3 Master Identification
|
||||
'''
|
||||
|
||||
ediv: int
|
||||
rand: bytes
|
||||
ediv: int = field(metadata=metadata(2))
|
||||
rand: bytes = field(metadata=metadata(8))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@SMP_Command.subclass([('identity_resolving_key', 16)])
|
||||
@SMP_Command.subclass
|
||||
@dataclass
|
||||
class SMP_Identity_Information_Command(SMP_Command):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part H - 3.6.4 Identity Information
|
||||
'''
|
||||
|
||||
identity_resolving_key: bytes
|
||||
identity_resolving_key: bytes = field(metadata=metadata(16))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@SMP_Command.subclass(
|
||||
[
|
||||
('addr_type', Address.ADDRESS_TYPE_SPEC),
|
||||
('bd_addr', Address.parse_address_preceded_by_type),
|
||||
]
|
||||
)
|
||||
@SMP_Command.subclass
|
||||
@dataclass
|
||||
class SMP_Identity_Address_Information_Command(SMP_Command):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part H - 3.6.5 Identity Address Information
|
||||
'''
|
||||
|
||||
addr_type: int
|
||||
bd_addr: Address
|
||||
addr_type: int = field(metadata=metadata(Address.ADDRESS_TYPE_SPEC))
|
||||
bd_addr: Address = field(metadata=metadata(Address.parse_address_preceded_by_type))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@SMP_Command.subclass([('signature_key', 16)])
|
||||
@SMP_Command.subclass
|
||||
@dataclass
|
||||
class SMP_Signing_Information_Command(SMP_Command):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part H - 3.6.6 Signing Information
|
||||
'''
|
||||
|
||||
signature_key: bytes
|
||||
signature_key: bytes = field(metadata=metadata(16))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@SMP_Command.subclass(
|
||||
[
|
||||
('auth_req', {'size': 1, 'mapper': SMP_Command.auth_req_str}),
|
||||
]
|
||||
)
|
||||
@SMP_Command.subclass
|
||||
@dataclass
|
||||
class SMP_Security_Request_Command(SMP_Command):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part H - 3.6.7 Security Request
|
||||
'''
|
||||
|
||||
auth_req: int
|
||||
auth_req: int = field(
|
||||
metadata=metadata({'size': 1, 'mapper': SMP_Command.auth_req_str})
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -762,7 +751,9 @@ class Session:
|
||||
|
||||
# OOB
|
||||
self.oob_data_flag = (
|
||||
1 if pairing_config.oob and pairing_config.oob.peer_data else 0
|
||||
1
|
||||
if pairing_config.oob and (not self.sc or pairing_config.oob.peer_data)
|
||||
else 0
|
||||
)
|
||||
|
||||
# Set up addresses
|
||||
@@ -890,8 +881,8 @@ class Session:
|
||||
if response:
|
||||
next_steps()
|
||||
return
|
||||
except Exception as error:
|
||||
logger.warning(f'exception while confirm: {error}')
|
||||
except Exception:
|
||||
logger.exception('exception while confirm')
|
||||
|
||||
self.send_pairing_failed(SMP_CONFIRM_VALUE_FAILED_ERROR)
|
||||
|
||||
@@ -909,8 +900,8 @@ class Session:
|
||||
if response:
|
||||
next_steps()
|
||||
return
|
||||
except Exception as error:
|
||||
logger.warning(f'exception while prompting: {error}')
|
||||
except Exception:
|
||||
logger.exception('exception while prompting')
|
||||
|
||||
self.send_pairing_failed(SMP_CONFIRM_VALUE_FAILED_ERROR)
|
||||
|
||||
@@ -927,8 +918,8 @@ class Session:
|
||||
return
|
||||
logger.debug(f'user input: {passkey}')
|
||||
next_steps(passkey)
|
||||
except Exception as error:
|
||||
logger.warning(f'exception while prompting: {error}')
|
||||
except Exception:
|
||||
logger.exception('exception while prompting')
|
||||
self.send_pairing_failed(SMP_PASSKEY_ENTRY_FAILED_ERROR)
|
||||
|
||||
self.connection.cancel_on_disconnection(prompt())
|
||||
@@ -944,7 +935,9 @@ class Session:
|
||||
self.tk = self.passkey.to_bytes(16, byteorder='little')
|
||||
logger.debug(f'TK from passkey = {self.tk.hex()}')
|
||||
|
||||
await self.pairing_config.delegate.display_number(self.passkey, digits=6)
|
||||
self.connection.cancel_on_disconnection(
|
||||
self.pairing_config.delegate.display_number(self.passkey, digits=6)
|
||||
)
|
||||
|
||||
def input_passkey(self, next_steps: Optional[Callable[[], None]] = None) -> None:
|
||||
# Prompt the user for the passkey displayed on the peer
|
||||
@@ -974,8 +967,8 @@ class Session:
|
||||
|
||||
try:
|
||||
self.connection.cancel_on_disconnection(display_passkey())
|
||||
except Exception as error:
|
||||
logger.warning(f'exception while displaying passkey: {error}')
|
||||
except Exception:
|
||||
logger.exception('exception while displaying passkey')
|
||||
else:
|
||||
self.input_passkey(next_steps)
|
||||
|
||||
@@ -1420,8 +1413,8 @@ class Session:
|
||||
if handler is not None:
|
||||
try:
|
||||
handler(command)
|
||||
except Exception as error:
|
||||
logger.exception(f'{color("!!! Exception in handler:", "red")} {error}')
|
||||
except Exception:
|
||||
logger.exception(color("!!! Exception in handler:", "red"))
|
||||
response = SMP_Pairing_Failed_Command(
|
||||
reason=SMP_UNSPECIFIED_REASON_ERROR
|
||||
)
|
||||
@@ -1442,8 +1435,8 @@ class Session:
|
||||
# Check if the request should proceed
|
||||
try:
|
||||
accepted = await self.pairing_config.delegate.accept()
|
||||
except Exception as error:
|
||||
logger.warning(f'exception while accepting: {error}')
|
||||
except Exception:
|
||||
logger.exception('exception while accepting')
|
||||
accepted = False
|
||||
if not accepted:
|
||||
logger.debug('pairing rejected by delegate')
|
||||
@@ -1567,11 +1560,12 @@ class Session:
|
||||
if self.pairing_method == PairingMethod.CTKD_OVER_CLASSIC:
|
||||
# Authentication is already done in SMP, so remote shall start keys distribution immediately
|
||||
return
|
||||
elif self.sc:
|
||||
|
||||
if self.sc:
|
||||
self.send_public_key_command()
|
||||
|
||||
if self.pairing_method == PairingMethod.PASSKEY:
|
||||
self.display_or_input_passkey()
|
||||
|
||||
self.send_public_key_command()
|
||||
else:
|
||||
if self.pairing_method == PairingMethod.PASSKEY:
|
||||
self.display_or_input_passkey(self.send_pairing_confirm_command)
|
||||
@@ -1844,10 +1838,10 @@ class Session:
|
||||
elif self.pairing_method == PairingMethod.PASSKEY:
|
||||
self.send_pairing_confirm_command()
|
||||
else:
|
||||
# Send our public key back to the initiator
|
||||
self.send_public_key_command()
|
||||
|
||||
def next_steps() -> None:
|
||||
# Send our public key back to the initiator
|
||||
self.send_public_key_command()
|
||||
|
||||
if self.pairing_method in (
|
||||
PairingMethod.JUST_WORKS,
|
||||
|
||||
+5
-5
@@ -12,21 +12,21 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import struct
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from contextlib import contextmanager
|
||||
from enum import IntEnum
|
||||
import logging
|
||||
import struct
|
||||
import datetime
|
||||
from typing import BinaryIO, Generator
|
||||
import os
|
||||
|
||||
from bumble import core
|
||||
from bumble.hci import HCI_COMMAND_PACKET, HCI_EVENT_PACKET
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user