bumble 0.0.169__py3-none-any.whl → 0.0.172__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/gatt_client.py CHANGED
@@ -28,7 +28,18 @@ import asyncio
28
28
  import logging
29
29
  import struct
30
30
  from datetime import datetime
31
- from typing import List, Optional, Dict, Tuple, Callable, Union, Any
31
+ from typing import (
32
+ List,
33
+ Optional,
34
+ Dict,
35
+ Tuple,
36
+ Callable,
37
+ Union,
38
+ Any,
39
+ Iterable,
40
+ Type,
41
+ TYPE_CHECKING,
42
+ )
32
43
 
33
44
  from pyee import EventEmitter
34
45
 
@@ -66,8 +77,12 @@ from .gatt import (
66
77
  GATT_INCLUDE_ATTRIBUTE_TYPE,
67
78
  Characteristic,
68
79
  ClientCharacteristicConfigurationBits,
80
+ TemplateService,
69
81
  )
70
82
 
83
+ if TYPE_CHECKING:
84
+ from bumble.device import Connection
85
+
71
86
  # -----------------------------------------------------------------------------
72
87
  # Logging
73
88
  # -----------------------------------------------------------------------------
@@ -78,16 +93,16 @@ logger = logging.getLogger(__name__)
78
93
  # Proxies
79
94
  # -----------------------------------------------------------------------------
80
95
  class AttributeProxy(EventEmitter):
81
- client: Client
82
-
83
- def __init__(self, client, handle, end_group_handle, attribute_type):
96
+ def __init__(
97
+ self, client: Client, handle: int, end_group_handle: int, attribute_type: UUID
98
+ ) -> None:
84
99
  EventEmitter.__init__(self)
85
100
  self.client = client
86
101
  self.handle = handle
87
102
  self.end_group_handle = end_group_handle
88
103
  self.type = attribute_type
89
104
 
90
- async def read_value(self, no_long_read=False):
105
+ async def read_value(self, no_long_read: bool = False) -> bytes:
91
106
  return self.decode_value(
92
107
  await self.client.read_value(self.handle, no_long_read)
93
108
  )
@@ -97,13 +112,13 @@ class AttributeProxy(EventEmitter):
97
112
  self.handle, self.encode_value(value), with_response
98
113
  )
99
114
 
100
- def encode_value(self, value):
115
+ def encode_value(self, value: Any) -> bytes:
101
116
  return value
102
117
 
103
- def decode_value(self, value_bytes):
118
+ def decode_value(self, value_bytes: bytes) -> Any:
104
119
  return value_bytes
105
120
 
106
- def __str__(self):
121
+ def __str__(self) -> str:
107
122
  return f'Attribute(handle=0x{self.handle:04X}, type={self.type})'
108
123
 
109
124
 
@@ -136,14 +151,14 @@ class ServiceProxy(AttributeProxy):
136
151
  def get_characteristics_by_uuid(self, uuid):
137
152
  return self.client.get_characteristics_by_uuid(uuid, self)
138
153
 
139
- def __str__(self):
154
+ def __str__(self) -> str:
140
155
  return f'Service(handle=0x{self.handle:04X}, uuid={self.uuid})'
141
156
 
142
157
 
143
158
  class CharacteristicProxy(AttributeProxy):
144
159
  properties: Characteristic.Properties
145
160
  descriptors: List[DescriptorProxy]
146
- subscribers: Dict[Any, Callable]
161
+ subscribers: Dict[Any, Callable[[bytes], Any]]
147
162
 
