bumble 0.0.207__py3-none-any.whl → 0.0.208__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 +9 -4
- bumble/apps/auracast.py +29 -35
- bumble/apps/bench.py +13 -10
- bumble/apps/console.py +19 -12
- bumble/apps/gg_bridge.py +1 -1
- bumble/att.py +51 -37
- bumble/core.py +301 -155
- bumble/device.py +102 -56
- bumble/gatt.py +9 -228
- bumble/gatt_adapters.py +374 -0
- bumble/gatt_client.py +38 -31
- bumble/gatt_server.py +5 -5
- bumble/host.py +11 -5
- bumble/pairing.py +5 -5
- bumble/pandora/host.py +7 -12
- bumble/profiles/aics.py +33 -23
- bumble/profiles/ascs.py +2 -1
- bumble/profiles/asha.py +11 -9
- bumble/profiles/bass.py +8 -5
- bumble/profiles/battery_service.py +13 -3
- bumble/profiles/device_information_service.py +16 -14
- bumble/profiles/gap.py +12 -8
- bumble/profiles/gatt_service.py +1 -0
- bumble/profiles/gmap.py +16 -11
- bumble/profiles/hap.py +8 -6
- bumble/profiles/heart_rate_service.py +20 -4
- bumble/profiles/mcp.py +11 -9
- bumble/profiles/pacs.py +37 -24
- bumble/profiles/tmap.py +6 -4
- bumble/profiles/vcs.py +6 -5
- bumble/profiles/vocs.py +49 -41
- bumble/utils.py +10 -0
- {bumble-0.0.207.dist-info → bumble-0.0.208.dist-info}/METADATA +3 -3
- {bumble-0.0.207.dist-info → bumble-0.0.208.dist-info}/RECORD +38 -37
- {bumble-0.0.207.dist-info → bumble-0.0.208.dist-info}/LICENSE +0 -0
- {bumble-0.0.207.dist-info → bumble-0.0.208.dist-info}/WHEEL +0 -0
- {bumble-0.0.207.dist-info → bumble-0.0.208.dist-info}/entry_points.txt +0 -0
- {bumble-0.0.207.dist-info → bumble-0.0.208.dist-info}/top_level.txt +0 -0
bumble/gatt_adapters.py
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
# Copyright 2025 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
# -----------------------------------------------------------------------------
|
|
16
|
+
# GATT - Type Adapters
|
|
17
|
+
# -----------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
# -----------------------------------------------------------------------------
|
|
20
|
+
# Imports
|
|
21
|
+
# -----------------------------------------------------------------------------
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
import struct
|
|
24
|
+
from typing import (
|
|
25
|
+
Any,
|
|
26
|
+
Callable,
|
|
27
|
+
Generic,
|
|
28
|
+
Iterable,
|
|
29
|
+
Literal,
|
|
30
|
+
Optional,
|
|
31
|
+
Type,
|
|
32
|
+
TypeVar,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
from bumble.core import InvalidOperationError
|
|
36
|
+
from bumble.gatt import Characteristic
|
|
37
|
+
from bumble.gatt_client import CharacteristicProxy
|
|
38
|
+
from bumble.utils import ByteSerializable, IntConvertible
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# -----------------------------------------------------------------------------
|
|
42
|
+
# Typing
|
|
43
|
+
# -----------------------------------------------------------------------------
|
|
44
|
+
_T = TypeVar('_T')
|
|
45
|
+
_T2 = TypeVar('_T2', bound=ByteSerializable)
|
|
46
|
+
_T3 = TypeVar('_T3', bound=IntConvertible)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# -----------------------------------------------------------------------------
|
|
50
|
+
class CharacteristicAdapter(Characteristic, Generic[_T]):
|
|
51
|
+
'''Base class for GATT Characteristic adapters.'''
|
|
52
|
+
|
|
53
|
+
def __init__(self, characteristic: Characteristic) -> None:
|
|
54
|
+
super().__init__(
|
|
55
|
+
characteristic.uuid,
|
|
56
|
+
characteristic.properties,
|
|
57
|
+
characteristic.permissions,
|
|
58
|
+
characteristic.value,
|
|
59
|
+
characteristic.descriptors,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# -----------------------------------------------------------------------------
|
|
64
|
+
class CharacteristicProxyAdapter(CharacteristicProxy[_T]):
|
|
65
|
+
'''Base class for GATT CharacteristicProxy adapters.'''
|
|
66
|
+
|
|
67
|
+
def __init__(self, characteristic_proxy: CharacteristicProxy):
|
|
68
|
+
super().__init__(
|
|
69
|
+
characteristic_proxy.client,
|
|
70
|
+
characteristic_proxy.handle,
|
|
71
|
+
characteristic_proxy.end_group_handle,
|
|
72
|
+
characteristic_proxy.uuid,
|
|
73
|
+
characteristic_proxy.properties,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# -----------------------------------------------------------------------------
|
|
78
|
+
class DelegatedCharacteristicAdapter(CharacteristicAdapter[_T]):
|
|
79
|
+
'''
|
|
80
|
+
Adapter that converts bytes values using an encode and/or a decode function.
|
|
81
|
+
'''
|
|
82
|
+
|
|
83
|
+
def __init__(
|
|
84
|
+
self,
|
|
85
|
+
characteristic: Characteristic,
|
|
86
|
+
encode: Optional[Callable[[_T], bytes]] = None,
|
|
87
|
+
decode: Optional[Callable[[bytes], _T]] = None,
|
|
88
|
+
):
|
|
89
|
+
super().__init__(characteristic)
|
|
90
|
+
self.encode = encode
|
|
91
|
+
self.decode = decode
|
|
92
|
+
|
|
93
|
+
def encode_value(self, value: _T) -> bytes:
|
|
94
|
+
if self.encode is None:
|
|
95
|
+
raise InvalidOperationError('delegated adapter does not have an encoder')
|
|
96
|
+
return self.encode(value)
|
|
97
|
+
|
|
98
|
+
def decode_value(self, value: bytes) -> _T:
|
|
99
|
+
if self.decode is None:
|
|
100
|
+
raise InvalidOperationError('delegate adapter does not have a decoder')
|
|
101
|
+
return self.decode(value)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# -----------------------------------------------------------------------------
|
|
105
|
+
class DelegatedCharacteristicProxyAdapter(CharacteristicProxyAdapter[_T]):
|
|
106
|
+
'''
|
|
107
|
+
Adapter that converts bytes values using an encode and a decode function.
|
|
108
|
+
'''
|
|
109
|
+
|
|
110
|
+
def __init__(
|
|
111
|
+
self,
|
|
112
|
+
characteristic_proxy: CharacteristicProxy,
|
|
113
|
+
encode: Optional[Callable[[_T], bytes]] = None,
|
|
114
|
+
decode: Optional[Callable[[bytes], _T]] = None,
|
|
115
|
+
):
|
|
116
|
+
super().__init__(characteristic_proxy)
|
|
117
|
+
self.encode = encode
|
|
118
|
+
self.decode = decode
|
|
119
|
+
|
|
120
|
+
def encode_value(self, value: _T) -> bytes:
|
|
121
|
+
if self.encode is None:
|
|
122
|
+
raise InvalidOperationError('delegated adapter does not have an encoder')
|
|
123
|
+
return self.encode(value)
|
|
124
|
+
|
|
125
|
+
def decode_value(self, value: bytes) -> _T:
|
|
126
|
+
if self.decode is None:
|
|
127
|
+
raise InvalidOperationError('delegate adapter does not have a decoder')
|
|
128
|
+
return self.decode(value)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# -----------------------------------------------------------------------------
|
|
132
|
+
class PackedCharacteristicAdapter(CharacteristicAdapter):
|
|
133
|
+
'''
|
|
134
|
+
Adapter that packs/unpacks characteristic values according to a standard
|
|
135
|
+
Python `struct` format.
|
|
136
|
+
For formats with a single value, the adapted `read_value` and `write_value`
|
|
137
|
+
methods return/accept single values. For formats with multiple values,
|
|
138
|
+
they return/accept a tuple with the same number of elements as is required for
|
|
139
|
+
the format.
|
|
140
|
+
'''
|
|
141
|
+
|
|
142
|
+
def __init__(self, characteristic: Characteristic, pack_format: str) -> None:
|
|
143
|
+
super().__init__(characteristic)
|
|
144
|
+
self.struct = struct.Struct(pack_format)
|
|
145
|
+
|
|
146
|
+
def pack(self, *values) -> bytes:
|
|
147
|
+
return self.struct.pack(*values)
|
|
148
|
+
|
|
149
|
+
def unpack(self, buffer: bytes) -> tuple:
|
|
150
|
+
return self.struct.unpack(buffer)
|
|
151
|
+
|
|
152
|
+
def encode_value(self, value: Any) -> bytes:
|
|
153
|
+
return self.pack(*value if isinstance(value, tuple) else (value,))
|
|
154
|
+
|
|
155
|
+
def decode_value(self, value: bytes) -> Any:
|
|
156
|
+
unpacked = self.unpack(value)
|
|
157
|
+
return unpacked[0] if len(unpacked) == 1 else unpacked
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# -----------------------------------------------------------------------------
|
|
161
|
+
class PackedCharacteristicProxyAdapter(CharacteristicProxyAdapter):
|
|
162
|
+
'''
|
|
163
|
+
Adapter that packs/unpacks characteristic values according to a standard
|
|
164
|
+
Python `struct` format.
|
|
165
|
+
For formats with a single value, the adapted `read_value` and `write_value`
|
|
166
|
+
methods return/accept single values. For formats with multiple values,
|
|
167
|
+
they return/accept a tuple with the same number of elements as is required for
|
|
168
|
+
the format.
|
|
169
|
+
'''
|
|
170
|
+
|
|
171
|
+
def __init__(self, characteristic_proxy, pack_format):
|
|
172
|
+
super().__init__(characteristic_proxy)
|
|
173
|
+
self.struct = struct.Struct(pack_format)
|
|
174
|
+
|
|
175
|
+
def pack(self, *values) -> bytes:
|
|
176
|
+
return self.struct.pack(*values)
|
|
177
|
+
|
|
178
|
+
def unpack(self, buffer: bytes) -> tuple:
|
|
179
|
+
return self.struct.unpack(buffer)
|
|
180
|
+
|
|
181
|
+
def encode_value(self, value: Any) -> bytes:
|
|
182
|
+
return self.pack(*value if isinstance(value, tuple) else (value,))
|
|
183
|
+
|
|
184
|
+
def decode_value(self, value: bytes) -> Any:
|
|
185
|
+
unpacked = self.unpack(value)
|
|
186
|
+
return unpacked[0] if len(unpacked) == 1 else unpacked
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# -----------------------------------------------------------------------------
|
|
190
|
+
class MappedCharacteristicAdapter(PackedCharacteristicAdapter):
|
|
191
|
+
'''
|
|
192
|
+
Adapter that packs/unpacks characteristic values according to a standard
|
|
193
|
+
Python `struct` format.
|
|
194
|
+
The adapted `read_value` and `write_value` methods return/accept a dictionary which
|
|
195
|
+
is packed/unpacked according to format, with the arguments extracted from the
|
|
196
|
+
dictionary by key, in the same order as they occur in the `keys` parameter.
|
|
197
|
+
'''
|
|
198
|
+
|
|
199
|
+
def __init__(
|
|
200
|
+
self, characteristic: Characteristic, pack_format: str, keys: Iterable[str]
|
|
201
|
+
) -> None:
|
|
202
|
+
super().__init__(characteristic, pack_format)
|
|
203
|
+
self.keys = keys
|
|
204
|
+
|
|
205
|
+
# pylint: disable=arguments-differ
|
|
206
|
+
def pack(self, values) -> bytes:
|
|
207
|
+
return super().pack(*(values[key] for key in self.keys))
|
|
208
|
+
|
|
209
|
+
def unpack(self, buffer: bytes) -> Any:
|
|
210
|
+
return dict(zip(self.keys, super().unpack(buffer)))
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# -----------------------------------------------------------------------------
|
|
214
|
+
class MappedCharacteristicProxyAdapter(PackedCharacteristicProxyAdapter):
|
|
215
|
+
'''
|
|
216
|
+
Adapter that packs/unpacks characteristic values according to a standard
|
|
217
|
+
Python `struct` format.
|
|
218
|
+
The adapted `read_value` and `write_value` methods return/accept a dictionary which
|
|
219
|
+
is packed/unpacked according to format, with the arguments extracted from the
|
|
220
|
+
dictionary by key, in the same order as they occur in the `keys` parameter.
|
|
221
|
+
'''
|
|
222
|
+
|
|
223
|
+
def __init__(
|
|
224
|
+
self,
|
|
225
|
+
characteristic_proxy: CharacteristicProxy,
|
|
226
|
+
pack_format: str,
|
|
227
|
+
keys: Iterable[str],
|
|
228
|
+
) -> None:
|
|
229
|
+
super().__init__(characteristic_proxy, pack_format)
|
|
230
|
+
self.keys = keys
|
|
231
|
+
|
|
232
|
+
# pylint: disable=arguments-differ
|
|
233
|
+
def pack(self, values) -> bytes:
|
|
234
|
+
return super().pack(*(values[key] for key in self.keys))
|
|
235
|
+
|
|
236
|
+
def unpack(self, buffer: bytes) -> Any:
|
|
237
|
+
return dict(zip(self.keys, super().unpack(buffer)))
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# -----------------------------------------------------------------------------
|
|
241
|
+
class UTF8CharacteristicAdapter(CharacteristicAdapter[str]):
|
|
242
|
+
'''
|
|
243
|
+
Adapter that converts strings to/from bytes using UTF-8 encoding
|
|
244
|
+
'''
|
|
245
|
+
|
|
246
|
+
def encode_value(self, value: str) -> bytes:
|
|
247
|
+
return value.encode('utf-8')
|
|
248
|
+
|
|
249
|
+
def decode_value(self, value: bytes) -> str:
|
|
250
|
+
return value.decode('utf-8')
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
# -----------------------------------------------------------------------------
|
|
254
|
+
class UTF8CharacteristicProxyAdapter(CharacteristicProxyAdapter[str]):
|
|
255
|
+
'''
|
|
256
|
+
Adapter that converts strings to/from bytes using UTF-8 encoding
|
|
257
|
+
'''
|
|
258
|
+
|
|
259
|
+
def encode_value(self, value: str) -> bytes:
|
|
260
|
+
return value.encode('utf-8')
|
|
261
|
+
|
|
262
|
+
def decode_value(self, value: bytes) -> str:
|
|
263
|
+
return value.decode('utf-8')
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
# -----------------------------------------------------------------------------
|
|
267
|
+
class SerializableCharacteristicAdapter(CharacteristicAdapter[_T2]):
|
|
268
|
+
'''
|
|
269
|
+
Adapter that converts any class to/from bytes using the class'
|
|
270
|
+
`to_bytes` and `__bytes__` methods, respectively.
|
|
271
|
+
'''
|
|
272
|
+
|
|
273
|
+
def __init__(self, characteristic: Characteristic, cls: Type[_T2]) -> None:
|
|
274
|
+
super().__init__(characteristic)
|
|
275
|
+
self.cls = cls
|
|
276
|
+
|
|
277
|
+
def encode_value(self, value: _T2) -> bytes:
|
|
278
|
+
return bytes(value)
|
|
279
|
+
|
|
280
|
+
def decode_value(self, value: bytes) -> _T2:
|
|
281
|
+
return self.cls.from_bytes(value)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# -----------------------------------------------------------------------------
|
|
285
|
+
class SerializableCharacteristicProxyAdapter(CharacteristicProxyAdapter[_T2]):
|
|
286
|
+
'''
|
|
287
|
+
Adapter that converts any class to/from bytes using the class'
|
|
288
|
+
`to_bytes` and `__bytes__` methods, respectively.
|
|
289
|
+
'''
|
|
290
|
+
|
|
291
|
+
def __init__(
|
|
292
|
+
self, characteristic_proxy: CharacteristicProxy, cls: Type[_T2]
|
|
293
|
+
) -> None:
|
|
294
|
+
super().__init__(characteristic_proxy)
|
|
295
|
+
self.cls = cls
|
|
296
|
+
|
|
297
|
+
def encode_value(self, value: _T2) -> bytes:
|
|
298
|
+
return bytes(value)
|
|
299
|
+
|
|
300
|
+
def decode_value(self, value: bytes) -> _T2:
|
|
301
|
+
return self.cls.from_bytes(value)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
# -----------------------------------------------------------------------------
|
|
305
|
+
class EnumCharacteristicAdapter(CharacteristicAdapter[_T3]):
|
|
306
|
+
'''
|
|
307
|
+
Adapter that converts int-enum-like classes to/from bytes using the class'
|
|
308
|
+
`int().to_bytes()` and `from_bytes()` methods, respectively.
|
|
309
|
+
'''
|
|
310
|
+
|
|
311
|
+
def __init__(
|
|
312
|
+
self,
|
|
313
|
+
characteristic: Characteristic,
|
|
314
|
+
cls: Type[_T3],
|
|
315
|
+
length: int,
|
|
316
|
+
byteorder: Literal['little', 'big'] = 'little',
|
|
317
|
+
):
|
|
318
|
+
"""
|
|
319
|
+
Initialize an instance.
|
|
320
|
+
|
|
321
|
+
Params:
|
|
322
|
+
characteristic: the Characteristic to adapt to/from
|
|
323
|
+
cls: the class to/from which to convert integer values
|
|
324
|
+
length: number of bytes used to represent integer values
|
|
325
|
+
byteorder: byte order of the byte representation of integers.
|
|
326
|
+
"""
|
|
327
|
+
super().__init__(characteristic)
|
|
328
|
+
self.cls = cls
|
|
329
|
+
self.length = length
|
|
330
|
+
self.byteorder = byteorder
|
|
331
|
+
|
|
332
|
+
def encode_value(self, value: _T3) -> bytes:
|
|
333
|
+
return int(value).to_bytes(self.length, self.byteorder)
|
|
334
|
+
|
|
335
|
+
def decode_value(self, value: bytes) -> _T3:
|
|
336
|
+
int_value = int.from_bytes(value, self.byteorder)
|
|
337
|
+
return self.cls(int_value)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
# -----------------------------------------------------------------------------
|
|
341
|
+
class EnumCharacteristicProxyAdapter(CharacteristicProxyAdapter[_T3]):
|
|
342
|
+
'''
|
|
343
|
+
Adapter that converts int-enum-like classes to/from bytes using the class'
|
|
344
|
+
`int().to_bytes()` and `from_bytes()` methods, respectively.
|
|
345
|
+
'''
|
|
346
|
+
|
|
347
|
+
def __init__(
|
|
348
|
+
self,
|
|
349
|
+
characteristic_proxy: CharacteristicProxy,
|
|
350
|
+
cls: Type[_T3],
|
|
351
|
+
length: int,
|
|
352
|
+
byteorder: Literal['little', 'big'] = 'little',
|
|
353
|
+
):
|
|
354
|
+
"""
|
|
355
|
+
Initialize an instance.
|
|
356
|
+
|
|
357
|
+
Params:
|
|
358
|
+
characteristic_proxy: the CharacteristicProxy to adapt to/from
|
|
359
|
+
cls: the class to/from which to convert integer values
|
|
360
|
+
length: number of bytes used to represent integer values
|
|
361
|
+
byteorder: byte order of the byte representation of integers.
|
|
362
|
+
"""
|
|
363
|
+
super().__init__(characteristic_proxy)
|
|
364
|
+
self.cls = cls
|
|
365
|
+
self.length = length
|
|
366
|
+
self.byteorder = byteorder
|
|
367
|
+
|
|
368
|
+
def encode_value(self, value: _T3) -> bytes:
|
|
369
|
+
return int(value).to_bytes(self.length, self.byteorder)
|
|
370
|
+
|
|
371
|
+
def decode_value(self, value: bytes) -> _T3:
|
|
372
|
+
int_value = int.from_bytes(value, self.byteorder)
|
|
373
|
+
a = self.cls(int_value)
|
|
374
|
+
return self.cls(int_value)
|
bumble/gatt_client.py
CHANGED
|
@@ -29,16 +29,18 @@ import logging
|
|
|
29
29
|
import struct
|
|
30
30
|
from datetime import datetime
|
|
31
31
|
from typing import (
|
|
32
|
+
Any,
|
|
33
|
+
Callable,
|
|
34
|
+
Dict,
|
|
35
|
+
Generic,
|
|
36
|
+
Iterable,
|
|
32
37
|
List,
|
|
33
38
|
Optional,
|
|
34
|
-
|
|
39
|
+
Set,
|
|
35
40
|
Tuple,
|
|
36
|
-
Callable,
|
|
37
41
|
Union,
|
|
38
|
-
Any,
|
|
39
|
-
Iterable,
|
|
40
42
|
Type,
|
|
41
|
-
|
|
43
|
+
TypeVar,
|
|
42
44
|
TYPE_CHECKING,
|
|
43
45
|
)
|
|
44
46
|
|
|
@@ -82,9 +84,14 @@ from .gatt import (
|
|
|
82
84
|
TemplateService,
|
|
83
85
|
)
|
|
84
86
|
|
|
87
|
+
# -----------------------------------------------------------------------------
|
|
88
|
+
# Typing
|
|
89
|
+
# -----------------------------------------------------------------------------
|
|
85
90
|
if TYPE_CHECKING:
|
|
86
91
|
from bumble.device import Connection
|
|
87
92
|
|
|
93
|
+
_T = TypeVar('_T')
|
|
94
|
+
|
|
88
95
|
# -----------------------------------------------------------------------------
|
|
89
96
|
# Logging
|
|
90
97
|
# -----------------------------------------------------------------------------
|
|
@@ -110,7 +117,7 @@ def show_services(services: Iterable[ServiceProxy]) -> None:
|
|
|
110
117
|
# -----------------------------------------------------------------------------
|
|
111
118
|
# Proxies
|
|
112
119
|
# -----------------------------------------------------------------------------
|
|
113
|
-
class AttributeProxy(EventEmitter):
|
|
120
|
+
class AttributeProxy(EventEmitter, Generic[_T]):
|
|
114
121
|
def __init__(
|
|
115
122
|
self, client: Client, handle: int, end_group_handle: int, attribute_type: UUID
|
|
116
123
|
) -> None:
|
|
@@ -120,21 +127,21 @@ class AttributeProxy(EventEmitter):
|
|
|
120
127
|
self.end_group_handle = end_group_handle
|
|
121
128
|
self.type = attribute_type
|
|
122
129
|
|
|
123
|
-
async def read_value(self, no_long_read: bool = False) ->
|
|
130
|
+
async def read_value(self, no_long_read: bool = False) -> _T:
|
|
124
131
|
return self.decode_value(
|
|
125
132
|
await self.client.read_value(self.handle, no_long_read)
|
|
126
133
|
)
|
|
127
134
|
|
|
128
|
-
async def write_value(self, value, with_response=False):
|
|
135
|
+
async def write_value(self, value: _T, with_response=False):
|
|
129
136
|
return await self.client.write_value(
|
|
130
137
|
self.handle, self.encode_value(value), with_response
|
|
131
138
|
)
|
|
132
139
|
|
|
133
|
-
def encode_value(self, value:
|
|
134
|
-
return value
|
|
140
|
+
def encode_value(self, value: _T) -> bytes:
|
|
141
|
+
return value # type: ignore
|
|
135
142
|
|
|
136
|
-
def decode_value(self,
|
|
137
|
-
return
|
|
143
|
+
def decode_value(self, value: bytes) -> _T:
|
|
144
|
+
return value # type: ignore
|
|
138
145
|
|
|
139
146
|
def __str__(self) -> str:
|
|
140
147
|
return f'Attribute(handle=0x{self.handle:04X}, type={self.type})'
|
|
@@ -184,19 +191,19 @@ class ServiceProxy(AttributeProxy):
|
|
|
184
191
|
return f'Service(handle=0x{self.handle:04X}, uuid={self.uuid})'
|
|
185
192
|
|
|
186
193
|
|
|
187
|
-
class CharacteristicProxy(AttributeProxy):
|
|
194
|
+
class CharacteristicProxy(AttributeProxy[_T]):
|
|
188
195
|
properties: Characteristic.Properties
|
|
189
196
|
descriptors: List[DescriptorProxy]
|
|
190
|
-
subscribers: Dict[Any, Callable[[
|
|
197
|
+
subscribers: Dict[Any, Callable[[_T], Any]]
|
|
191
198
|
|
|
192
199
|
def __init__(
|
|
193
200
|
self,
|
|
194
|
-
client,
|
|
195
|
-
handle,
|
|
196
|
-
end_group_handle,
|
|
197
|
-
uuid,
|
|
201
|
+
client: Client,
|
|
202
|
+
handle: int,
|
|
203
|
+
end_group_handle: int,
|
|
204
|
+
uuid: UUID,
|
|
198
205
|
properties: int,
|
|
199
|
-
):
|
|
206
|
+
) -> None:
|
|
200
207
|
super().__init__(client, handle, end_group_handle, uuid)
|
|
201
208
|
self.uuid = uuid
|
|
202
209
|
self.properties = Characteristic.Properties(properties)
|
|
@@ -204,21 +211,21 @@ class CharacteristicProxy(AttributeProxy):
|
|
|
204
211
|
self.descriptors_discovered = False
|
|
205
212
|
self.subscribers = {} # Map from subscriber to proxy subscriber
|
|
206
213
|
|
|
207
|
-
def get_descriptor(self, descriptor_type):
|
|
214
|
+
def get_descriptor(self, descriptor_type: UUID) -> Optional[DescriptorProxy]:
|
|
208
215
|
for descriptor in self.descriptors:
|
|
209
216
|
if descriptor.type == descriptor_type:
|
|
210
217
|
return descriptor
|
|
211
218
|
|
|
212
219
|
return None
|
|
213
220
|
|
|
214
|
-
async def discover_descriptors(self):
|
|
221
|
+
async def discover_descriptors(self) -> list[DescriptorProxy]:
|
|
215
222
|
return await self.client.discover_descriptors(self)
|
|
216
223
|
|
|
217
224
|
async def subscribe(
|
|
218
225
|
self,
|
|
219
|
-
subscriber: Optional[Callable[[
|
|
226
|
+
subscriber: Optional[Callable[[_T], Any]] = None,
|
|
220
227
|
prefer_notify: bool = True,
|
|
221
|
-
):
|
|
228
|
+
) -> None:
|
|
222
229
|
if subscriber is not None:
|
|
223
230
|
if subscriber in self.subscribers:
|
|
224
231
|
# We already have a proxy subscriber
|
|
@@ -233,13 +240,13 @@ class CharacteristicProxy(AttributeProxy):
|
|
|
233
240
|
self.subscribers[subscriber] = on_change
|
|
234
241
|
subscriber = on_change
|
|
235
242
|
|
|
236
|
-
|
|
243
|
+
await self.client.subscribe(self, subscriber, prefer_notify)
|
|
237
244
|
|
|
238
|
-
async def unsubscribe(self, subscriber=None, force=False):
|
|
245
|
+
async def unsubscribe(self, subscriber=None, force=False) -> None:
|
|
239
246
|
if subscriber in self.subscribers:
|
|
240
247
|
subscriber = self.subscribers.pop(subscriber)
|
|
241
248
|
|
|
242
|
-
|
|
249
|
+
await self.client.unsubscribe(self, subscriber, force)
|
|
243
250
|
|
|
244
251
|
def __str__(self) -> str:
|
|
245
252
|
return (
|
|
@@ -250,7 +257,7 @@ class CharacteristicProxy(AttributeProxy):
|
|
|
250
257
|
|
|
251
258
|
|
|
252
259
|
class DescriptorProxy(AttributeProxy):
|
|
253
|
-
def __init__(self, client, handle, descriptor_type):
|
|
260
|
+
def __init__(self, client: Client, handle: int, descriptor_type: UUID) -> None:
|
|
254
261
|
super().__init__(client, handle, 0, descriptor_type)
|
|
255
262
|
|
|
256
263
|
def __str__(self) -> str:
|
|
@@ -679,7 +686,7 @@ class Client:
|
|
|
679
686
|
|
|
680
687
|
properties, handle = struct.unpack_from('<BH', attribute_value)
|
|
681
688
|
characteristic_uuid = UUID.from_bytes(attribute_value[3:])
|
|
682
|
-
characteristic = CharacteristicProxy(
|
|
689
|
+
characteristic: CharacteristicProxy = CharacteristicProxy(
|
|
683
690
|
self, handle, 0, characteristic_uuid, properties
|
|
684
691
|
)
|
|
685
692
|
|
|
@@ -805,7 +812,7 @@ class Client:
|
|
|
805
812
|
logger.warning(f'bogus handle value: {attribute_handle}')
|
|
806
813
|
return []
|
|
807
814
|
|
|
808
|
-
attribute = AttributeProxy(
|
|
815
|
+
attribute: AttributeProxy = AttributeProxy(
|
|
809
816
|
self, attribute_handle, 0, UUID.from_bytes(attribute_uuid)
|
|
810
817
|
)
|
|
811
818
|
attributes.append(attribute)
|
|
@@ -818,7 +825,7 @@ class Client:
|
|
|
818
825
|
async def subscribe(
|
|
819
826
|
self,
|
|
820
827
|
characteristic: CharacteristicProxy,
|
|
821
|
-
subscriber: Optional[Callable[[
|
|
828
|
+
subscriber: Optional[Callable[[Any], Any]] = None,
|
|
822
829
|
prefer_notify: bool = True,
|
|
823
830
|
) -> None:
|
|
824
831
|
# If we haven't already discovered the descriptors for this characteristic,
|
|
@@ -868,7 +875,7 @@ class Client:
|
|
|
868
875
|
async def unsubscribe(
|
|
869
876
|
self,
|
|
870
877
|
characteristic: CharacteristicProxy,
|
|
871
|
-
subscriber: Optional[Callable[[
|
|
878
|
+
subscriber: Optional[Callable[[Any], Any]] = None,
|
|
872
879
|
force: bool = False,
|
|
873
880
|
) -> None:
|
|
874
881
|
'''
|
bumble/gatt_server.py
CHANGED
|
@@ -36,7 +36,6 @@ from typing import (
|
|
|
36
36
|
Tuple,
|
|
37
37
|
TypeVar,
|
|
38
38
|
Type,
|
|
39
|
-
Union,
|
|
40
39
|
TYPE_CHECKING,
|
|
41
40
|
)
|
|
42
41
|
from pyee import EventEmitter
|
|
@@ -78,7 +77,6 @@ from bumble.gatt import (
|
|
|
78
77
|
GATT_REQUEST_TIMEOUT,
|
|
79
78
|
GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE,
|
|
80
79
|
Characteristic,
|
|
81
|
-
CharacteristicAdapter,
|
|
82
80
|
CharacteristicDeclaration,
|
|
83
81
|
CharacteristicValue,
|
|
84
82
|
IncludedServiceDeclaration,
|
|
@@ -469,7 +467,7 @@ class Server(EventEmitter):
|
|
|
469
467
|
finally:
|
|
470
468
|
self.pending_confirmations[connection.handle] = None
|
|
471
469
|
|
|
472
|
-
async def
|
|
470
|
+
async def _notify_or_indicate_subscribers(
|
|
473
471
|
self,
|
|
474
472
|
indicate: bool,
|
|
475
473
|
attribute: Attribute,
|
|
@@ -503,7 +501,9 @@ class Server(EventEmitter):
|
|
|
503
501
|
value: Optional[bytes] = None,
|
|
504
502
|
force: bool = False,
|
|
505
503
|
):
|
|
506
|
-
return await self.
|
|
504
|
+
return await self._notify_or_indicate_subscribers(
|
|
505
|
+
False, attribute, value, force
|
|
506
|
+
)
|
|
507
507
|
|
|
508
508
|
async def indicate_subscribers(
|
|
509
509
|
self,
|
|
@@ -511,7 +511,7 @@ class Server(EventEmitter):
|
|
|
511
511
|
value: Optional[bytes] = None,
|
|
512
512
|
force: bool = False,
|
|
513
513
|
):
|
|
514
|
-
return await self.
|
|
514
|
+
return await self._notify_or_indicate_subscribers(True, attribute, value, force)
|
|
515
515
|
|
|
516
516
|
def on_disconnection(self, connection: Connection) -> None:
|
|
517
517
|
if connection.handle in self.subscribers:
|
bumble/host.py
CHANGED
|
@@ -235,7 +235,7 @@ class Host(AbortableEventEmitter):
|
|
|
235
235
|
cis_links: Dict[int, IsoLink]
|
|
236
236
|
bis_links: Dict[int, IsoLink]
|
|
237
237
|
sco_links: Dict[int, ScoLink]
|
|
238
|
-
bigs: dict[int, set[int]]
|
|
238
|
+
bigs: dict[int, set[int]]
|
|
239
239
|
acl_packet_queue: Optional[DataPacketQueue] = None
|
|
240
240
|
le_acl_packet_queue: Optional[DataPacketQueue] = None
|
|
241
241
|
iso_packet_queue: Optional[DataPacketQueue] = None
|
|
@@ -259,6 +259,7 @@ class Host(AbortableEventEmitter):
|
|
|
259
259
|
self.cis_links = {} # CIS links, by connection handle
|
|
260
260
|
self.bis_links = {} # BIS links, by connection handle
|
|
261
261
|
self.sco_links = {} # SCO links, by connection handle
|
|
262
|
+
self.bigs = {} # BIG Handle to BIS Handles
|
|
262
263
|
self.pending_command = None
|
|
263
264
|
self.pending_response: Optional[asyncio.Future[Any]] = None
|
|
264
265
|
self.number_of_supported_advertising_sets = 0
|
|
@@ -1061,8 +1062,10 @@ class Host(AbortableEventEmitter):
|
|
|
1061
1062
|
)
|
|
1062
1063
|
|
|
1063
1064
|
# Flush the data queues
|
|
1064
|
-
self.acl_packet_queue
|
|
1065
|
-
|
|
1065
|
+
if self.acl_packet_queue:
|
|
1066
|
+
self.acl_packet_queue.flush(handle)
|
|
1067
|
+
if self.le_acl_packet_queue:
|
|
1068
|
+
self.le_acl_packet_queue.flush(handle)
|
|
1066
1069
|
if self.iso_packet_queue:
|
|
1067
1070
|
self.iso_packet_queue.flush(handle)
|
|
1068
1071
|
else:
|
|
@@ -1098,8 +1101,11 @@ class Host(AbortableEventEmitter):
|
|
|
1098
1101
|
|
|
1099
1102
|
# Notify the client
|
|
1100
1103
|
if event.status == hci.HCI_SUCCESS:
|
|
1101
|
-
|
|
1102
|
-
|
|
1104
|
+
self.emit(
|
|
1105
|
+
'connection_phy_update',
|
|
1106
|
+
connection.handle,
|
|
1107
|
+
ConnectionPHY(event.tx_phy, event.rx_phy),
|
|
1108
|
+
)
|
|
1103
1109
|
else:
|
|
1104
1110
|
self.emit('connection_phy_update_failure', connection.handle, event.status)
|
|
1105
1111
|
|
bumble/pairing.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2021-
|
|
1
|
+
# Copyright 2021-2025 Google LLC
|
|
2
2
|
#
|
|
3
3
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
4
|
# you may not use this file except in compliance with the License.
|
|
@@ -76,18 +76,18 @@ class OobData:
|
|
|
76
76
|
return instance
|
|
77
77
|
|
|
78
78
|
def to_ad(self) -> AdvertisingData:
|
|
79
|
-
ad_structures = []
|
|
79
|
+
ad_structures: list[tuple[int, bytes]] = []
|
|
80
80
|
if self.address is not None:
|
|
81
81
|
ad_structures.append(
|
|
82
|
-
(AdvertisingData.LE_BLUETOOTH_DEVICE_ADDRESS, bytes(self.address))
|
|
82
|
+
(AdvertisingData.Type.LE_BLUETOOTH_DEVICE_ADDRESS, bytes(self.address))
|
|
83
83
|
)
|
|
84
84
|
if self.role is not None:
|
|
85
|
-
ad_structures.append((AdvertisingData.LE_ROLE, bytes([self.role])))
|
|
85
|
+
ad_structures.append((AdvertisingData.Type.LE_ROLE, bytes([self.role])))
|
|
86
86
|
if self.shared_data is not None:
|
|
87
87
|
ad_structures.extend(self.shared_data.to_ad().ad_structures)
|
|
88
88
|
if self.legacy_context is not None:
|
|
89
89
|
ad_structures.append(
|
|
90
|
-
(AdvertisingData.SECURITY_MANAGER_TK_VALUE, self.legacy_context.tk)
|
|
90
|
+
(AdvertisingData.Type.SECURITY_MANAGER_TK_VALUE, self.legacy_context.tk)
|
|
91
91
|
)
|
|
92
92
|
|
|
93
93
|
return AdvertisingData(ad_structures)
|