bumble 0.0.195__py3-none-any.whl → 0.0.198__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bumble/_version.py +2 -2
- bumble/apps/auracast.py +351 -66
- bumble/apps/console.py +5 -20
- bumble/apps/device_info.py +230 -0
- bumble/apps/gatt_dump.py +4 -0
- bumble/apps/lea_unicast/app.py +16 -17
- bumble/at.py +12 -6
- bumble/avc.py +8 -5
- bumble/avctp.py +3 -2
- bumble/avdtp.py +5 -1
- bumble/avrcp.py +2 -1
- bumble/codecs.py +17 -13
- bumble/colors.py +6 -2
- bumble/core.py +37 -7
- bumble/device.py +382 -111
- bumble/drivers/rtk.py +13 -8
- bumble/gatt.py +6 -1
- bumble/gatt_client.py +10 -4
- bumble/hci.py +50 -25
- bumble/hid.py +24 -28
- bumble/host.py +4 -0
- bumble/l2cap.py +24 -17
- bumble/link.py +8 -3
- bumble/profiles/ascs.py +739 -0
- bumble/profiles/bap.py +1 -874
- bumble/profiles/bass.py +440 -0
- bumble/profiles/csip.py +4 -4
- bumble/profiles/gap.py +110 -0
- bumble/profiles/heart_rate_service.py +4 -3
- bumble/profiles/le_audio.py +43 -9
- bumble/profiles/mcp.py +448 -0
- bumble/profiles/pacs.py +210 -0
- bumble/profiles/tmap.py +89 -0
- bumble/rfcomm.py +4 -2
- bumble/sdp.py +13 -11
- bumble/smp.py +20 -8
- bumble/snoop.py +5 -4
- bumble/transport/__init__.py +8 -2
- bumble/transport/android_emulator.py +9 -3
- bumble/transport/android_netsim.py +9 -7
- bumble/transport/common.py +46 -18
- bumble/transport/pyusb.py +2 -2
- bumble/transport/unix.py +56 -0
- bumble/transport/usb.py +57 -46
- {bumble-0.0.195.dist-info → bumble-0.0.198.dist-info}/METADATA +41 -41
- {bumble-0.0.195.dist-info → bumble-0.0.198.dist-info}/RECORD +50 -42
- {bumble-0.0.195.dist-info → bumble-0.0.198.dist-info}/WHEEL +1 -1
- {bumble-0.0.195.dist-info → bumble-0.0.198.dist-info}/LICENSE +0 -0
- {bumble-0.0.195.dist-info → bumble-0.0.198.dist-info}/entry_points.txt +0 -0
- {bumble-0.0.195.dist-info → bumble-0.0.198.dist-info}/top_level.txt +0 -0
bumble/drivers/rtk.py
CHANGED
|
@@ -33,6 +33,7 @@ from typing import Tuple
|
|
|
33
33
|
import weakref
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
from bumble import core
|
|
36
37
|
from bumble.hci import (
|
|
37
38
|
hci_vendor_command_op_code,
|
|
38
39
|
STATUS_SPEC,
|
|
@@ -49,6 +50,10 @@ from bumble.drivers import common
|
|
|
49
50
|
logger = logging.getLogger(__name__)
|
|
50
51
|
|
|
51
52
|
|
|
53
|
+
class RtkFirmwareError(core.BaseBumbleError):
|
|
54
|
+
"""Error raised when RTK firmware initialization fails."""
|
|
55
|
+
|
|
56
|
+
|
|
52
57
|
# -----------------------------------------------------------------------------
|
|
53
58
|
# Constants
|
|
54
59
|
# -----------------------------------------------------------------------------
|
|
@@ -208,15 +213,15 @@ class Firmware:
|
|
|
208
213
|
extension_sig = bytes([0x51, 0x04, 0xFD, 0x77])
|
|
209
214
|
|
|
210
215
|
if not firmware.startswith(RTK_EPATCH_SIGNATURE):
|
|
211
|
-
raise
|
|
216
|
+
raise RtkFirmwareError("Firmware does not start with epatch signature")
|
|
212
217
|
|
|
213
218
|
if not firmware.endswith(extension_sig):
|
|
214
|
-
raise
|
|
219
|
+
raise RtkFirmwareError("Firmware does not end with extension sig")
|
|
215
220
|
|
|
216
221
|
# The firmware should start with a 14 byte header.
|
|
217
222
|
epatch_header_size = 14
|
|
218
223
|
if len(firmware) < epatch_header_size:
|
|
219
|
-
raise
|
|
224
|
+
raise RtkFirmwareError("Firmware too short")
|
|
220
225
|
|
|
221
226
|
# Look for the "project ID", starting from the end.
|
|
222
227
|
offset = len(firmware) - len(extension_sig)
|
|
@@ -230,7 +235,7 @@ class Firmware:
|
|
|
230
235
|
break
|
|
231
236
|
|
|
232
237
|
if length == 0:
|
|
233
|
-
raise
|
|
238
|
+
raise RtkFirmwareError("Invalid 0-length instruction")
|
|
234
239
|
|
|
235
240
|
if opcode == 0 and length == 1:
|
|
236
241
|
project_id = firmware[offset - 1]
|
|
@@ -239,7 +244,7 @@ class Firmware:
|
|
|
239
244
|
offset -= length
|
|
240
245
|
|
|
241
246
|
if project_id < 0:
|
|
242
|
-
raise
|
|
247
|
+
raise RtkFirmwareError("Project ID not found")
|
|
243
248
|
|
|
244
249
|
self.project_id = project_id
|
|
245
250
|
|
|
@@ -252,7 +257,7 @@ class Firmware:
|
|
|
252
257
|
# <PatchLength_1><PatchLength_2>...<PatchLength_N> (16 bits each)
|
|
253
258
|
# <PatchOffset_1><PatchOffset_2>...<PatchOffset_N> (32 bits each)
|
|
254
259
|
if epatch_header_size + 8 * num_patches > len(firmware):
|
|
255
|
-
raise
|
|
260
|
+
raise RtkFirmwareError("Firmware too short")
|
|
256
261
|
chip_id_table_offset = epatch_header_size
|
|
257
262
|
patch_length_table_offset = chip_id_table_offset + 2 * num_patches
|
|
258
263
|
patch_offset_table_offset = chip_id_table_offset + 4 * num_patches
|
|
@@ -266,7 +271,7 @@ class Firmware:
|
|
|
266
271
|
"<I", firmware, patch_offset_table_offset + 4 * patch_index
|
|
267
272
|
)
|
|
268
273
|
if patch_offset + patch_length > len(firmware):
|
|
269
|
-
raise
|
|
274
|
+
raise RtkFirmwareError("Firmware too short")
|
|
270
275
|
|
|
271
276
|
# Get the SVN version for the patch
|
|
272
277
|
(svn_version,) = struct.unpack_from(
|
|
@@ -645,7 +650,7 @@ class Driver(common.Driver):
|
|
|
645
650
|
):
|
|
646
651
|
return await self.download_for_rtl8723b()
|
|
647
652
|
|
|
648
|
-
raise
|
|
653
|
+
raise RtkFirmwareError("ROM not supported")
|
|
649
654
|
|
|
650
655
|
async def init_controller(self):
|
|
651
656
|
await self.download_firmware()
|
bumble/gatt.py
CHANGED
|
@@ -39,7 +39,7 @@ from typing import (
|
|
|
39
39
|
)
|
|
40
40
|
|
|
41
41
|
from bumble.colors import color
|
|
42
|
-
from bumble.core import UUID
|
|
42
|
+
from bumble.core import BaseBumbleError, UUID
|
|
43
43
|
from bumble.att import Attribute, AttributeValue
|
|
44
44
|
|
|
45
45
|
if TYPE_CHECKING:
|
|
@@ -320,6 +320,11 @@ def show_services(services: Iterable[Service]) -> None:
|
|
|
320
320
|
print(color(' ' + str(descriptor), 'green'))
|
|
321
321
|
|
|
322
322
|
|
|
323
|
+
# -----------------------------------------------------------------------------
|
|
324
|
+
class InvalidServiceError(BaseBumbleError):
|
|
325
|
+
"""The service is not compliant with the spec/profile"""
|
|
326
|
+
|
|
327
|
+
|
|
323
328
|
# -----------------------------------------------------------------------------
|
|
324
329
|
class Service(Attribute):
|
|
325
330
|
'''
|
bumble/gatt_client.py
CHANGED
|
@@ -253,7 +253,7 @@ class ProfileServiceProxy:
|
|
|
253
253
|
SERVICE_CLASS: Type[TemplateService]
|
|
254
254
|
|
|
255
255
|
@classmethod
|
|
256
|
-
def from_client(cls, client: Client) -> ProfileServiceProxy:
|
|
256
|
+
def from_client(cls, client: Client) -> Optional[ProfileServiceProxy]:
|
|
257
257
|
return ServiceProxy.from_client(cls, client, cls.SERVICE_CLASS.UUID)
|
|
258
258
|
|
|
259
259
|
|
|
@@ -283,6 +283,8 @@ class Client:
|
|
|
283
283
|
self.services = []
|
|
284
284
|
self.cached_values = {}
|
|
285
285
|
|
|
286
|
+
connection.on('disconnection', self.on_disconnection)
|
|
287
|
+
|
|
286
288
|
def send_gatt_pdu(self, pdu: bytes) -> None:
|
|
287
289
|
self.connection.send_l2cap_pdu(ATT_CID, pdu)
|
|
288
290
|
|
|
@@ -331,9 +333,9 @@ class Client:
|
|
|
331
333
|
async def request_mtu(self, mtu: int) -> int:
|
|
332
334
|
# Check the range
|
|
333
335
|
if mtu < ATT_DEFAULT_MTU:
|
|
334
|
-
raise
|
|
336
|
+
raise core.InvalidArgumentError(f'MTU must be >= {ATT_DEFAULT_MTU}')
|
|
335
337
|
if mtu > 0xFFFF:
|
|
336
|
-
raise
|
|
338
|
+
raise core.InvalidArgumentError('MTU must be <= 0xFFFF')
|
|
337
339
|
|
|
338
340
|
# We can only send one request per connection
|
|
339
341
|
if self.mtu_exchange_done:
|
|
@@ -405,7 +407,7 @@ class Client:
|
|
|
405
407
|
if not already_known:
|
|
406
408
|
self.services.append(service)
|
|
407
409
|
|
|
408
|
-
async def discover_services(self, uuids: Iterable[UUID] =
|
|
410
|
+
async def discover_services(self, uuids: Iterable[UUID] = ()) -> List[ServiceProxy]:
|
|
409
411
|
'''
|
|
410
412
|
See Vol 3, Part G - 4.4.1 Discover All Primary Services
|
|
411
413
|
'''
|
|
@@ -1072,6 +1074,10 @@ class Client:
|
|
|
1072
1074
|
)
|
|
1073
1075
|
)
|
|
1074
1076
|
|
|
1077
|
+
def on_disconnection(self, _) -> None:
|
|
1078
|
+
if self.pending_response and not self.pending_response.done():
|
|
1079
|
+
self.pending_response.cancel()
|
|
1080
|
+
|
|
1075
1081
|
def on_gatt_pdu(self, att_pdu: ATT_PDU) -> None:
|
|
1076
1082
|
logger.debug(
|
|
1077
1083
|
f'GATT Response to client: [0x{self.connection.handle:04X}] {att_pdu}'
|
bumble/hci.py
CHANGED
|
@@ -31,6 +31,8 @@ from bumble.core import (
|
|
|
31
31
|
BT_BR_EDR_TRANSPORT,
|
|
32
32
|
AdvertisingData,
|
|
33
33
|
DeviceClass,
|
|
34
|
+
InvalidArgumentError,
|
|
35
|
+
InvalidPacketError,
|
|
34
36
|
ProtocolError,
|
|
35
37
|
bit_flags_to_strings,
|
|
36
38
|
name_or_number,
|
|
@@ -92,14 +94,14 @@ def map_class_of_device(class_of_device):
|
|
|
92
94
|
)
|
|
93
95
|
|
|
94
96
|
|
|
95
|
-
def phy_list_to_bits(phys):
|
|
97
|
+
def phy_list_to_bits(phys: Optional[Iterable[int]]) -> int:
|
|
96
98
|
if phys is None:
|
|
97
99
|
return 0
|
|
98
100
|
|
|
99
101
|
phy_bits = 0
|
|
100
102
|
for phy in phys:
|
|
101
103
|
if phy not in HCI_LE_PHY_TYPE_TO_BIT:
|
|
102
|
-
raise
|
|
104
|
+
raise InvalidArgumentError('invalid PHY')
|
|
103
105
|
phy_bits |= 1 << HCI_LE_PHY_TYPE_TO_BIT[phy]
|
|
104
106
|
return phy_bits
|
|
105
107
|
|
|
@@ -1553,7 +1555,7 @@ class HCI_Object:
|
|
|
1553
1555
|
new_offset, field_value = field_type(data, offset)
|
|
1554
1556
|
return (field_value, new_offset - offset)
|
|
1555
1557
|
|
|
1556
|
-
raise
|
|
1558
|
+
raise InvalidArgumentError(f'unknown field type {field_type}')
|
|
1557
1559
|
|
|
1558
1560
|
@staticmethod
|
|
1559
1561
|
def dict_from_bytes(data, offset, fields):
|
|
@@ -1622,7 +1624,7 @@ class HCI_Object:
|
|
|
1622
1624
|
if 0 <= field_value <= 255:
|
|
1623
1625
|
field_bytes = bytes([field_value])
|
|
1624
1626
|
else:
|
|
1625
|
-
raise
|
|
1627
|
+
raise InvalidArgumentError('value too large for *-typed field')
|
|
1626
1628
|
else:
|
|
1627
1629
|
field_bytes = bytes(field_value)
|
|
1628
1630
|
elif field_type == 'v':
|
|
@@ -1641,7 +1643,9 @@ class HCI_Object:
|
|
|
1641
1643
|
elif len(field_bytes) > field_type:
|
|
1642
1644
|
field_bytes = field_bytes[:field_type]
|
|
1643
1645
|
else:
|
|
1644
|
-
raise
|
|
1646
|
+
raise InvalidArgumentError(
|
|
1647
|
+
f"don't know how to serialize type {type(field_value)}"
|
|
1648
|
+
)
|
|
1645
1649
|
|
|
1646
1650
|
return field_bytes
|
|
1647
1651
|
|
|
@@ -1835,6 +1839,12 @@ class Address:
|
|
|
1835
1839
|
data, offset, Address.PUBLIC_DEVICE_ADDRESS
|
|
1836
1840
|
)
|
|
1837
1841
|
|
|
1842
|
+
@staticmethod
|
|
1843
|
+
def parse_random_address(data, offset):
|
|
1844
|
+
return Address.parse_address_with_type(
|
|
1845
|
+
data, offset, Address.RANDOM_DEVICE_ADDRESS
|
|
1846
|
+
)
|
|
1847
|
+
|
|
1838
1848
|
@staticmethod
|
|
1839
1849
|
def parse_address_with_type(data, offset, address_type):
|
|
1840
1850
|
return offset + 6, Address(data[offset : offset + 6], address_type)
|
|
@@ -1905,7 +1915,7 @@ class Address:
|
|
|
1905
1915
|
self.address_bytes = bytes(reversed(bytes.fromhex(address)))
|
|
1906
1916
|
|
|
1907
1917
|
if len(self.address_bytes) != 6:
|
|
1908
|
-
raise
|
|
1918
|
+
raise InvalidArgumentError('invalid address length')
|
|
1909
1919
|
|
|
1910
1920
|
self.address_type = address_type
|
|
1911
1921
|
|
|
@@ -1961,7 +1971,8 @@ class Address:
|
|
|
1961
1971
|
|
|
1962
1972
|
def __eq__(self, other):
|
|
1963
1973
|
return (
|
|
1964
|
-
|
|
1974
|
+
isinstance(other, Address)
|
|
1975
|
+
and self.address_bytes == other.address_bytes
|
|
1965
1976
|
and self.is_public == other.is_public
|
|
1966
1977
|
)
|
|
1967
1978
|
|
|
@@ -2108,7 +2119,7 @@ class HCI_Command(HCI_Packet):
|
|
|
2108
2119
|
op_code, length = struct.unpack_from('<HB', packet, 1)
|
|
2109
2120
|
parameters = packet[4:]
|
|
2110
2121
|
if len(parameters) != length:
|
|
2111
|
-
raise
|
|
2122
|
+
raise InvalidPacketError('invalid packet length')
|
|
2112
2123
|
|
|
2113
2124
|
# Look for a registered class
|
|
2114
2125
|
cls = HCI_Command.command_classes.get(op_code)
|
|
@@ -4518,18 +4529,6 @@ class HCI_LE_Periodic_Advertising_Terminate_Sync_Command(HCI_Command):
|
|
|
4518
4529
|
'''
|
|
4519
4530
|
|
|
4520
4531
|
|
|
4521
|
-
# -----------------------------------------------------------------------------
|
|
4522
|
-
@HCI_Command.command([('sync_handle', 2), ('enable', 1)])
|
|
4523
|
-
class HCI_LE_Set_Periodic_Advertising_Receive_Enable_Command(HCI_Command):
|
|
4524
|
-
'''
|
|
4525
|
-
See Bluetooth spec @ 7.8.88 LE Set Periodic Advertising Receive Enable Command
|
|
4526
|
-
'''
|
|
4527
|
-
|
|
4528
|
-
class Enable(enum.IntFlag):
|
|
4529
|
-
REPORTING_ENABLED = 1 << 0
|
|
4530
|
-
DUPLICATE_FILTERING_ENABLED = 1 << 1
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
4532
|
# -----------------------------------------------------------------------------
|
|
4534
4533
|
@HCI_Command.command(
|
|
4535
4534
|
[
|
|
@@ -4565,6 +4564,32 @@ class HCI_LE_Set_Privacy_Mode_Command(HCI_Command):
|
|
|
4565
4564
|
return name_or_number(cls.PRIVACY_MODE_NAMES, privacy_mode)
|
|
4566
4565
|
|
|
4567
4566
|
|
|
4567
|
+
# -----------------------------------------------------------------------------
|
|
4568
|
+
@HCI_Command.command([('sync_handle', 2), ('enable', 1)])
|
|
4569
|
+
class HCI_LE_Set_Periodic_Advertising_Receive_Enable_Command(HCI_Command):
|
|
4570
|
+
'''
|
|
4571
|
+
See Bluetooth spec @ 7.8.88 LE Set Periodic Advertising Receive Enable Command
|
|
4572
|
+
'''
|
|
4573
|
+
|
|
4574
|
+
class Enable(enum.IntFlag):
|
|
4575
|
+
REPORTING_ENABLED = 1 << 0
|
|
4576
|
+
DUPLICATE_FILTERING_ENABLED = 1 << 1
|
|
4577
|
+
|
|
4578
|
+
|
|
4579
|
+
# -----------------------------------------------------------------------------
|
|
4580
|
+
@HCI_Command.command(
|
|
4581
|
+
fields=[('connection_handle', 2), ('service_data', 2), ('sync_handle', 2)],
|
|
4582
|
+
return_parameters_fields=[
|
|
4583
|
+
('status', STATUS_SPEC),
|
|
4584
|
+
('connection_handle', 2),
|
|
4585
|
+
],
|
|
4586
|
+
)
|
|
4587
|
+
class HCI_LE_Periodic_Advertising_Sync_Transfer_Command(HCI_Command):
|
|
4588
|
+
'''
|
|
4589
|
+
See Bluetooth spec @ 7.8.89 LE Periodic Advertising Sync Transfer Command
|
|
4590
|
+
'''
|
|
4591
|
+
|
|
4592
|
+
|
|
4568
4593
|
# -----------------------------------------------------------------------------
|
|
4569
4594
|
@HCI_Command.command(
|
|
4570
4595
|
fields=[
|
|
@@ -4807,7 +4832,7 @@ class HCI_Event(HCI_Packet):
|
|
|
4807
4832
|
length = packet[2]
|
|
4808
4833
|
parameters = packet[3:]
|
|
4809
4834
|
if len(parameters) != length:
|
|
4810
|
-
raise
|
|
4835
|
+
raise InvalidPacketError('invalid packet length')
|
|
4811
4836
|
|
|
4812
4837
|
cls: Any
|
|
4813
4838
|
if event_code == HCI_LE_META_EVENT:
|
|
@@ -5174,8 +5199,8 @@ class HCI_LE_Data_Length_Change_Event(HCI_LE_Meta_Event):
|
|
|
5174
5199
|
),
|
|
5175
5200
|
('peer_address_type', Address.ADDRESS_TYPE_SPEC),
|
|
5176
5201
|
('peer_address', Address.parse_address_preceded_by_type),
|
|
5177
|
-
('local_resolvable_private_address', Address.
|
|
5178
|
-
('peer_resolvable_private_address', Address.
|
|
5202
|
+
('local_resolvable_private_address', Address.parse_random_address),
|
|
5203
|
+
('peer_resolvable_private_address', Address.parse_random_address),
|
|
5179
5204
|
('connection_interval', 2),
|
|
5180
5205
|
('peripheral_latency', 2),
|
|
5181
5206
|
('supervision_timeout', 2),
|
|
@@ -6342,7 +6367,7 @@ class HCI_AclDataPacket(HCI_Packet):
|
|
|
6342
6367
|
bc_flag = (h >> 14) & 3
|
|
6343
6368
|
data = packet[5:]
|
|
6344
6369
|
if len(data) != data_total_length:
|
|
6345
|
-
raise
|
|
6370
|
+
raise InvalidPacketError('invalid packet length')
|
|
6346
6371
|
return HCI_AclDataPacket(
|
|
6347
6372
|
connection_handle, pb_flag, bc_flag, data_total_length, data
|
|
6348
6373
|
)
|
|
@@ -6390,7 +6415,7 @@ class HCI_SynchronousDataPacket(HCI_Packet):
|
|
|
6390
6415
|
packet_status = (h >> 12) & 0b11
|
|
6391
6416
|
data = packet[4:]
|
|
6392
6417
|
if len(data) != data_total_length:
|
|
6393
|
-
raise
|
|
6418
|
+
raise InvalidPacketError(
|
|
6394
6419
|
f'invalid packet length {len(data)} != {data_total_length}'
|
|
6395
6420
|
)
|
|
6396
6421
|
return HCI_SynchronousDataPacket(
|
bumble/hid.py
CHANGED
|
@@ -23,13 +23,12 @@ import struct
|
|
|
23
23
|
|
|
24
24
|
from abc import ABC, abstractmethod
|
|
25
25
|
from pyee import EventEmitter
|
|
26
|
-
from typing import Optional, Callable
|
|
26
|
+
from typing import Optional, Callable
|
|
27
27
|
from typing_extensions import override
|
|
28
28
|
|
|
29
29
|
from bumble import l2cap, device
|
|
30
|
-
from bumble.colors import color
|
|
31
30
|
from bumble.core import InvalidStateError, ProtocolError
|
|
32
|
-
from .hci import Address
|
|
31
|
+
from bumble.hci import Address
|
|
33
32
|
|
|
34
33
|
|
|
35
34
|
# -----------------------------------------------------------------------------
|
|
@@ -220,31 +219,27 @@ class HID(ABC, EventEmitter):
|
|
|
220
219
|
async def connect_control_channel(self) -> None:
|
|
221
220
|
# Create a new L2CAP connection - control channel
|
|
222
221
|
try:
|
|
223
|
-
|
|
222
|
+
channel = await self.device.l2cap_channel_manager.connect(
|
|
224
223
|
self.connection, HID_CONTROL_PSM
|
|
225
224
|
)
|
|
225
|
+
channel.sink = self.on_ctrl_pdu
|
|
226
|
+
self.l2cap_ctrl_channel = channel
|
|
226
227
|
except ProtocolError:
|
|
227
228
|
logging.exception(f'L2CAP connection failed.')
|
|
228
229
|
raise
|
|
229
230
|
|
|
230
|
-
assert self.l2cap_ctrl_channel is not None
|
|
231
|
-
# Become a sink for the L2CAP channel
|
|
232
|
-
self.l2cap_ctrl_channel.sink = self.on_ctrl_pdu
|
|
233
|
-
|
|
234
231
|
async def connect_interrupt_channel(self) -> None:
|
|
235
232
|
# Create a new L2CAP connection - interrupt channel
|
|
236
233
|
try:
|
|
237
|
-
|
|
234
|
+
channel = await self.device.l2cap_channel_manager.connect(
|
|
238
235
|
self.connection, HID_INTERRUPT_PSM
|
|
239
236
|
)
|
|
237
|
+
channel.sink = self.on_intr_pdu
|
|
238
|
+
self.l2cap_intr_channel = channel
|
|
240
239
|
except ProtocolError:
|
|
241
240
|
logging.exception(f'L2CAP connection failed.')
|
|
242
241
|
raise
|
|
243
242
|
|
|
244
|
-
assert self.l2cap_intr_channel is not None
|
|
245
|
-
# Become a sink for the L2CAP channel
|
|
246
|
-
self.l2cap_intr_channel.sink = self.on_intr_pdu
|
|
247
|
-
|
|
248
243
|
async def disconnect_interrupt_channel(self) -> None:
|
|
249
244
|
if self.l2cap_intr_channel is None:
|
|
250
245
|
raise InvalidStateError('invalid state')
|
|
@@ -334,17 +329,18 @@ class Device(HID):
|
|
|
334
329
|
ERR_INVALID_PARAMETER = 0x04
|
|
335
330
|
SUCCESS = 0xFF
|
|
336
331
|
|
|
332
|
+
@dataclass
|
|
337
333
|
class GetSetStatus:
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
334
|
+
data: bytes = b''
|
|
335
|
+
status: int = 0
|
|
336
|
+
|
|
337
|
+
get_report_cb: Optional[Callable[[int, int, int], GetSetStatus]] = None
|
|
338
|
+
set_report_cb: Optional[Callable[[int, int, int, bytes], GetSetStatus]] = None
|
|
339
|
+
get_protocol_cb: Optional[Callable[[], GetSetStatus]] = None
|
|
340
|
+
set_protocol_cb: Optional[Callable[[int], GetSetStatus]] = None
|
|
341
341
|
|
|
342
342
|
def __init__(self, device: device.Device) -> None:
|
|
343
343
|
super().__init__(device, HID.Role.DEVICE)
|
|
344
|
-
get_report_cb: Optional[Callable[[int, int, int], None]] = None
|
|
345
|
-
set_report_cb: Optional[Callable[[int, int, int, bytes], None]] = None
|
|
346
|
-
get_protocol_cb: Optional[Callable[[], None]] = None
|
|
347
|
-
set_protocol_cb: Optional[Callable[[int], None]] = None
|
|
348
344
|
|
|
349
345
|
@override
|
|
350
346
|
def on_ctrl_pdu(self, pdu: bytes) -> None:
|
|
@@ -410,7 +406,6 @@ class Device(HID):
|
|
|
410
406
|
buffer_size = 0
|
|
411
407
|
|
|
412
408
|
ret = self.get_report_cb(report_id, report_type, buffer_size)
|
|
413
|
-
assert ret is not None
|
|
414
409
|
if ret.status == self.GetSetReturn.FAILURE:
|
|
415
410
|
self.send_handshake_message(Message.Handshake.ERR_UNKNOWN)
|
|
416
411
|
elif ret.status == self.GetSetReturn.SUCCESS:
|
|
@@ -428,7 +423,9 @@ class Device(HID):
|
|
|
428
423
|
elif ret.status == self.GetSetReturn.ERR_UNSUPPORTED_REQUEST:
|
|
429
424
|
self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST)
|
|
430
425
|
|
|
431
|
-
def register_get_report_cb(
|
|
426
|
+
def register_get_report_cb(
|
|
427
|
+
self, cb: Callable[[int, int, int], Device.GetSetStatus]
|
|
428
|
+
) -> None:
|
|
432
429
|
self.get_report_cb = cb
|
|
433
430
|
logger.debug("GetReport callback registered successfully")
|
|
434
431
|
|
|
@@ -442,7 +439,6 @@ class Device(HID):
|
|
|
442
439
|
report_data = pdu[2:]
|
|
443
440
|
report_size = len(report_data) + 1
|
|
444
441
|
ret = self.set_report_cb(report_id, report_type, report_size, report_data)
|
|
445
|
-
assert ret is not None
|
|
446
442
|
if ret.status == self.GetSetReturn.SUCCESS:
|
|
447
443
|
self.send_handshake_message(Message.Handshake.SUCCESSFUL)
|
|
448
444
|
elif ret.status == self.GetSetReturn.ERR_INVALID_PARAMETER:
|
|
@@ -453,7 +449,7 @@ class Device(HID):
|
|
|
453
449
|
self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST)
|
|
454
450
|
|
|
455
451
|
def register_set_report_cb(
|
|
456
|
-
self, cb: Callable[[int, int, int, bytes],
|
|
452
|
+
self, cb: Callable[[int, int, int, bytes], Device.GetSetStatus]
|
|
457
453
|
) -> None:
|
|
458
454
|
self.set_report_cb = cb
|
|
459
455
|
logger.debug("SetReport callback registered successfully")
|
|
@@ -464,13 +460,12 @@ class Device(HID):
|
|
|
464
460
|
self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST)
|
|
465
461
|
return
|
|
466
462
|
ret = self.get_protocol_cb()
|
|
467
|
-
assert ret is not None
|
|
468
463
|
if ret.status == self.GetSetReturn.SUCCESS:
|
|
469
464
|
self.send_control_data(Message.ReportType.OTHER_REPORT, ret.data)
|
|
470
465
|
else:
|
|
471
466
|
self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST)
|
|
472
467
|
|
|
473
|
-
def register_get_protocol_cb(self, cb: Callable[[],
|
|
468
|
+
def register_get_protocol_cb(self, cb: Callable[[], Device.GetSetStatus]) -> None:
|
|
474
469
|
self.get_protocol_cb = cb
|
|
475
470
|
logger.debug("GetProtocol callback registered successfully")
|
|
476
471
|
|
|
@@ -480,13 +475,14 @@ class Device(HID):
|
|
|
480
475
|
self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST)
|
|
481
476
|
return
|
|
482
477
|
ret = self.set_protocol_cb(pdu[0] & 0x01)
|
|
483
|
-
assert ret is not None
|
|
484
478
|
if ret.status == self.GetSetReturn.SUCCESS:
|
|
485
479
|
self.send_handshake_message(Message.Handshake.SUCCESSFUL)
|
|
486
480
|
else:
|
|
487
481
|
self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST)
|
|
488
482
|
|
|
489
|
-
def register_set_protocol_cb(
|
|
483
|
+
def register_set_protocol_cb(
|
|
484
|
+
self, cb: Callable[[int], Device.GetSetStatus]
|
|
485
|
+
) -> None:
|
|
490
486
|
self.set_protocol_cb = cb
|
|
491
487
|
logger.debug("SetProtocol callback registered successfully")
|
|
492
488
|
|
bumble/host.py
CHANGED
|
@@ -772,6 +772,8 @@ class Host(AbortableEventEmitter):
|
|
|
772
772
|
event.connection_handle,
|
|
773
773
|
BT_LE_TRANSPORT,
|
|
774
774
|
event.peer_address,
|
|
775
|
+
getattr(event, 'local_resolvable_private_address', None),
|
|
776
|
+
getattr(event, 'peer_resolvable_private_address', None),
|
|
775
777
|
event.role,
|
|
776
778
|
connection_parameters,
|
|
777
779
|
)
|
|
@@ -817,6 +819,8 @@ class Host(AbortableEventEmitter):
|
|
|
817
819
|
event.bd_addr,
|
|
818
820
|
None,
|
|
819
821
|
None,
|
|
822
|
+
None,
|
|
823
|
+
None,
|
|
820
824
|
)
|
|
821
825
|
else:
|
|
822
826
|
logger.debug(f'### BR/EDR CONNECTION FAILED: {event.status}')
|
bumble/l2cap.py
CHANGED
|
@@ -41,7 +41,14 @@ from typing import (
|
|
|
41
41
|
|
|
42
42
|
from .utils import deprecated
|
|
43
43
|
from .colors import color
|
|
44
|
-
from .core import
|
|
44
|
+
from .core import (
|
|
45
|
+
BT_CENTRAL_ROLE,
|
|
46
|
+
InvalidStateError,
|
|
47
|
+
InvalidArgumentError,
|
|
48
|
+
InvalidPacketError,
|
|
49
|
+
OutOfResourcesError,
|
|
50
|
+
ProtocolError,
|
|
51
|
+
)
|
|
45
52
|
from .hci import (
|
|
46
53
|
HCI_LE_Connection_Update_Command,
|
|
47
54
|
HCI_Object,
|
|
@@ -189,17 +196,17 @@ class LeCreditBasedChannelSpec:
|
|
|
189
196
|
self.max_credits < 1
|
|
190
197
|
or self.max_credits > L2CAP_LE_CREDIT_BASED_CONNECTION_MAX_CREDITS
|
|
191
198
|
):
|
|
192
|
-
raise
|
|
199
|
+
raise InvalidArgumentError('max credits out of range')
|
|
193
200
|
if (
|
|
194
201
|
self.mtu < L2CAP_LE_CREDIT_BASED_CONNECTION_MIN_MTU
|
|
195
202
|
or self.mtu > L2CAP_LE_CREDIT_BASED_CONNECTION_MAX_MTU
|
|
196
203
|
):
|
|
197
|
-
raise
|
|
204
|
+
raise InvalidArgumentError('MTU out of range')
|
|
198
205
|
if (
|
|
199
206
|
self.mps < L2CAP_LE_CREDIT_BASED_CONNECTION_MIN_MPS
|
|
200
207
|
or self.mps > L2CAP_LE_CREDIT_BASED_CONNECTION_MAX_MPS
|
|
201
208
|
):
|
|
202
|
-
raise
|
|
209
|
+
raise InvalidArgumentError('MPS out of range')
|
|
203
210
|
|
|
204
211
|
|
|
205
212
|
class L2CAP_PDU:
|
|
@@ -211,7 +218,7 @@ class L2CAP_PDU:
|
|
|
211
218
|
def from_bytes(data: bytes) -> L2CAP_PDU:
|
|
212
219
|
# Check parameters
|
|
213
220
|
if len(data) < 4:
|
|
214
|
-
raise
|
|
221
|
+
raise InvalidPacketError('not enough data for L2CAP header')
|
|
215
222
|
|
|
216
223
|
_, l2cap_pdu_cid = struct.unpack_from('<HH', data, 0)
|
|
217
224
|
l2cap_pdu_payload = data[4:]
|
|
@@ -816,7 +823,7 @@ class ClassicChannel(EventEmitter):
|
|
|
816
823
|
|
|
817
824
|
# Check that we can start a new connection
|
|
818
825
|
if self.connection_result:
|
|
819
|
-
raise
|
|
826
|
+
raise InvalidStateError('connection already pending')
|
|
820
827
|
|
|
821
828
|
self._change_state(self.State.WAIT_CONNECT_RSP)
|
|
822
829
|
self.send_control_frame(
|
|
@@ -1129,7 +1136,7 @@ class LeCreditBasedChannel(EventEmitter):
|
|
|
1129
1136
|
# Check that we can start a new connection
|
|
1130
1137
|
identifier = self.manager.next_identifier(self.connection)
|
|
1131
1138
|
if identifier in self.manager.le_coc_requests:
|
|
1132
|
-
raise
|
|
1139
|
+
raise InvalidStateError('too many concurrent connection requests')
|
|
1133
1140
|
|
|
1134
1141
|
self._change_state(self.State.CONNECTING)
|
|
1135
1142
|
request = L2CAP_LE_Credit_Based_Connection_Request(
|
|
@@ -1516,7 +1523,7 @@ class ChannelManager:
|
|
|
1516
1523
|
if cid not in channels:
|
|
1517
1524
|
return cid
|
|
1518
1525
|
|
|
1519
|
-
raise
|
|
1526
|
+
raise OutOfResourcesError('no free CID available')
|
|
1520
1527
|
|
|
1521
1528
|
@staticmethod
|
|
1522
1529
|
def find_free_le_cid(channels: Iterable[int]) -> int:
|
|
@@ -1529,7 +1536,7 @@ class ChannelManager:
|
|
|
1529
1536
|
if cid not in channels:
|
|
1530
1537
|
return cid
|
|
1531
1538
|
|
|
1532
|
-
raise
|
|
1539
|
+
raise OutOfResourcesError('no free CID')
|
|
1533
1540
|
|
|
1534
1541
|
def next_identifier(self, connection: Connection) -> int:
|
|
1535
1542
|
identifier = (self.identifiers.setdefault(connection.handle, 0) + 1) % 256
|
|
@@ -1576,15 +1583,15 @@ class ChannelManager:
|
|
|
1576
1583
|
else:
|
|
1577
1584
|
# Check that the PSM isn't already in use
|
|
1578
1585
|
if spec.psm in self.servers:
|
|
1579
|
-
raise
|
|
1586
|
+
raise InvalidArgumentError('PSM already in use')
|
|
1580
1587
|
|
|
1581
1588
|
# Check that the PSM is valid
|
|
1582
1589
|
if spec.psm % 2 == 0:
|
|
1583
|
-
raise
|
|
1590
|
+
raise InvalidArgumentError('invalid PSM (not odd)')
|
|
1584
1591
|
check = spec.psm >> 8
|
|
1585
1592
|
while check:
|
|
1586
1593
|
if check % 2 != 0:
|
|
1587
|
-
raise
|
|
1594
|
+
raise InvalidArgumentError('invalid PSM')
|
|
1588
1595
|
check >>= 8
|
|
1589
1596
|
|
|
1590
1597
|
self.servers[spec.psm] = ClassicChannelServer(self, spec.psm, handler, spec.mtu)
|
|
@@ -1626,7 +1633,7 @@ class ChannelManager:
|
|
|
1626
1633
|
else:
|
|
1627
1634
|
# Check that the PSM isn't already in use
|
|
1628
1635
|
if spec.psm in self.le_coc_servers:
|
|
1629
|
-
raise
|
|
1636
|
+
raise InvalidArgumentError('PSM already in use')
|
|
1630
1637
|
|
|
1631
1638
|
self.le_coc_servers[spec.psm] = LeCreditBasedChannelServer(
|
|
1632
1639
|
self,
|
|
@@ -2154,10 +2161,10 @@ class ChannelManager:
|
|
|
2154
2161
|
connection_channels = self.channels.setdefault(connection.handle, {})
|
|
2155
2162
|
source_cid = self.find_free_le_cid(connection_channels)
|
|
2156
2163
|
if source_cid is None: # Should never happen!
|
|
2157
|
-
raise
|
|
2164
|
+
raise OutOfResourcesError('all CIDs already in use')
|
|
2158
2165
|
|
|
2159
2166
|
if spec.psm is None:
|
|
2160
|
-
raise
|
|
2167
|
+
raise InvalidArgumentError('PSM cannot be None')
|
|
2161
2168
|
|
|
2162
2169
|
# Create the channel
|
|
2163
2170
|
logger.debug(f'creating coc channel with cid={source_cid} for psm {spec.psm}')
|
|
@@ -2206,10 +2213,10 @@ class ChannelManager:
|
|
|
2206
2213
|
connection_channels = self.channels.setdefault(connection.handle, {})
|
|
2207
2214
|
source_cid = self.find_free_br_edr_cid(connection_channels)
|
|
2208
2215
|
if source_cid is None: # Should never happen!
|
|
2209
|
-
raise
|
|
2216
|
+
raise OutOfResourcesError('all CIDs already in use')
|
|
2210
2217
|
|
|
2211
2218
|
if spec.psm is None:
|
|
2212
|
-
raise
|
|
2219
|
+
raise InvalidArgumentError('PSM cannot be None')
|
|
2213
2220
|
|
|
2214
2221
|
# Create the channel
|
|
2215
2222
|
logger.debug(
|
bumble/link.py
CHANGED
|
@@ -19,7 +19,12 @@ import logging
|
|
|
19
19
|
import asyncio
|
|
20
20
|
from functools import partial
|
|
21
21
|
|
|
22
|
-
from bumble.core import
|
|
22
|
+
from bumble.core import (
|
|
23
|
+
BT_PERIPHERAL_ROLE,
|
|
24
|
+
BT_BR_EDR_TRANSPORT,
|
|
25
|
+
BT_LE_TRANSPORT,
|
|
26
|
+
InvalidStateError,
|
|
27
|
+
)
|
|
23
28
|
from bumble.colors import color
|
|
24
29
|
from bumble.hci import (
|
|
25
30
|
Address,
|
|
@@ -405,12 +410,12 @@ class RemoteLink:
|
|
|
405
410
|
|
|
406
411
|
def add_controller(self, controller):
|
|
407
412
|
if self.controller:
|
|
408
|
-
raise
|
|
413
|
+
raise InvalidStateError('controller already set')
|
|
409
414
|
self.controller = controller
|
|
410
415
|
|
|
411
416
|
def remove_controller(self, controller):
|
|
412
417
|
if self.controller != controller:
|
|
413
|
-
raise
|
|
418
|
+
raise InvalidStateError('controller mismatch')
|
|
414
419
|
self.controller = None
|
|
415
420
|
|
|
416
421
|
def get_pending_connection(self):
|