148
163
  def __init__(
149
164
  self,
@@ -171,7 +186,9 @@ class CharacteristicProxy(AttributeProxy):
171
186
  return await self.client.discover_descriptors(self)
172
187
 
173
188
  async def subscribe(
174
- self, subscriber: Optional[Callable] = None, prefer_notify=True
189
+ self,
190
+ subscriber: Optional[Callable[[bytes], Any]] = None,
191
+ prefer_notify: bool = True,
175
192
  ):
176
193
  if subscriber is not None:
177
194
  if subscriber in self.subscribers:
@@ -195,7 +212,7 @@ class CharacteristicProxy(AttributeProxy):
195
212
 
196
213
  return await self.client.unsubscribe(self, subscriber)
197
214
 
198
- def __str__(self):
215
+ def __str__(self) -> str:
199
216
  return (
200
217
  f'Characteristic(handle=0x{self.handle:04X}, '
201
218
  f'uuid={self.uuid}, '
@@ -207,7 +224,7 @@ class DescriptorProxy(AttributeProxy):
207
224
  def __init__(self, client, handle, descriptor_type):
208
225
  super().__init__(client, handle, 0, descriptor_type)
209
226
 
210
- def __str__(self):
227
+ def __str__(self) -> str:
211
228
  return f'Descriptor(handle=0x{self.handle:04X}, type={self.type})'
212
229
 
213
230
 
@@ -216,8 +233,10 @@ class ProfileServiceProxy:
216
233
  Base class for profile-specific service proxies
217
234
  '''
218
235
 
236
+ SERVICE_CLASS: Type[TemplateService]
237
+
219
238
  @classmethod
220
- def from_client(cls, client):
239
+ def from_client(cls, client: Client) -> ProfileServiceProxy:
221
240
  return ServiceProxy.from_client(cls, client, cls.SERVICE_CLASS.UUID)
222
241
 
223
242
 
@@ -227,8 +246,12 @@ class ProfileServiceProxy:
227
246
  class Client:
228
247
  services: List[ServiceProxy]
229
248
  cached_values: Dict[int, Tuple[datetime, bytes]]
249
+ notification_subscribers: Dict[int, Callable[[bytes], Any]]
250
+ indication_subscribers: Dict[int, Callable[[bytes], Any]]
251
+ pending_response: Optional[asyncio.futures.Future[ATT_PDU]]
252
+ pending_request: Optional[ATT_PDU]
230
253
 
231
- def __init__(self, connection):
254
+ def __init__(self, connection: Connection) -> None:
232
255
  self.connection = connection
233
256
  self.mtu_exchange_done = False
234
257
  self.request_semaphore = asyncio.Semaphore(1)
@@ -241,16 +264,16 @@ class Client:
241
264
  self.services = []
242
265
  self.cached_values = {}
243
266
 
244
- def send_gatt_pdu(self, pdu):
267
+ def send_gatt_pdu(self, pdu: bytes) -> None:
245
268
  self.connection.send_l2cap_pdu(ATT_CID, pdu)
246
269
 
247
- async def send_command(self, command):
270
+ async def send_command(self, command: ATT_PDU) -> None:
248
271
  logger.debug(
249
272
  f'GATT Command from client: [0x{self.connection.handle:04X}] {command}'
250
273
  )
251
274
  self.send_gatt_pdu(command.to_bytes())
252
275
 
253
- async def send_request(self, request):
276
+ async def send_request(self, request: ATT_PDU):
254
277
  logger.debug(
255
278
  f'GATT Request from client: [0x{self.connection.handle:04X}] {request}'
256
279
  )
@@ -279,14 +302,14 @@ class Client:
279
302
 
280
303
  return response
281
304
 
282
- def send_confirmation(self, confirmation):
305
+ def send_confirmation(self, confirmation: ATT_Handle_Value_Confirmation) -> None:
283
306
  logger.debug(
284
307
  f'GATT Confirmation from client: [0x{self.connection.handle:04X}] '
285
308
  f'{confirmation}'
286
309
  )
287
310
  self.send_gatt_pdu(confirmation.to_bytes())
288
311
 
289
- async def request_mtu(self, mtu):
312
+ async def request_mtu(self, mtu: int) -> int:
290
313
  # Check the range
291
314
  if mtu < ATT_DEFAULT_MTU:
292
315
  raise ValueError(f'MTU must be >= {ATT_DEFAULT_MTU}')
@@ -313,10 +336,12 @@ class Client:
313
336
 
314
337
  return self.connection.att_mtu
315
338
 
316
- def get_services_by_uuid(self, uuid):
339
+ def get_services_by_uuid(self, uuid: UUID) -> List[ServiceProxy]:
317
340
  return [service for service in self.services if service.uuid == uuid]
318
341
 
319
- def get_characteristics_by_uuid(self, uuid, service=None):
342
+ def get_characteristics_by_uuid(
343
+ self, uuid: UUID, service: Optional[ServiceProxy] = None
344
+ ) -> List[CharacteristicProxy]:
320
345
  services = [service] if service else self.services
321
346
  return [
322
347
  c
@@ -363,7 +388,7 @@ class Client:
363
388
  if not already_known:
364
389
  self.services.append(service)
365
390
 
366
- async def discover_services(self, uuids=None) -> List[ServiceProxy]:
391
+ async def discover_services(self, uuids: Iterable[UUID] = []) -> List[ServiceProxy]:
367
392
  '''
368
393
  See Vol 3, Part G - 4.4.1 Discover All Primary Services
369
394
  '''
@@ -435,7 +460,7 @@ class Client:
435
460
 
436
461
  return services
437
462
 
438
- async def discover_service(self, uuid):
463
+ async def discover_service(self, uuid: Union[str, UUID]) -> List[ServiceProxy]:
439
464
  '''
440
465
  See Vol 3, Part G - 4.4.2 Discover Primary Service by Service UUID
441
466
  '''
@@ -468,7 +493,7 @@ class Client:
468
493
  f'{HCI_Constant.error_name(response.error_code)}'
469
494
  )
470
495
  # TODO raise appropriate exception
471
- return
496
+ return []
472
497
  break
473
498
 
474
499
  for attribute_handle, end_group_handle in response.handles_information:
@@ -480,7 +505,7 @@ class Client:
480
505
  logger.warning(
481
506
  f'bogus handle values: {attribute_handle} {end_group_handle}'
482
507
  )
483
- return
508
+ return []
484
509
 
485
510
  # Create a service proxy for this service
486
511
  service = ServiceProxy(
@@ -721,7 +746,7 @@ class Client:
721
746
 
722
747
  return descriptors
723
748
 
724
- async def discover_attributes(self):
749
+ async def discover_attributes(self) -> List[AttributeProxy]:
725
750
  '''
726
751
  Discover all attributes, regardless of type
727
752
  '''
@@ -844,7 +869,9 @@ class Client:
844
869
  # No more subscribers left
845
870
  await self.write_value(cccd, b'\x00\x00', with_response=True)
846
871
 
847
- async def read_value(self, attribute, no_long_read=False):
872
+ async def read_value(
873
+ self, attribute: Union[int, AttributeProxy], no_long_read: bool = False
874
+ ) -> Any:
848
875
  '''
849
876
  See Vol 3, Part G - 4.8.1 Read Characteristic Value
850
877
 
@@ -905,7 +932,9 @@ class Client:
905
932
  # Return the value as bytes
906
933
  return attribute_value
907
934
 
908
- async def read_characteristics_by_uuid(self, uuid, service):
935
+ async def read_characteristics_by_uuid(
936
+ self, uuid: UUID, service: Optional[ServiceProxy]
937
+ ) -> List[bytes]:
909
938
  '''
910
939
  See Vol 3, Part G - 4.8.2 Read Using Characteristic UUID
911
940
  '''
@@ -960,7 +989,12 @@ class Client:
960
989
 
961
990
  return characteristics_values
962
991
 
963
- async def write_value(self, attribute, value, with_response=False):
992
+ async def write_value(
993
+ self,
994
+ attribute: Union[int, AttributeProxy],
995
+ value: bytes,
996
+ with_response: bool = False,
997
+ ) -> None:
964
998
  '''
965
999
  See Vol 3, Part G - 4.9.1 Write Without Response & 4.9.3 Write Characteristic
966
1000
  Value
@@ -990,7 +1024,7 @@ class Client:
990
1024
  )
991
1025
  )
992
1026
 
993
- def on_gatt_pdu(self, att_pdu):
1027
+ def on_gatt_pdu(self, att_pdu: ATT_PDU) -> None:
994
1028
  logger.debug(
995
1029
  f'GATT Response to client: [0x{self.connection.handle:04X}] {att_pdu}'
996
1030
  )
@@ -1013,6 +1047,7 @@ class Client:
1013
1047
  return
1014
1048
 
1015
1049
  # Return the response to the coroutine that is waiting for it
1050
+ assert self.pending_response is not None
1016
1051
  self.pending_response.set_result(att_pdu)
1017
1052
  else:
1018
1053
  handler_name = f'on_{att_pdu.name.lower()}'
@@ -1060,7 +1095,7 @@ class Client:
1060
1095
  # Confirm that we received the indication
1061
1096
  self.send_confirmation(ATT_Handle_Value_Confirmation())
1062
1097
 
1063
- def cache_value(self, attribute_handle: int, value: bytes):
1098
+ def cache_value(self, attribute_handle: int, value: bytes) -> None:
1064
1099
  self.cached_values[attribute_handle] = (
1065
1100
  datetime.now(),
1066
1101
  value,
bumble/gatt_server.py CHANGED
@@ -23,11 +23,12 @@
23
23
  # -----------------------------------------------------------------------------
24
24
  # Imports
25
25
  # -----------------------------------------------------------------------------
26
+ from __future__ import annotations
26
27
  import asyncio
27
28
  import logging
28
29
  from collections import defaultdict
29
30
  import struct
30
- from typing import List, Tuple, Optional, TypeVar, Type
31
+ from typing import List, Tuple, Optional, TypeVar, Type, Dict, Iterable, TYPE_CHECKING
31
32
  from pyee import EventEmitter
32
33
 
33
34
  from .colors import color
@@ -42,6 +43,7 @@ from .att import (
42
43
  ATT_INVALID_OFFSET_ERROR,
43
44
  ATT_REQUEST_NOT_SUPPORTED_ERROR,
44
45
  ATT_REQUESTS,
46
+ ATT_PDU,
45
47
  ATT_UNLIKELY_ERROR_ERROR,
46
48
  ATT_UNSUPPORTED_GROUP_TYPE_ERROR,
47
49
  ATT_Error,
@@ -73,6 +75,8 @@ from .gatt import (
73
75
  Service,
74
76
  )
75
77
 
78
+ if TYPE_CHECKING:
79
+ from bumble.device import Device, Connection
76
80
 
77
81
  # -----------------------------------------------------------------------------
78
82
  # Logging
@@ -91,8 +95,13 @@ GATT_SERVER_DEFAULT_MAX_MTU = 517
91
95
  # -----------------------------------------------------------------------------
92
96
  class Server(EventEmitter):
93
97
  attributes: List[Attribute]
98
+ services: List[Service]
99
+ attributes_by_handle: Dict[int, Attribute]
100
+ subscribers: Dict[int, Dict[int, bytes]]
101
+ indication_semaphores: defaultdict[int, asyncio.Semaphore]
102
+ pending_confirmations: defaultdict[int, Optional[asyncio.futures.Future]]
94
103
 
95
- def __init__(self, device):
104
+ def __init__(self, device: Device) -> None:
96
105
  super().__init__()
97
106
  self.device = device
98
107
  self.services = []
@@ -107,16 +116,16 @@ class Server(EventEmitter):
107
116
  self.indication_semaphores = defaultdict(lambda: asyncio.Semaphore(1))
108
117
  self.pending_confirmations = defaultdict(lambda: None)
109
118
 
110
- def __str__(self):
119
+ def __str__(self) -> str:
111
120
  return "\n".join(map(str, self.attributes))
112
121
 
113
- def send_gatt_pdu(self, connection_handle, pdu):
122
+ def send_gatt_pdu(self, connection_handle: int, pdu: bytes) -> None:
114
123
  self.device.send_l2cap_pdu(connection_handle, ATT_CID, pdu)
115
124
 
116
- def next_handle(self):
125
+ def next_handle(self) -> int:
117
126
  return 1 + len(self.attributes)
118
127
 
119
- def get_advertising_service_data(self):
128
+ def get_advertising_service_data(self) -> Dict[Attribute, bytes]:
120
129
  return {
121
130
  attribute: data
122
131
  for attribute in self.attributes
@@ -124,7 +133,7 @@ class Server(EventEmitter):
124
133
  and (data := attribute.get_advertising_data())
125
134
  }
126
135
 
127
- def get_attribute(self, handle):
136
+ def get_attribute(self, handle: int) -> Optional[Attribute]:
128
137
  attribute = self.attributes_by_handle.get(handle)
129
138
  if attribute:
130
139
  return attribute
@@ -173,12 +182,17 @@ class Server(EventEmitter):
173
182
 
174
183
  return next(
175
184
  (
176
- (attribute, self.get_attribute(attribute.characteristic.handle))
185
+ (
186
+ attribute,
187
+ self.get_attribute(attribute.characteristic.handle),
188
+ ) # type: ignore
177
189
  for attribute in map(
178
190
  self.get_attribute,
179
191
  range(service_handle.handle, service_handle.end_group_handle + 1),
180
192
  )
181
- if attribute.type == GATT_CHARACTERISTIC_ATTRIBUTE_TYPE
193
+ if attribute is not None
194
+ and attribute.type == GATT_CHARACTERISTIC_ATTRIBUTE_TYPE
195
+ and isinstance(attribute, CharacteristicDeclaration)
182
196
  and attribute.characteristic.uuid == characteristic_uuid
183
197
  ),
184
198
  None,
@@ -197,7 +211,7 @@ class Server(EventEmitter):
197
211
 
198
212
  return next(
199
213
  (
200
- attribute
214
+ attribute # type: ignore
201
215
  for attribute in map(
202
216
  self.get_attribute,
203
217
  range(
@@ -205,12 +219,12 @@ class Server(EventEmitter):
205
219
  characteristic_value.end_group_handle + 1,
206
220
  ),
207
221
  )
208
- if attribute.type == descriptor_uuid
222
+ if attribute is not None and attribute.type == descriptor_uuid
209
223
  ),
210
224
  None,
211
225
  )
212
226
 
213
- def add_attribute(self, attribute):
227
+ def add_attribute(self, attribute: Attribute) -> None:
214
228
  # Assign a handle to this attribute
215
229
  attribute.handle = self.next_handle()
216
230
  attribute.end_group_handle = (
@@ -220,7 +234,7 @@ class Server(EventEmitter):
220
234
  # Add this attribute to the list
221
235
  self.attributes.append(attribute)
222
236
 
223
- def add_service(self, service: Service):
237
+ def add_service(self, service: Service) -> None:
224
238
  # Add the service attribute to the DB
225
239
  self.add_attribute(service)
226
240
 
@@ -285,11 +299,13 @@ class Server(EventEmitter):
285
299
  service.end_group_handle = self.attributes[-1].handle
286
300
  self.services.append(service)
287
301
 
288
- def add_services(self, services):
302
+ def add_services(self, services: Iterable[Service]) -> None:
289
303
  for service in services:
290
304
  self.add_service(service)
291
305
 
292
- def read_cccd(self, connection, characteristic):
306
+ def read_cccd(
307
+ self, connection: Optional[Connection], characteristic: Characteristic
308
+ ) -> bytes:
293
309
  if connection is None:
294
310
  return bytes([0, 0])
295
311
 
@@ -300,7 +316,12 @@ class Server(EventEmitter):
300
316
 
301
317
  return cccd or bytes([0, 0])
302
318
 
303
- def write_cccd(self, connection, characteristic, value):
319
+ def write_cccd(
320
+ self,
321
+ connection: Connection,
322
+ characteristic: Characteristic,
323
+ value: bytes,
324
+ ) -> None:
304
325
  logger.debug(
305
326
  f'Subscription update for connection=0x{connection.handle:04X}, '
306
327
  f'handle=0x{characteristic.handle:04X}: {value.hex()}'
@@ -327,13 +348,19 @@ class Server(EventEmitter):
327
348
  indicate_enabled,
328
349
  )
329
350
 
330
- def send_response(self, connection, response):
351
+ def send_response(self, connection: Connection, response: ATT_PDU) -> None:
331
352
  logger.debug(
332
353
  f'GATT Response from server: [0x{connection.handle:04X}] {response}'
333
354
  )
334
355
  self.send_gatt_pdu(connection.handle, response.to_bytes())
335
356
 
336
- async def notify_subscriber(self, connection, attribute, value=None, force=False):
357
+ async def notify_subscriber(
358
+ self,
359
+ connection: Connection,
360
+ attribute: Attribute,
361
+ value: Optional[bytes] = None,
362
+ force: bool = False,
363
+ ) -> None:
337
364
  # Check if there's a subscriber
338
365
  if not force:
339
366
  subscribers = self.subscribers.get(connection.handle)
@@ -370,7 +397,13 @@ class Server(EventEmitter):
370
397
  )
371
398
  self.send_gatt_pdu(connection.handle, bytes(notification))
372
399
 
373
- async def indicate_subscriber(self, connection, attribute, value=None, force=False):
400
+ async def indicate_subscriber(
401
+ self,
402
+ connection: Connection,
403
+ attribute: Attribute,
404
+ value: Optional[bytes] = None,
405
+ force: bool = False,
406
+ ) -> None:
374
407
  # Check if there's a subscriber
375
408
  if not force:
376
409
  subscribers = self.subscribers.get(connection.handle)
@@ -411,15 +444,13 @@ class Server(EventEmitter):
411
444
  assert self.pending_confirmations[connection.handle] is None
412
445
 
413
446
  # Create a future value to hold the eventual response
414
- self.pending_confirmations[
447
+ pending_confirmation = self.pending_confirmations[
415
448
  connection.handle
416
449
  ] = asyncio.get_running_loop().create_future()
417
450
 
418
451
  try:
419
452
  self.send_gatt_pdu(connection.handle, indication.to_bytes())
420
- await asyncio.wait_for(
421
- self.pending_confirmations[connection.handle], GATT_REQUEST_TIMEOUT
422
- )
453
+ await asyncio.wait_for(pending_confirmation, GATT_REQUEST_TIMEOUT)
423
454
  except asyncio.TimeoutError as error:
424
455
  logger.warning(color('!!! GATT Indicate timeout', 'red'))
425
456
  raise TimeoutError(f'GATT timeout for {indication.name}') from error
@@ -427,8 +458,12 @@ class Server(EventEmitter):
427
458
  self.pending_confirmations[connection.handle] = None
428
459
 
429
460
  async def notify_or_indicate_subscribers(
430
- self, indicate, attribute, value=None, force=False
431
- ):
461
+ self,
462
+ indicate: bool,
463
+ attribute: Attribute,
464
+ value: Optional[bytes] = None,
465
+ force: bool = False,
466
+ ) -> None:
432
467
  # Get all the connections for which there's at least one subscription
433
468
  connections = [
434
469
  connection
@@ -450,13 +485,23 @@ class Server(EventEmitter):
450
485
  ]
451
486
  )
452
487
 
453
- async def notify_subscribers(self, attribute, value=None, force=False):
488
+ async def notify_subscribers(
489
+ self,
490
+ attribute: Attribute,
491
+ value: Optional[bytes] = None,
492
+ force: bool = False,
493
+ ):
454
494
  return await self.notify_or_indicate_subscribers(False, attribute, value, force)
455
495
 
456
- async def indicate_subscribers(self, attribute, value=None, force=False):
496
+ async def indicate_subscribers(
497
+ self,
498
+ attribute: Attribute,
499
+ value: Optional[bytes] = None,
500
+ force: bool = False,
501
+ ):
457
502
  return await self.notify_or_indicate_subscribers(True, attribute, value, force)
458
503
 
459
- def on_disconnection(self, connection):
504
+ def on_disconnection(self, connection: Connection) -> None:
460
505
  if connection.handle in self.subscribers:
461
506
  del self.subscribers[connection.handle]
462
507
  if connection.handle in self.indication_semaphores:
@@ -464,7 +509,7 @@ class Server(EventEmitter):
464
509
  if connection.handle in self.pending_confirmations:
465
510
  del self.pending_confirmations[connection.handle]
466
511
 
467
- def on_gatt_pdu(self, connection, att_pdu):
512
+ def on_gatt_pdu(self, connection: Connection, att_pdu: ATT_PDU) -> None:
468
513
  logger.debug(f'GATT Request to server: [0x{connection.handle:04X}] {att_pdu}')
469
514
  handler_name = f'on_{att_pdu.name.lower()}'
470
515
  handler = getattr(self, handler_name, None)
@@ -506,7 +551,7 @@ class Server(EventEmitter):
506
551
  #######################################################
507
552
  # ATT handlers
508
553
  #######################################################
509
- def on_att_request(self, connection, pdu):
554
+ def on_att_request(self, connection: Connection, pdu: ATT_PDU) -> None:
510
555
  '''
511
556
  Handler for requests without a more specific handler
512
557
  '''
@@ -679,7 +724,6 @@ class Server(EventEmitter):
679
724
  and attribute.handle <= request.ending_handle
680
725
  and pdu_space_available
681
726
  ):
682
-
683
727
  try:
684
728
  attribute_value = attribute.read_value(connection)
685
729
  except ATT_Error as error: