bumble 0.0.180__py3-none-any.whl → 0.0.181__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.
@@ -19,12 +19,17 @@ like loading firmware after a cold start.
19
19
  # -----------------------------------------------------------------------------
20
20
  # Imports
21
21
  # -----------------------------------------------------------------------------
22
- import abc
22
+ from __future__ import annotations
23
23
  import logging
24
24
  import pathlib
25
25
  import platform
26
+ from typing import Dict, Iterable, Optional, Type, TYPE_CHECKING
27
+
26
28
  from . import rtk
29
+ from .common import Driver
27
30
 
31
+ if TYPE_CHECKING:
32
+ from bumble.host import Host
28
33
 
29
34
  # -----------------------------------------------------------------------------
30
35
  # Logging
@@ -32,40 +37,31 @@ from . import rtk
32
37
  logger = logging.getLogger(__name__)
33
38
 
34
39
 
35
- # -----------------------------------------------------------------------------
36
- # Classes
37
- # -----------------------------------------------------------------------------
38
- class Driver(abc.ABC):
39
- """Base class for drivers."""
40
-
41
- @staticmethod
42
- async def for_host(_host):
43
- """Return a driver instance for a host.
44
-
45
- Args:
46
- host: Host object for which a driver should be created.
47
-
48
- Returns:
49
- A Driver instance if a driver should be instantiated for this host, or
50
- None if no driver instance of this class is needed.
51
- """
52
- return None
53
-
54
- @abc.abstractmethod
55
- async def init_controller(self):
56
- """Initialize the controller."""
57
-
58
-
59
40
  # -----------------------------------------------------------------------------
60
41
  # Functions
61
42
  # -----------------------------------------------------------------------------
62
- async def get_driver_for_host(host):
63
- """Probe all known diver classes until one returns a valid instance for a host,
64
- or none is found.
43
+ async def get_driver_for_host(host: Host) -> Optional[Driver]:
44
+ """Probe diver classes until one returns a valid instance for a host, or none is
45
+ found.
46
+ If a "driver" HCI metadata entry is present, only that driver class will be probed.
65
47
  """
66
- if driver := await rtk.Driver.for_host(host):
67
- logger.debug("Instantiated RTK driver")
68
- return driver
48
+ driver_classes: Dict[str, Type[Driver]] = {"rtk": rtk.Driver}
49
+ probe_list: Iterable[str]
50
+ if driver_name := host.hci_metadata.get("driver"):
51
+ # Only probe a single driver
52
+ probe_list = [driver_name]
53
+ else:
54
+ # Probe all drivers
55
+ probe_list = driver_classes.keys()
56
+
57
+ for driver_name in probe_list:
58
+ if driver_class := driver_classes.get(driver_name):
59
+ logger.debug(f"Probing driver class: {driver_name}")
60
+ if driver := await driver_class.for_host(host):
61
+ logger.debug(f"Instantiated {driver_name} driver")
62
+ return driver
63
+ else:
64
+ logger.debug(f"Skipping unknown driver class: {driver_name}")
69
65
 
70
66
  return None
71
67
 
@@ -0,0 +1,45 @@
1
+ # Copyright 2021-2023 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
+ Common types for drivers.
16
+ """
17
+
18
+ # -----------------------------------------------------------------------------
19
+ # Imports
20
+ # -----------------------------------------------------------------------------
21
+ import abc
22
+
23
+
24
+ # -----------------------------------------------------------------------------
25
+ # Classes
26
+ # -----------------------------------------------------------------------------
27
+ class Driver(abc.ABC):
28
+ """Base class for drivers."""
29
+
30
+ @staticmethod
31
+ async def for_host(_host):
32
+ """Return a driver instance for a host.
33
+
34
+ Args:
35
+ host: Host object for which a driver should be created.
36
+
37
+ Returns:
38
+ A Driver instance if a driver should be instantiated for this host, or
39
+ None if no driver instance of this class is needed.
40
+ """
41
+ return None
42
+
43
+ @abc.abstractmethod
44
+ async def init_controller(self):
45
+ """Initialize the controller."""
bumble/drivers/rtk.py CHANGED
@@ -41,7 +41,7 @@ from bumble.hci import (
41
41
  HCI_Reset_Command,
42
42
  HCI_Read_Local_Version_Information_Command,
43
43
  )
44
-
44
+ from bumble.drivers import common
45
45
 
46
46
  # -----------------------------------------------------------------------------
47
47
  # Logging
@@ -285,7 +285,7 @@ class Firmware:
285
285
  )
286
286
 
287
287
 
288
- class Driver:
288
+ class Driver(common.Driver):
289
289
  @dataclass
290
290
  class DriverInfo:
291
291
  rom: int
@@ -470,8 +470,12 @@ class Driver:
470
470
  logger.debug("USB metadata not found")
471
471
  return False
472
472
 
473
- vendor_id = host.hci_metadata.get("vendor_id", None)
474
- product_id = host.hci_metadata.get("product_id", None)
473
+ if host.hci_metadata.get('driver') == 'rtk':
474
+ # Forced driver
475
+ return True
476
+
477
+ vendor_id = host.hci_metadata.get("vendor_id")
478
+ product_id = host.hci_metadata.get("product_id")
475
479
  if vendor_id is None or product_id is None:
476
480
  logger.debug("USB metadata not sufficient")
477
481
  return False
@@ -486,6 +490,9 @@ class Driver:
486
490
 
487
491
  @classmethod
488
492
  async def driver_info_for_host(cls, host):
493
+ await host.send_command(HCI_Reset_Command(), check_result=True)
494
+ host.ready = True # Needed to let the host know the controller is ready.
495
+
489
496
  response = await host.send_command(
490
497
  HCI_Read_Local_Version_Information_Command(), check_result=True
491
498
  )
bumble/gatt.py CHANGED
@@ -23,16 +23,28 @@
23
23
  # Imports
24
24
  # -----------------------------------------------------------------------------
25
25
  from __future__ import annotations
26
- import asyncio
27
26
  import enum
28
27
  import functools
29
28
  import logging
30
29
  import struct
31
- from typing import Optional, Sequence, Iterable, List, Union
32
-
33
- from .colors import color
34
- from .core import UUID, get_dict_key_by_value
35
- from .att import Attribute
30
+ from typing import (
31
+ Callable,
32
+ Dict,
33
+ Iterable,
34
+ List,
35
+ Optional,
36
+ Sequence,
37
+ Union,
38
+ TYPE_CHECKING,
39
+ )
40
+
41
+ from bumble.colors import color
42
+ from bumble.core import UUID
43
+ from bumble.att import Attribute, AttributeValue
44
+
45
+ if TYPE_CHECKING:
46
+ from bumble.gatt_client import AttributeProxy
47
+ from bumble.device import Connection
36
48
 
37
49
 
38
50
  # -----------------------------------------------------------------------------
@@ -368,9 +380,12 @@ class TemplateService(Service):
368
380
  UUID: UUID
369
381
 
370
382
  def __init__(
371
- self, characteristics: List[Characteristic], primary: bool = True
383
+ self,
384
+ characteristics: List[Characteristic],
385
+ primary: bool = True,
386
+ included_services: List[Service] = [],
372
387
  ) -> None:
373
- super().__init__(self.UUID, characteristics, primary)
388
+ super().__init__(self.UUID, characteristics, primary, included_services)
374
389
 
375
390
 
376
391
  # -----------------------------------------------------------------------------
@@ -519,56 +534,43 @@ class CharacteristicDeclaration(Attribute):
519
534
 
520
535
 
521
536
  # -----------------------------------------------------------------------------
522
- class CharacteristicValue:
523
- '''
524
- Characteristic value where reading and/or writing is delegated to functions
525
- passed as arguments to the constructor.
526
- '''
527
-
528
- def __init__(self, read=None, write=None):
529
- self._read = read
530
- self._write = write
531
-
532
- def read(self, connection):
533
- return self._read(connection) if self._read else b''
534
-
535
- def write(self, connection, value):
536
- if self._write:
537
- self._write(connection, value)
537
+ class CharacteristicValue(AttributeValue):
538
+ """Same as AttributeValue, for backward compatibility"""
538
539
 
539
540
 
540
541
  # -----------------------------------------------------------------------------
541
542
  class CharacteristicAdapter:
