bumble 0.0.213__py3-none-any.whl → 0.0.214__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 +3 -2
- bumble/apps/bench.py +2 -6
- bumble/apps/controller_info.py +2 -7
- bumble/apps/controller_loopback.py +5 -9
- bumble/apps/controllers.py +2 -3
- bumble/apps/device_info.py +2 -3
- bumble/apps/gatt_dump.py +3 -3
- bumble/apps/gg_bridge.py +3 -3
- bumble/apps/hci_bridge.py +3 -2
- bumble/apps/l2cap_bridge.py +3 -3
- bumble/apps/lea_unicast/app.py +2 -2
- bumble/apps/player/player.py +2 -3
- bumble/apps/rfcomm_bridge.py +2 -3
- bumble/apps/scan.py +2 -3
- bumble/apps/show.py +2 -2
- bumble/apps/speaker/speaker.py +2 -6
- bumble/apps/unbond.py +2 -3
- bumble/apps/usb_probe.py +2 -3
- bumble/avrcp.py +1 -1
- bumble/controller.py +90 -22
- bumble/device.py +55 -5
- bumble/hci.py +47 -0
- bumble/host.py +10 -0
- bumble/l2cap.py +17 -30
- bumble/link.py +10 -10
- bumble/logging.py +65 -0
- bumble/pandora/__init__.py +1 -1
- bumble/profiles/ams.py +404 -0
- bumble/profiles/ascs.py +10 -0
- bumble/smp.py +9 -6
- bumble/tools/intel_util.py +3 -2
- bumble/tools/rtk_util.py +4 -3
- {bumble-0.0.213.dist-info → bumble-0.0.214.dist-info}/METADATA +3 -2
- {bumble-0.0.213.dist-info → bumble-0.0.214.dist-info}/RECORD +39 -37
- {bumble-0.0.213.dist-info → bumble-0.0.214.dist-info}/WHEEL +0 -0
- {bumble-0.0.213.dist-info → bumble-0.0.214.dist-info}/entry_points.txt +0 -0
- {bumble-0.0.213.dist-info → bumble-0.0.214.dist-info}/licenses/LICENSE +0 -0
- {bumble-0.0.213.dist-info → bumble-0.0.214.dist-info}/top_level.txt +0 -0
bumble/device.py
CHANGED
|
@@ -1752,6 +1752,8 @@ class Connection(utils.CompositeEventEmitter):
|
|
|
1752
1752
|
EVENT_CIS_REQUEST = "cis_request"
|
|
1753
1753
|
EVENT_CIS_ESTABLISHMENT = "cis_establishment"
|
|
1754
1754
|
EVENT_CIS_ESTABLISHMENT_FAILURE = "cis_establishment_failure"
|
|
1755
|
+
EVENT_LE_SUBRATE_CHANGE = "le_subrate_change"
|
|
1756
|
+
EVENT_LE_SUBRATE_CHANGE_FAILURE = "le_subrate_change_failure"
|
|
1755
1757
|
|
|
1756
1758
|
@utils.composite_listener
|
|
1757
1759
|
class Listener:
|
|
@@ -1787,6 +1789,12 @@ class Connection(utils.CompositeEventEmitter):
|
|
|
1787
1789
|
connection_interval: float # Connection interval, in milliseconds. [LE only]
|
|
1788
1790
|
peripheral_latency: int # Peripheral latency, in number of intervals. [LE only]
|
|
1789
1791
|
supervision_timeout: float # Supervision timeout, in milliseconds.
|
|
1792
|
+
subrate_factor: int = (
|
|
1793
|
+
1 # See Bluetooth spec Vol 6, Part B - 4.5.1 Connection events
|
|
1794
|
+
)
|
|
1795
|
+
continuation_number: int = (
|
|
1796
|
+
0 # See Bluetooth spec Vol 6, Part B - 4.5.1 Connection events
|
|
1797
|
+
)
|
|
1790
1798
|
|
|
1791
1799
|
def __init__(
|
|
1792
1800
|
self,
|
|
@@ -2058,6 +2066,7 @@ class DeviceConfiguration:
|
|
|
2058
2066
|
le_simultaneous_enabled: bool = False
|
|
2059
2067
|
le_privacy_enabled: bool = False
|
|
2060
2068
|
le_rpa_timeout: int = DEVICE_DEFAULT_LE_RPA_TIMEOUT
|
|
2069
|
+
le_subrate_enabled: bool = False
|
|
2061
2070
|
classic_enabled: bool = False
|
|
2062
2071
|
classic_sc_enabled: bool = True
|
|
2063
2072
|
classic_ssp_enabled: bool = True
|
|
@@ -2410,6 +2419,7 @@ class Device(utils.CompositeEventEmitter):
|
|
|
2410
2419
|
self.le_privacy_enabled = config.le_privacy_enabled
|
|
2411
2420
|
self.le_rpa_timeout = config.le_rpa_timeout
|
|
2412
2421
|
self.le_rpa_periodic_update_task: Optional[asyncio.Task] = None
|
|
2422
|
+
self.le_subrate_enabled = config.le_subrate_enabled
|
|
2413
2423
|
self.classic_enabled = config.classic_enabled
|
|
2414
2424
|
self.cis_enabled = config.cis_enabled
|
|
2415
2425
|
self.classic_sc_enabled = config.classic_sc_enabled
|
|
@@ -2789,6 +2799,15 @@ class Device(utils.CompositeEventEmitter):
|
|
|
2789
2799
|
check_result=True,
|
|
2790
2800
|
)
|
|
2791
2801
|
|
|
2802
|
+
if self.le_subrate_enabled:
|
|
2803
|
+
await self.send_command(
|
|
2804
|
+
hci.HCI_LE_Set_Host_Feature_Command(
|
|
2805
|
+
bit_number=hci.LeFeature.CONNECTION_SUBRATING_HOST_SUPPORT,
|
|
2806
|
+
bit_value=1,
|
|
2807
|
+
),
|
|
2808
|
+
check_result=True,
|
|
2809
|
+
)
|
|
2810
|
+
|
|
2792
2811
|
if self.config.channel_sounding_enabled:
|
|
2793
2812
|
await self.send_command(
|
|
2794
2813
|
hci.HCI_LE_Set_Host_Feature_Command(
|
|
@@ -6189,11 +6208,23 @@ class Device(utils.CompositeEventEmitter):
|
|
|
6189
6208
|
f'{connection.peer_address} as {connection.role_name}, '
|
|
6190
6209
|
f'{connection_parameters}'
|
|
6191
6210
|
)
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
connection_parameters.
|
|
6195
|
-
|
|
6196
|
-
|
|
6211
|
+
if (
|
|
6212
|
+
connection.parameters.connection_interval
|
|
6213
|
+
!= connection_parameters.connection_interval * 1.25
|
|
6214
|
+
):
|
|
6215
|
+
connection.parameters = Connection.Parameters(
|
|
6216
|
+
connection_parameters.connection_interval * 1.25,
|
|
6217
|
+
connection_parameters.peripheral_latency,
|
|
6218
|
+
connection_parameters.supervision_timeout * 10.0,
|
|
6219
|
+
)
|
|
6220
|
+
else:
|
|
6221
|
+
connection.parameters = Connection.Parameters(
|
|
6222
|
+
connection_parameters.connection_interval * 1.25,
|
|
6223
|
+
connection_parameters.peripheral_latency,
|
|
6224
|
+
connection_parameters.supervision_timeout * 10.0,
|
|
6225
|
+
connection.parameters.subrate_factor,
|
|
6226
|
+
connection.parameters.continuation_number,
|
|
6227
|
+
)
|
|
6197
6228
|
connection.emit(connection.EVENT_CONNECTION_PARAMETERS_UPDATE)
|
|
6198
6229
|
|
|
6199
6230
|
@host_event_handler
|
|
@@ -6226,6 +6257,25 @@ class Device(utils.CompositeEventEmitter):
|
|
|
6226
6257
|
)
|
|
6227
6258
|
connection.emit(connection.EVENT_CONNECTION_PHY_UPDATE_FAILURE, error)
|
|
6228
6259
|
|
|
6260
|
+
@host_event_handler
|
|
6261
|
+
@with_connection_from_handle
|
|
6262
|
+
def on_le_subrate_change(
|
|
6263
|
+
self,
|
|
6264
|
+
connection: Connection,
|
|
6265
|
+
subrate_factor: int,
|
|
6266
|
+
peripheral_latency: int,
|
|
6267
|
+
continuation_number: int,
|
|
6268
|
+
supervision_timeout: int,
|
|
6269
|
+
):
|
|
6270
|
+
connection.parameters = Connection.Parameters(
|
|
6271
|
+
connection.parameters.connection_interval,
|
|
6272
|
+
peripheral_latency,
|
|
6273
|
+
supervision_timeout * 10.0,
|
|
6274
|
+
subrate_factor,
|
|
6275
|
+
continuation_number,
|
|
6276
|
+
)
|
|
6277
|
+
connection.emit(connection.EVENT_LE_SUBRATE_CHANGE)
|
|
6278
|
+
|
|
6229
6279
|
@host_event_handler
|
|
6230
6280
|
@with_connection_from_handle
|
|
6231
6281
|
def on_connection_att_mtu_update(self, connection, att_mtu):
|
bumble/hci.py
CHANGED
|
@@ -5315,6 +5315,37 @@ class HCI_LE_Set_Host_Feature_Command(HCI_Command):
|
|
|
5315
5315
|
bit_value: int = field(metadata=metadata(1))
|
|
5316
5316
|
|
|
5317
5317
|
|
|
5318
|
+
# -----------------------------------------------------------------------------
|
|
5319
|
+
@HCI_Command.command
|
|
5320
|
+
@dataclasses.dataclass
|
|
5321
|
+
class HCI_LE_Set_Default_Subrate_Command(HCI_Command):
|
|
5322
|
+
'''
|
|
5323
|
+
See Bluetooth spec @ 7.8.123 LE Set Default Subrate command
|
|
5324
|
+
'''
|
|
5325
|
+
|
|
5326
|
+
subrate_min: int = field(metadata=metadata(2))
|
|
5327
|
+
subrate_max: int = field(metadata=metadata(2))
|
|
5328
|
+
max_latency: int = field(metadata=metadata(2))
|
|
5329
|
+
continuation_number: int = field(metadata=metadata(2))
|
|
5330
|
+
supervision_timeout: int = field(metadata=metadata(2))
|
|
5331
|
+
|
|
5332
|
+
|
|
5333
|
+
# -----------------------------------------------------------------------------
|
|
5334
|
+
@HCI_Command.command
|
|
5335
|
+
@dataclasses.dataclass
|
|
5336
|
+
class HCI_LE_Subrate_Request_Command(HCI_Command):
|
|
5337
|
+
'''
|
|
5338
|
+
See Bluetooth spec @ 7.8.124 LE Subrate Request command
|
|
5339
|
+
'''
|
|
5340
|
+
|
|
5341
|
+
connection_handle: int = field(metadata=metadata(2))
|
|
5342
|
+
subrate_min: int = field(metadata=metadata(2))
|
|
5343
|
+
subrate_max: int = field(metadata=metadata(2))
|
|
5344
|
+
max_latency: int = field(metadata=metadata(2))
|
|
5345
|
+
continuation_number: int = field(metadata=metadata(2))
|
|
5346
|
+
supervision_timeout: int = field(metadata=metadata(2))
|
|
5347
|
+
|
|
5348
|
+
|
|
5318
5349
|
# -----------------------------------------------------------------------------
|
|
5319
5350
|
@HCI_Command.command
|
|
5320
5351
|
@dataclasses.dataclass
|
|
@@ -6460,6 +6491,22 @@ class HCI_LE_BIGInfo_Advertising_Report_Event(HCI_LE_Meta_Event):
|
|
|
6460
6491
|
encryption: int = field(metadata=metadata(1))
|
|
6461
6492
|
|
|
6462
6493
|
|
|
6494
|
+
# -----------------------------------------------------------------------------
|
|
6495
|
+
@HCI_LE_Meta_Event.event
|
|
6496
|
+
@dataclasses.dataclass
|
|
6497
|
+
class HCI_LE_Subrate_Change_Event(HCI_LE_Meta_Event):
|
|
6498
|
+
'''
|
|
6499
|
+
See Bluetooth spec @ 7.7.65.35 LE Subrate Change event
|
|
6500
|
+
'''
|
|
6501
|
+
|
|
6502
|
+
status: int = field(metadata=metadata(STATUS_SPEC))
|
|
6503
|
+
connection_handle: int = field(metadata=metadata(2))
|
|
6504
|
+
subrate_factor: int = field(metadata=metadata(2))
|
|
6505
|
+
peripheral_latency: int = field(metadata=metadata(2))
|
|
6506
|
+
continuation_number: int = field(metadata=metadata(2))
|
|
6507
|
+
supervision_timeout: int = field(metadata=metadata(2))
|
|
6508
|
+
|
|
6509
|
+
|
|
6463
6510
|
# -----------------------------------------------------------------------------
|
|
6464
6511
|
@HCI_LE_Meta_Event.event
|
|
6465
6512
|
@dataclasses.dataclass
|
bumble/host.py
CHANGED
|
@@ -1645,5 +1645,15 @@ class Host(utils.EventEmitter):
|
|
|
1645
1645
|
def on_hci_le_cs_subevent_result_continue_event(self, event):
|
|
1646
1646
|
self.emit('cs_subevent_result_continue', event)
|
|
1647
1647
|
|
|
1648
|
+
def on_hci_le_subrate_change_event(self, event: hci.HCI_LE_Subrate_Change_Event):
|
|
1649
|
+
self.emit(
|
|
1650
|
+
'le_subrate_change',
|
|
1651
|
+
event.connection_handle,
|
|
1652
|
+
event.subrate_factor,
|
|
1653
|
+
event.peripheral_latency,
|
|
1654
|
+
event.continuation_number,
|
|
1655
|
+
event.supervision_timeout,
|
|
1656
|
+
)
|
|
1657
|
+
|
|
1648
1658
|
def on_hci_vendor_event(self, event):
|
|
1649
1659
|
self.emit('vendor_event', event)
|
bumble/l2cap.py
CHANGED
|
@@ -213,7 +213,7 @@ class L2CAP_Control_Frame:
|
|
|
213
213
|
fields: ClassVar[hci.Fields] = ()
|
|
214
214
|
code: int = dataclasses.field(default=0, init=False)
|
|
215
215
|
name: str = dataclasses.field(default='', init=False)
|
|
216
|
-
|
|
216
|
+
_payload: Optional[bytes] = dataclasses.field(default=None, init=False)
|
|
217
217
|
|
|
218
218
|
identifier: int
|
|
219
219
|
|
|
@@ -223,7 +223,8 @@ class L2CAP_Control_Frame:
|
|
|
223
223
|
|
|
224
224
|
subclass = L2CAP_Control_Frame.classes.get(code)
|
|
225
225
|
if subclass is None:
|
|
226
|
-
instance = L2CAP_Control_Frame(
|
|
226
|
+
instance = L2CAP_Control_Frame(identifier=identifier)
|
|
227
|
+
instance.payload = pdu[4:]
|
|
227
228
|
instance.code = CommandCode(code)
|
|
228
229
|
instance.name = instance.code.name
|
|
229
230
|
return instance
|
|
@@ -232,11 +233,11 @@ class L2CAP_Control_Frame:
|
|
|
232
233
|
identifier=identifier,
|
|
233
234
|
)
|
|
234
235
|
frame.identifier = identifier
|
|
235
|
-
frame.
|
|
236
|
-
if length != len(
|
|
236
|
+
frame.payload = pdu[4:]
|
|
237
|
+
if length != len(frame.payload):
|
|
237
238
|
logger.warning(
|
|
238
239
|
color(
|
|
239
|
-
f'!!! length mismatch: expected {
|
|
240
|
+
f'!!! length mismatch: expected {length} but got {len(frame.payload)}',
|
|
240
241
|
'red',
|
|
241
242
|
)
|
|
242
243
|
)
|
|
@@ -273,34 +274,20 @@ class L2CAP_Control_Frame:
|
|
|
273
274
|
|
|
274
275
|
return subclass
|
|
275
276
|
|
|
276
|
-
def __init__(self, pdu: Optional[bytes] = None, **kwargs) -> None:
|
|
277
|
-
self.identifier = kwargs.get('identifier', 0)
|
|
278
|
-
if self.fields:
|
|
279
|
-
if kwargs:
|
|
280
|
-
hci.HCI_Object.init_from_fields(self, self.fields, kwargs)
|
|
281
|
-
if pdu is None:
|
|
282
|
-
data = hci.HCI_Object.dict_to_bytes(kwargs, self.fields)
|
|
283
|
-
pdu = (
|
|
284
|
-
bytes([self.code, self.identifier])
|
|
285
|
-
+ struct.pack('<H', len(data))
|
|
286
|
-
+ data
|
|
287
|
-
)
|
|
288
|
-
self.data = pdu[4:] if pdu else b''
|
|
289
|
-
|
|
290
277
|
@property
|
|
291
|
-
def
|
|
292
|
-
if self.
|
|
293
|
-
self.
|
|
294
|
-
return self.
|
|
278
|
+
def payload(self) -> bytes:
|
|
279
|
+
if self._payload is None:
|
|
280
|
+
self._payload = hci.HCI_Object.dict_to_bytes(self.__dict__, self.fields)
|
|
281
|
+
return self._payload
|
|
295
282
|
|
|
296
|
-
@
|
|
297
|
-
def
|
|
298
|
-
self.
|
|
283
|
+
@payload.setter
|
|
284
|
+
def payload(self, payload: bytes) -> None:
|
|
285
|
+
self._payload = payload
|
|
299
286
|
|
|
300
287
|
def __bytes__(self) -> bytes:
|
|
301
288
|
return (
|
|
302
|
-
struct.pack('<BBH', self.code, self.identifier, len(self.
|
|
303
|
-
+ self.
|
|
289
|
+
struct.pack('<BBH', self.code, self.identifier, len(self.payload))
|
|
290
|
+
+ self.payload
|
|
304
291
|
)
|
|
305
292
|
|
|
306
293
|
def __str__(self) -> str:
|
|
@@ -308,8 +295,8 @@ class L2CAP_Control_Frame:
|
|
|
308
295
|
if fields := getattr(self, 'fields', None):
|
|
309
296
|
result += ':\n' + hci.HCI_Object.format_fields(self.__dict__, fields, ' ')
|
|
310
297
|
else:
|
|
311
|
-
if len(self.
|
|
312
|
-
result += f': {self.
|
|
298
|
+
if len(self.payload) > 1:
|
|
299
|
+
result += f': {self.payload.hex()}'
|
|
313
300
|
return result
|
|
314
301
|
|
|
315
302
|
|
bumble/link.py
CHANGED
|
@@ -159,29 +159,29 @@ class LocalLink:
|
|
|
159
159
|
asyncio.get_running_loop().call_soon(self.on_connection_complete)
|
|
160
160
|
|
|
161
161
|
def on_disconnection_complete(
|
|
162
|
-
self,
|
|
162
|
+
self, initiating_address, target_address, disconnect_command
|
|
163
163
|
):
|
|
164
164
|
# Find the controller that initiated the disconnection
|
|
165
|
-
if not (
|
|
165
|
+
if not (initiating_controller := self.find_controller(initiating_address)):
|
|
166
166
|
logger.warning('!!! Initiating controller not found')
|
|
167
167
|
return
|
|
168
168
|
|
|
169
169
|
# Disconnect from the first controller with a matching address
|
|
170
|
-
if
|
|
171
|
-
|
|
172
|
-
|
|
170
|
+
if target_controller := self.find_controller(target_address):
|
|
171
|
+
target_controller.on_link_disconnected(
|
|
172
|
+
initiating_address, disconnect_command.reason
|
|
173
173
|
)
|
|
174
174
|
|
|
175
|
-
|
|
175
|
+
initiating_controller.on_link_disconnection_complete(
|
|
176
176
|
disconnect_command, HCI_SUCCESS
|
|
177
177
|
)
|
|
178
178
|
|
|
179
|
-
def disconnect(self,
|
|
179
|
+
def disconnect(self, initiating_address, target_address, disconnect_command):
|
|
180
180
|
logger.debug(
|
|
181
|
-
f'$$$ DISCONNECTION {
|
|
182
|
-
f'{
|
|
181
|
+
f'$$$ DISCONNECTION {initiating_address} -> '
|
|
182
|
+
f'{target_address}: reason = {disconnect_command.reason}'
|
|
183
183
|
)
|
|
184
|
-
args = [
|
|
184
|
+
args = [initiating_address, target_address, disconnect_command]
|
|
185
185
|
asyncio.get_running_loop().call_soon(self.on_disconnection_complete, *args)
|
|
186
186
|
|
|
187
187
|
# pylint: disable=too-many-arguments
|
bumble/logging.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
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
|
+
# Imports
|
|
17
|
+
# -----------------------------------------------------------------------------
|
|
18
|
+
import functools
|
|
19
|
+
import logging
|
|
20
|
+
import os
|
|
21
|
+
|
|
22
|
+
from bumble import colors
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# -----------------------------------------------------------------------------
|
|
26
|
+
class ColorFormatter(logging.Formatter):
|
|
27
|
+
_colorizers = {
|
|
28
|
+
logging.DEBUG: functools.partial(colors.color, fg="white"),
|
|
29
|
+
logging.INFO: functools.partial(colors.color, fg="green"),
|
|
30
|
+
logging.WARNING: functools.partial(colors.color, fg="yellow"),
|
|
31
|
+
logging.ERROR: functools.partial(colors.color, fg="red"),
|
|
32
|
+
logging.CRITICAL: functools.partial(colors.color, fg="black", bg="red"),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
_formatters = {
|
|
36
|
+
level: logging.Formatter(
|
|
37
|
+
fmt=colorizer("{asctime}.{msecs:03.0f} {levelname:.1} {name}: ")
|
|
38
|
+
+ "{message}",
|
|
39
|
+
datefmt="%H:%M:%S",
|
|
40
|
+
style="{",
|
|
41
|
+
)
|
|
42
|
+
for level, colorizer in _colorizers.items()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
46
|
+
return self._formatters[record.levelno].format(record)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def setup_basic_logging(default_level: str = "INFO") -> None:
|
|
50
|
+
"""
|
|
51
|
+
Set up basic logging with logging.basicConfig, configured with a simple formatter
|
|
52
|
+
that prints out the date and log level in color.
|
|
53
|
+
If the BUMBLE_LOGLEVEL environment variable is set to the name of a log level, it
|
|
54
|
+
is used. Otherwise the default_level argument is used.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
default_level: default logging level
|
|
58
|
+
|
|
59
|
+
"""
|
|
60
|
+
handler = logging.StreamHandler()
|
|
61
|
+
handler.setFormatter(ColorFormatter())
|
|
62
|
+
logging.basicConfig(
|
|
63
|
+
level=os.environ.get("BUMBLE_LOGLEVEL", default_level).upper(),
|
|
64
|
+
handlers=[handler],
|
|
65
|
+
)
|
bumble/pandora/__init__.py
CHANGED
|
@@ -49,7 +49,7 @@ _SERVICERS_HOOKS: list[Callable[[PandoraDevice, Config, grpc.aio.Server], None]]
|
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
def register_servicer_hook(
|
|
52
|
-
hook: Callable[[PandoraDevice, Config, grpc.aio.Server], None]
|
|
52
|
+
hook: Callable[[PandoraDevice, Config, grpc.aio.Server], None],
|
|
53
53
|
) -> None:
|
|
54
54
|
_SERVICERS_HOOKS.append(hook)
|
|
55
55
|
|