542
543
  '''
543
- An adapter that can adapt any object with `read_value` and `write_value`
544
- methods (like Characteristic and CharacteristicProxy objects) by wrapping
545
- those methods with ones that return/accept encoded/decoded values.
546
- Objects with async methods are considered proxies, so the adaptation is one
547
- where the return value of `read_value` is decoded and the value passed to
548
- `write_value` is encoded. Other objects are considered local characteristics
549
- so the adaptation is one where the return value of `read_value` is encoded
550
- and the value passed to `write_value` is decoded.
551
- If the characteristic has a `subscribe` method, it is wrapped with one where
552
- the values are decoded before being passed to the subscriber.
544
+ An adapter that can adapt Characteristic and AttributeProxy objects
545
+ by wrapping their `read_value()` and `write_value()` methods with ones that
546
+ return/accept encoded/decoded values.
547
+
548
+ For proxies (i.e used by a GATT client), the adaptation is one where the return
549
+ value of `read_value()` is decoded and the value passed to `write_value()` is
550
+ encoded. The `subscribe()` method, is wrapped with one where the values are decoded
551
+ before being passed to the subscriber.
552
+
553
+ For local values (i.e hosted by a GATT server) the adaptation is one where the
554
+ return value of `read_value()` is encoded and the value passed to `write_value()`
555
+ is decoded.
553
556
  '''
554
557
 
555
- def __init__(self, characteristic):
558
+ read_value: Callable
559
+ write_value: Callable
560
+
561
+ def __init__(self, characteristic: Union[Characteristic, AttributeProxy]):
556
562
  self.wrapped_characteristic = characteristic
557
- self.subscribers = {} # Map from subscriber to proxy subscriber
563
+ self.subscribers: Dict[
564
+ Callable, Callable
565
+ ] = {} # Map from subscriber to proxy subscriber
558
566
 
559
- if asyncio.iscoroutinefunction(
560
- characteristic.read_value
561
- ) and asyncio.iscoroutinefunction(characteristic.write_value):
562
- self.read_value = self.read_decoded_value
563
- self.write_value = self.write_decoded_value
564
- else:
567
+ if isinstance(characteristic, Characteristic):
565
568
  self.read_value = self.read_encoded_value
566
569
  self.write_value = self.write_encoded_value
567
-
568
- if hasattr(self.wrapped_characteristic, 'subscribe'):
570
+ else:
571
+ self.read_value = self.read_decoded_value
572
+ self.write_value = self.write_decoded_value
569
573
  self.subscribe = self.wrapped_subscribe
570
-
571
- if hasattr(self.wrapped_characteristic, 'unsubscribe'):
572
574
  self.unsubscribe = self.wrapped_unsubscribe
573
575
 
574
576
  def __getattr__(self, name):
@@ -587,11 +589,13 @@ class CharacteristicAdapter:
587
589
  else:
588
590
  setattr(self.wrapped_characteristic, name, value)
589
591
 
590
- def read_encoded_value(self, connection):
591
- return self.encode_value(self.wrapped_characteristic.read_value(connection))
592
+ async def read_encoded_value(self, connection):
593
+ return self.encode_value(
594
+ await self.wrapped_characteristic.read_value(connection)
595
+ )
592
596
 
593
- def write_encoded_value(self, connection, value):
594
- return self.wrapped_characteristic.write_value(
597
+ async def write_encoded_value(self, connection, value):
598
+ return await self.wrapped_characteristic.write_value(
595
599
  connection, self.decode_value(value)
596
600
  )
597
601
 
@@ -726,13 +730,24 @@ class Descriptor(Attribute):
726
730
  '''
727
731
 
728
732
  def __str__(self) -> str:
733
+ if isinstance(self.value, bytes):
734
+ value_str = self.value.hex()
735
+ elif isinstance(self.value, CharacteristicValue):
736
+ value = self.value.read(None)
737
+ if isinstance(value, bytes):
738
+ value_str = value.hex()
739
+ else:
740
+ value_str = '<async>'
741
+ else:
742
+ value_str = '<...>'
729
743
  return (
730
744
  f'Descriptor(handle=0x{self.handle:04X}, '
731
745
  f'type={self.type}, '
732
- f'value={self.read_value(None).hex()})'
746
+ f'value={value_str})'
733
747
  )
734
748
 
735
749
 
750
+ # -----------------------------------------------------------------------------
736
751
  class ClientCharacteristicConfigurationBits(enum.IntFlag):
737
752
  '''
738
753
  See Vol 3, Part G - 3.3.3.3 - Table 3.11 Client Characteristic Configuration bit
bumble/gatt_server.py CHANGED
@@ -31,9 +31,9 @@ import struct
31
31
  from typing import List, Tuple, Optional, TypeVar, Type, Dict, Iterable, TYPE_CHECKING
32
32
  from pyee import EventEmitter
33
33
 
34
- from .colors import color
35
- from .core import UUID
36
- from .att import (
34
+ from bumble.colors import color
35
+ from bumble.core import UUID
36
+ from bumble.att import (
37
37
  ATT_ATTRIBUTE_NOT_FOUND_ERROR,
38
38
  ATT_ATTRIBUTE_NOT_LONG_ERROR,
39
39
  ATT_CID,
@@ -60,7 +60,7 @@ from .att import (
60
60
  ATT_Write_Response,
61
61
  Attribute,
62
62
  )
63
- from .gatt import (
63
+ from bumble.gatt import (
64
64
  GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
65
65
  GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
66
66
  GATT_MAX_ATTRIBUTE_VALUE_SIZE,
@@ -74,6 +74,7 @@ from .gatt import (
74
74
  Descriptor,
75
75
  Service,
76
76
  )
77
+ from bumble.utils import AsyncRunner
77
78
 
78
79
  if TYPE_CHECKING:
79
80
  from bumble.device import Device, Connection
@@ -379,7 +380,7 @@ class Server(EventEmitter):
379
380
 
380
381
  # Get or encode the value
381
382
  value = (
382
- attribute.read_value(connection)
383
+ await attribute.read_value(connection)
383
384
  if value is None
384
385
  else attribute.encode_value(value)
385
386
  )
@@ -422,7 +423,7 @@ class Server(EventEmitter):
422
423
 
423
424
  # Get or encode the value
424
425
  value = (
425
- attribute.read_value(connection)
426
+ await attribute.read_value(connection)
426
427
  if value is None
427
428
  else attribute.encode_value(value)
428
429
  )
@@ -650,7 +651,8 @@ class Server(EventEmitter):
650
651
 
651
652
  self.send_response(connection, response)
652
653
 
653
- def on_att_find_by_type_value_request(self, connection, request):
654
+ @AsyncRunner.run_in_task()
655
+ async def on_att_find_by_type_value_request(self, connection, request):
654
656
  '''
655
657
  See Bluetooth spec Vol 3, Part F - 3.4.3.3 Find By Type Value Request
656
658
  '''
@@ -658,13 +660,13 @@ class Server(EventEmitter):
658
660
  # Build list of returned attributes
659
661
  pdu_space_available = connection.att_mtu - 2
660
662
  attributes = []
661
- for attribute in (
663
+ async for attribute in (
662
664
  attribute
663
665
  for attribute in self.attributes
664
666
  if attribute.handle >= request.starting_handle
665
667
  and attribute.handle <= request.ending_handle
666
668
  and attribute.type == request.attribute_type
667
- and attribute.read_value(connection) == request.attribute_value
669
+ and (await attribute.read_value(connection)) == request.attribute_value
668
670
  and pdu_space_available >= 4
669
671
  ):
670
672
  # TODO: check permissions
@@ -702,7 +704,8 @@ class Server(EventEmitter):
702
704
 
703
705
  self.send_response(connection, response)
704
706
 
705
- def on_att_read_by_type_request(self, connection, request):
707
+ @AsyncRunner.run_in_task()
708
+ async def on_att_read_by_type_request(self, connection, request):
706
709
  '''
707
710
  See Bluetooth spec Vol 3, Part F - 3.4.4.1 Read By Type Request
708
711
  '''
@@ -725,7 +728,7 @@ class Server(EventEmitter):
725
728
  and pdu_space_available
726
729
  ):
727
730
  try:
728
- attribute_value = attribute.read_value(connection)
731
+ attribute_value = await attribute.read_value(connection)
729
732
  except ATT_Error as error:
730
733
  # If the first attribute is unreadable, return an error
731
734
  # Otherwise return attributes up to this point
@@ -767,14 +770,15 @@ class Server(EventEmitter):
767
770
 
768
771
  self.send_response(connection, response)
769
772
 
770
- def on_att_read_request(self, connection, request):
773
+ @AsyncRunner.run_in_task()
774
+ async def on_att_read_request(self, connection, request):
771
775
  '''
772
776
  See Bluetooth spec Vol 3, Part F - 3.4.4.3 Read Request
773
777
  '''
774
778
 
775
779
  if attribute := self.get_attribute(request.attribute_handle):
776
780
  try:
777
- value = attribute.read_value(connection)
781
+ value = await attribute.read_value(connection)
778
782
  except ATT_Error as error:
779
783
  response = ATT_Error_Response(
780
784
  request_opcode_in_error=request.op_code,
@@ -792,14 +796,15 @@ class Server(EventEmitter):
792
796
  )
793
797
  self.send_response(connection, response)
794
798
 
795
- def on_att_read_blob_request(self, connection, request):
799
+ @AsyncRunner.run_in_task()
800
+ async def on_att_read_blob_request(self, connection, request):
796
801
  '''
797
802
  See Bluetooth spec Vol 3, Part F - 3.4.4.5 Read Blob Request
798
803
  '''
799
804
 
800
805
  if attribute := self.get_attribute(request.attribute_handle):
801
806
  try:
802
- value = attribute.read_value(connection)
807
+ value = await attribute.read_value(connection)
803
808
  except ATT_Error as error:
804
809
  response = ATT_Error_Response(
805
810
  request_opcode_in_error=request.op_code,
@@ -836,7 +841,8 @@ class Server(EventEmitter):
836
841
  )
837
842
  self.send_response(connection, response)
838
843
 
839
- def on_att_read_by_group_type_request(self, connection, request):
844
+ @AsyncRunner.run_in_task()
845
+ async def on_att_read_by_group_type_request(self, connection, request):
840
846
  '''
841
847
  See Bluetooth spec Vol 3, Part F - 3.4.4.9 Read by Group Type Request
842
848
  '''
@@ -864,7 +870,7 @@ class Server(EventEmitter):
864
870
  ):
865
871
  # No need to catch permission errors here, since these attributes
866
872
  # must all be world-readable
867
- attribute_value = attribute.read_value(connection)
873
+ attribute_value = await attribute.read_value(connection)
868
874
  # Check the attribute value size
869
875
  max_attribute_size = min(connection.att_mtu - 6, 251)
870
876
  if len(attribute_value) > max_attribute_size:
@@ -903,7 +909,8 @@ class Server(EventEmitter):
903
909
 
904
910
  self.send_response(connection, response)
905
911
 
906
- def on_att_write_request(self, connection, request):
912
+ @AsyncRunner.run_in_task()
913
+ async def on_att_write_request(self, connection, request):
907
914
  '''
908
915
  See Bluetooth spec Vol 3, Part F - 3.4.5.1 Write Request
909
916
  '''
@@ -936,12 +943,13 @@ class Server(EventEmitter):
936
943
  return
937
944
 
938
945
  # Accept the value
939
- attribute.write_value(connection, request.attribute_value)
946
+ await attribute.write_value(connection, request.attribute_value)
940
947
 
941
948
  # Done
942
949
  self.send_response(connection, ATT_Write_Response())
943
950
 
944
- def on_att_write_command(self, connection, request):
951
+ @AsyncRunner.run_in_task()
952
+ async def on_att_write_command(self, connection, request):
945
953
  '''
946
954
  See Bluetooth spec Vol 3, Part F - 3.4.5.3 Write Command
947
955
  '''
@@ -959,9 +967,9 @@ class Server(EventEmitter):
959
967
 
960
968
  # Accept the value
961
969
  try:
962
- attribute.write_value(connection, request.attribute_value)
970
+ await attribute.write_value(connection, request.attribute_value)
963
971
  except Exception as error:
964
- logger.warning(f'!!! ignoring exception: {error}')
972
+ logger.exception(f'!!! ignoring exception: {error}')
965
973
 
966
974
  def on_att_handle_value_confirmation(self, connection, _confirmation):
967
975
  '''