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/device.py CHANGED
@@ -53,7 +53,7 @@ from pyee import EventEmitter
53
53
 
54
54
  from .colors import color
55
55
  from .att import ATT_CID, ATT_DEFAULT_MTU, ATT_PDU
56
- from .gatt import Characteristic, Descriptor, Service
56
+ from .gatt import Attribute, Characteristic, Descriptor, Service
57
57
  from .host import DataPacketQueue, Host
58
58
  from .profiles.gap import GenericAccessService
59
59
  from .core import (
@@ -1569,8 +1569,8 @@ class Connection(CompositeEventEmitter):
1569
1569
  gatt_client: gatt_client.Client
1570
1570
  pairing_peer_io_capability: Optional[int]
1571
1571
  pairing_peer_authentication_requirements: Optional[int]
1572
- cs_configs: dict[int, ChannelSoundingConfig] = {} # Config ID to Configuration
1573
- cs_procedures: dict[int, ChannelSoundingProcedure] = {} # Config ID to Procedures
1572
+ cs_configs: dict[int, ChannelSoundingConfig] # Config ID to Configuration
1573
+ cs_procedures: dict[int, ChannelSoundingProcedure] # Config ID to Procedures
1574
1574
 
1575
1575
  @composite_listener
1576
1576
  class Listener:
@@ -1586,7 +1586,7 @@ class Connection(CompositeEventEmitter):
1586
1586
  def on_connection_data_length_change(self):
1587
1587
  pass
1588
1588
 
1589
- def on_connection_phy_update(self):
1589
+ def on_connection_phy_update(self, phy):
1590
1590
  pass
1591
1591
 
1592
1592
  def on_connection_phy_update_failure(self, error):
@@ -1612,7 +1612,6 @@ class Connection(CompositeEventEmitter):
1612
1612
  peer_resolvable_address,
1613
1613
  role,
1614
1614
  parameters,
1615
- phy,
1616
1615
  ):
1617
1616
  super().__init__()
1618
1617
  self.device = device
@@ -1629,7 +1628,6 @@ class Connection(CompositeEventEmitter):
1629
1628
  self.authenticated = False
1630
1629
  self.sc = False
1631
1630
  self.link_key_type = None
1632
- self.phy = phy
1633
1631
  self.att_mtu = ATT_DEFAULT_MTU
1634
1632
  self.data_length = DEVICE_DEFAULT_DATA_LENGTH
1635
1633
  self.gatt_client = None # Per-connection client
@@ -1639,6 +1637,8 @@ class Connection(CompositeEventEmitter):
1639
1637
  self.pairing_peer_io_capability = None
1640
1638
  self.pairing_peer_authentication_requirements = None
1641
1639
  self.peer_le_features = None
1640
+ self.cs_configs = {}
1641
+ self.cs_procedures = {}
1642
1642
 
1643
1643
  # [Classic only]
1644
1644
  @classmethod
@@ -1658,7 +1658,6 @@ class Connection(CompositeEventEmitter):
1658
1658
  None,
1659
1659
  role,
1660
1660
  None,
1661
- None,
1662
1661
  )
1663
1662
 
1664
1663
  # [Classic only]
@@ -1774,12 +1773,12 @@ class Connection(CompositeEventEmitter):
1774
1773
  async def set_phy(self, tx_phys=None, rx_phys=None, phy_options=None):
1775
1774
  return await self.device.set_connection_phy(self, tx_phys, rx_phys, phy_options)
1776
1775
 
1776
+ async def get_phy(self) -> ConnectionPHY:
1777
+ return await self.device.get_connection_phy(self)
1778
+
1777
1779
  async def get_rssi(self):
1778
1780
  return await self.device.get_connection_rssi(self)
1779
1781
 
1780
- async def get_phy(self):
1781
- return await self.device.get_connection_phy(self)
1782
-
1783
1782
  async def transfer_periodic_sync(
1784
1783
  self, sync_handle: int, service_data: int = 0
1785
1784
  ) -> None:
@@ -2049,9 +2048,9 @@ class Device(CompositeEventEmitter):
2049
2048
  legacy_advertiser: Optional[LegacyAdvertiser]
2050
2049
  sco_links: Dict[int, ScoLink]
2051
2050
  cis_links: Dict[int, CisLink]
2052
- bigs = dict[int, Big]()
2053
- bis_links = dict[int, BisLink]()
2054
- big_syncs = dict[int, BigSync]()
2051
+ bigs: dict[int, Big]
2052
+ bis_links: dict[int, BisLink]
2053
+ big_syncs: dict[int, BigSync]
2055
2054
  _pending_cis: Dict[int, tuple[int, int]]
2056
2055
  gatt_service: gatt_service.GenericAttributeProfileService | None = None
2057
2056
 
@@ -2144,6 +2143,9 @@ class Device(CompositeEventEmitter):
2144
2143
  self.sco_links = {} # ScoLinks, by connection handle (BR/EDR only)
2145
2144
  self.cis_links = {} # CisLinks, by connection handle (LE only)
2146
2145
  self._pending_cis = {} # (CIS_ID, CIG_ID), by CIS_handle
2146
+ self.bigs = {}
2147
+ self.bis_links = {}
2148
+ self.big_syncs = {}
2147
2149
  self.classic_enabled = False
2148
2150
  self.inquiry_response = None
2149
2151
  self.address_resolver = None
@@ -2221,7 +2223,7 @@ class Device(CompositeEventEmitter):
2221
2223
  permissions=descriptor["permissions"],
2222
2224
  )
2223
2225
  descriptors.append(new_descriptor)
2224
- new_characteristic = Characteristic(
2226
+ new_characteristic: Characteristic[bytes] = Characteristic(
2225
2227
  uuid=characteristic["uuid"],
2226
2228
  properties=Characteristic.Properties.from_string(
2227
2229
  characteristic["properties"]
@@ -3937,12 +3939,14 @@ class Device(CompositeEventEmitter):
3937
3939
  )
3938
3940
  return result.return_parameters.rssi
3939
3941
 
3940
- async def get_connection_phy(self, connection):
3942
+ async def get_connection_phy(self, connection: Connection) -> ConnectionPHY:
3941
3943
  result = await self.send_command(
3942
3944
  hci.HCI_LE_Read_PHY_Command(connection_handle=connection.handle),
3943
3945
  check_result=True,
3944
3946
  )
3945
- return (result.return_parameters.tx_phy, result.return_parameters.rx_phy)
3947
+ return ConnectionPHY(
3948
+ result.return_parameters.tx_phy, result.return_parameters.rx_phy
3949
+ )
3946
3950
 
3947
3951
  async def set_connection_phy(
3948
3952
  self, connection, tx_phys=None, rx_phys=None, phy_options=None
@@ -4006,13 +4010,12 @@ class Device(CompositeEventEmitter):
4006
4010
  # Create a future to wait for an address to be found
4007
4011
  peer_address = asyncio.get_running_loop().create_future()
4008
4012
 
4009
- def on_peer_found(address, ad_data):
4010
- local_name = ad_data.get(AdvertisingData.COMPLETE_LOCAL_NAME, raw=True)
4011
- if local_name is None:
4012
- local_name = ad_data.get(AdvertisingData.SHORTENED_LOCAL_NAME, raw=True)
4013
- if local_name is not None:
4014
- if local_name.decode('utf-8') == name:
4015
- peer_address.set_result(address)
4013
+ def on_peer_found(address: hci.Address, ad_data: AdvertisingData) -> None:
4014
+ local_name = ad_data.get(
4015
+ AdvertisingData.Type.COMPLETE_LOCAL_NAME
4016
+ ) or ad_data.get(AdvertisingData.Type.SHORTENED_LOCAL_NAME)
4017
+ if local_name == name:
4018
+ peer_address.set_result(address)
4016
4019
 
4017
4020
  listener = None
4018
4021
  was_scanning = self.scanning
@@ -4920,16 +4923,84 @@ class Device(CompositeEventEmitter):
4920
4923
  self.gatt_service = gatt_service.GenericAttributeProfileService()
4921
4924
  self.gatt_server.add_service(self.gatt_service)
4922
4925
 
4923
- async def notify_subscriber(self, connection, attribute, value=None, force=False):
4926
+ async def notify_subscriber(
4927
+ self,
4928
+ connection: Connection,
4929
+ attribute: Attribute,
4930
+ value: Optional[Any] = None,
4931
+ force: bool = False,
4932
+ ) -> None:
4933
+ """
4934
+ Send a notification to an attribute subscriber.
4935
+
4936
+ Args:
4937
+ connection:
4938
+ The connection of the subscriber.
4939
+ attribute:
4940
+ The attribute whose value is notified.
4941
+ value:
4942
+ The value of the attribute (if None, the value is read from the attribute)
4943
+ force:
4944
+ If True, send a notification even if there is no subscriber.
4945
+ """
4924
4946
  await self.gatt_server.notify_subscriber(connection, attribute, value, force)
4925
4947
 
4926
- async def notify_subscribers(self, attribute, value=None, force=False):
4948
+ async def notify_subscribers(
4949
+ self, attribute: Attribute, value=None, force=False
4950
+ ) -> None:
4951
+ """
4952
+ Send a notification to all the subscribers of an attribute.
4953
+
4954
+ Args:
4955
+ attribute:
4956
+ The attribute whose value is notified.
4957
+ value:
4958
+ The value of the attribute (if None, the value is read from the attribute)
4959
+ force:
4960
+ If True, send a notification for every connection even if there is no
4961
+ subscriber.
4962
+ """
4927
4963
  await self.gatt_server.notify_subscribers(attribute, value, force)
4928
4964
 
4929
- async def indicate_subscriber(self, connection, attribute, value=None, force=False):
4965
+ async def indicate_subscriber(
4966
+ self,
4967
+ connection: Connection,
4968
+ attribute: Attribute,
4969
+ value: Optional[Any] = None,
4970
+ force: bool = False,
4971
+ ):
4972
+ """
4973
+ Send an indication to an attribute subscriber.
4974
+
4975
+ This method returns when the response to the indication has been received.
4976
+
4977
+ Args:
4978
+ connection:
4979
+ The connection of the subscriber.
4980
+ attribute:
4981
+ The attribute whose value is indicated.
4982
+ value:
4983
+ The value of the attribute (if None, the value is read from the attribute)
4984
+ force:
4985
+ If True, send an indication even if there is no subscriber.
4986
+ """
4930
4987
  await self.gatt_server.indicate_subscriber(connection, attribute, value, force)
4931
4988
 
4932
- async def indicate_subscribers(self, attribute, value=None, force=False):
4989
+ async def indicate_subscribers(
4990
+ self, attribute: Attribute, value: Optional[Any] = None, force: bool = False
4991
+ ):
4992
+ """
4993
+ Send an indication to all the subscribers of an attribute.
4994
+
4995
+ Args:
4996
+ attribute:
4997
+ The attribute whose value is notified.
4998
+ value:
4999
+ The value of the attribute (if None, the value is read from the attribute)
5000
+ force:
5001
+ If True, send an indication for every connection even if there is no
5002
+ subscriber.
5003
+ """
4933
5004
  await self.gatt_server.indicate_subscribers(attribute, value, force)
4934
5005
 
4935
5006
  @host_event_handler
@@ -5101,29 +5172,6 @@ class Device(CompositeEventEmitter):
5101
5172
  lambda _: self.abort_on('flush', advertising_set.start()),
5102
5173
  )
5103
5174
 
5104
- self._emit_le_connection(connection)
5105
-
5106
- def _emit_le_connection(self, connection: Connection) -> None:
5107
- # If supported, read which PHY we're connected with before
5108
- # notifying listeners of the new connection.
5109
- if self.host.supports_command(hci.HCI_LE_READ_PHY_COMMAND):
5110
-
5111
- async def read_phy():
5112
- result = await self.send_command(
5113
- hci.HCI_LE_Read_PHY_Command(connection_handle=connection.handle),
5114
- check_result=True,
5115
- )
5116
- connection.phy = ConnectionPHY(
5117
- result.return_parameters.tx_phy, result.return_parameters.rx_phy
5118
- )
5119
- # Emit an event to notify listeners of the new connection
5120
- self.emit('connection', connection)
5121
-
5122
- # Do so asynchronously to not block the current event handler
5123
- connection.abort_on('disconnection', read_phy())
5124
-
5125
- return
5126
-
5127
5175
  self.emit('connection', connection)
5128
5176
 
5129
5177
  @host_event_handler
@@ -5222,7 +5270,6 @@ class Device(CompositeEventEmitter):
5222
5270
  peer_resolvable_address,
5223
5271
  role,
5224
5272
  connection_parameters,
5225
- ConnectionPHY(hci.HCI_LE_1M_PHY, hci.HCI_LE_1M_PHY),
5226
5273
  )
5227
5274
  self.connections[connection_handle] = connection
5228
5275
 
@@ -5238,7 +5285,7 @@ class Device(CompositeEventEmitter):
5238
5285
 
5239
5286
  if role == hci.HCI_CENTRAL_ROLE or not self.supports_le_extended_advertising:
5240
5287
  # We can emit now, we have all the info we need
5241
- self._emit_le_connection(connection)
5288
+ self.emit('connection', connection)
5242
5289
  return
5243
5290
 
5244
5291
  if role == hci.HCI_PERIPHERAL_ROLE and self.supports_le_extended_advertising:
@@ -5792,14 +5839,13 @@ class Device(CompositeEventEmitter):
5792
5839
 
5793
5840
  @host_event_handler
5794
5841
  @with_connection_from_handle
5795
- def on_connection_phy_update(self, connection, connection_phy):
5842
+ def on_connection_phy_update(self, connection, phy):
5796
5843
  logger.debug(
5797
5844
  f'*** Connection PHY Update: [0x{connection.handle:04X}] '
5798
5845
  f'{connection.peer_address} as {connection.role_name}, '
5799
- f'{connection_phy}'
5846
+ f'{phy}'
5800
5847
  )
5801
- connection.phy = connection_phy
5802
- connection.emit('connection_phy_update')
5848
+ connection.emit('connection_phy_update', phy)
5803
5849
 
5804
5850
  @host_event_handler
5805
5851
  @with_connection_from_handle
bumble/gatt.py CHANGED
@@ -27,28 +27,16 @@ import enum
27
27
  import functools
28
28
  import logging
29
29
  import struct
30
- from typing import (
31
- Any,
32
- Callable,
33
- Dict,
34
- Iterable,
35
- List,
36
- Optional,
37
- Sequence,
38
- SupportsBytes,
39
- Type,
40
- Union,
41
- TYPE_CHECKING,
42
- )
30
+ from typing import Iterable, List, Optional, Sequence, TypeVar, Union
43
31
 
44
32
  from bumble.colors import color
45
- from bumble.core import BaseBumbleError, InvalidOperationError, UUID
33
+ from bumble.core import BaseBumbleError, UUID
46
34
  from bumble.att import Attribute, AttributeValue
47
- from bumble.utils import ByteSerializable
48
-
49
- if TYPE_CHECKING:
50
- from bumble.gatt_client import AttributeProxy
51
35
 
36
+ # -----------------------------------------------------------------------------
37
+ # Typing
38
+ # -----------------------------------------------------------------------------
39
+ _T = TypeVar('_T')
52
40
 
53
41
  # -----------------------------------------------------------------------------
54
42
  # Logging
@@ -436,7 +424,7 @@ class IncludedServiceDeclaration(Attribute):
436
424
 
437
425
 
438
426
  # -----------------------------------------------------------------------------
439
- class Characteristic(Attribute):
427
+ class Characteristic(Attribute[_T]):
440
428
  '''
441
429
  See Vol 3, Part G - 3.3 CHARACTERISTIC DEFINITION
442
430
  '''
@@ -499,7 +487,7 @@ class Characteristic(Attribute):
499
487
  uuid: Union[str, bytes, UUID],
500
488
  properties: Characteristic.Properties,
501
489
  permissions: Union[str, Attribute.Permissions],
502
- value: Any = b'',
490
+ value: Union[AttributeValue[_T], _T, None] = None,
503
491
  descriptors: Sequence[Descriptor] = (),
504
492
  ):
505
493
  super().__init__(uuid, permissions, value)
@@ -559,217 +547,10 @@ class CharacteristicDeclaration(Attribute):
559
547
 
560
548
 
561
549
  # -----------------------------------------------------------------------------
562
- class CharacteristicValue(AttributeValue):
550
+ class CharacteristicValue(AttributeValue[_T]):
563
551
  """Same as AttributeValue, for backward compatibility"""
564
552
 
565
553
 
566
- # -----------------------------------------------------------------------------
567
- class CharacteristicAdapter:
568
- '''
569
- An adapter that can adapt Characteristic and AttributeProxy objects
570
- by wrapping their `read_value()` and `write_value()` methods with ones that
571
- return/accept encoded/decoded values.
572
-
573
- For proxies (i.e used by a GATT client), the adaptation is one where the return
574
- value of `read_value()` is decoded and the value passed to `write_value()` is
575
- encoded. The `subscribe()` method, is wrapped with one where the values are decoded
576
- before being passed to the subscriber.
577
-
578
- For local values (i.e hosted by a GATT server) the adaptation is one where the
579
- return value of `read_value()` is encoded and the value passed to `write_value()`
580
- is decoded.
581
- '''
582
-
583
- read_value: Callable
584
- write_value: Callable
585
-
586
- def __init__(self, characteristic: Union[Characteristic, AttributeProxy]):
587
- self.wrapped_characteristic = characteristic
588
- self.subscribers: Dict[Callable, Callable] = (
589
- {}
590
- ) # Map from subscriber to proxy subscriber
591
-
592
- if isinstance(characteristic, Characteristic):
593
- self.read_value = self.read_encoded_value
594
- self.write_value = self.write_encoded_value
595
- else:
596
- self.read_value = self.read_decoded_value
597
- self.write_value = self.write_decoded_value
598
- self.subscribe = self.wrapped_subscribe
599
- self.unsubscribe = self.wrapped_unsubscribe
600
-
601
- def __getattr__(self, name):
602
- return getattr(self.wrapped_characteristic, name)
603
-
604
- def __setattr__(self, name, value):
605
- if name in (
606
- 'wrapped_characteristic',
607
- 'subscribers',
608
- 'read_value',
609
- 'write_value',
610
- 'subscribe',
611
- 'unsubscribe',
612
- ):
613
- super().__setattr__(name, value)
614
- else:
615
- setattr(self.wrapped_characteristic, name, value)
616
-
617
- async def read_encoded_value(self, connection):
618
- return self.encode_value(
619
- await self.wrapped_characteristic.read_value(connection)
620
- )
621
-
622
- async def write_encoded_value(self, connection, value):
623
- return await self.wrapped_characteristic.write_value(
624
- connection, self.decode_value(value)
625
- )
626
-
627
- async def read_decoded_value(self):
628
- return self.decode_value(await self.wrapped_characteristic.read_value())
629
-
630
- async def write_decoded_value(self, value, with_response=False):
631
- return await self.wrapped_characteristic.write_value(
632
- self.encode_value(value), with_response
633
- )
634
-
635
- def encode_value(self, value):
636
- return value
637
-
638
- def decode_value(self, value):
639
- return value
640
-
641
- def wrapped_subscribe(self, subscriber=None):
642
- if subscriber is not None:
643
- if subscriber in self.subscribers:
644
- # We already have a proxy subscriber
645
- subscriber = self.subscribers[subscriber]
646
- else:
647
- # Create and register a proxy that will decode the value
648
- original_subscriber = subscriber
649
-
650
- def on_change(value):
651
- original_subscriber(self.decode_value(value))
652
-
653
- self.subscribers[subscriber] = on_change
654
- subscriber = on_change
655
-
656
- return self.wrapped_characteristic.subscribe(subscriber)
657
-
658
- def wrapped_unsubscribe(self, subscriber=None):
659
- if subscriber in self.subscribers:
660
- subscriber = self.subscribers.pop(subscriber)
661
-
662
- return self.wrapped_characteristic.unsubscribe(subscriber)
663
-
664
- def __str__(self) -> str:
665
- wrapped = str(self.wrapped_characteristic)
666
- return f'{self.__class__.__name__}({wrapped})'
667
-
668
-
669
- # -----------------------------------------------------------------------------
670
- class DelegatedCharacteristicAdapter(CharacteristicAdapter):
671
- '''
672
- Adapter that converts bytes values using an encode and a decode function.
673
- '''
674
-
675
- def __init__(self, characteristic, encode=None, decode=None):
676
- super().__init__(characteristic)
677
- self.encode = encode
678
- self.decode = decode
679
-
680
- def encode_value(self, value):
681
- if self.encode is None:
682
- raise InvalidOperationError('delegated adapter does not have an encoder')
683
- return self.encode(value)
684
-
685
- def decode_value(self, value):
686
- if self.decode is None:
687
- raise InvalidOperationError('delegate adapter does not have a decoder')
688
- return self.decode(value)
689
-
690
-
691
- # -----------------------------------------------------------------------------
692
- class PackedCharacteristicAdapter(CharacteristicAdapter):
693
- '''
694
- Adapter that packs/unpacks characteristic values according to a standard
695
- Python `struct` format.
696
- For formats with a single value, the adapted `read_value` and `write_value`
697
- methods return/accept single values. For formats with multiple values,
698
- they return/accept a tuple with the same number of elements as is required for
699
- the format.
700
- '''
701
-
702
- def __init__(self, characteristic, pack_format):
703
- super().__init__(characteristic)
704
- self.struct = struct.Struct(pack_format)
705
-
706
- def pack(self, *values):
707
- return self.struct.pack(*values)
708
-
709
- def unpack(self, buffer):
710
- return self.struct.unpack(buffer)
711
-
712
- def encode_value(self, value):
713
- return self.pack(*value if isinstance(value, tuple) else (value,))
714
-
715
- def decode_value(self, value):
716
- unpacked = self.unpack(value)
717
- return unpacked[0] if len(unpacked) == 1 else unpacked
718
-
719
-
720
- # -----------------------------------------------------------------------------
721
- class MappedCharacteristicAdapter(PackedCharacteristicAdapter):
722
- '''
723
- Adapter that packs/unpacks characteristic values according to a standard
724
- Python `struct` format.
725
- The adapted `read_value` and `write_value` methods return/accept a dictionary which
726
- is packed/unpacked according to format, with the arguments extracted from the
727
- dictionary by key, in the same order as they occur in the `keys` parameter.
728
- '''
729
-
730
- def __init__(self, characteristic, pack_format, keys):
731
- super().__init__(characteristic, pack_format)
732
- self.keys = keys
733
-
734
- # pylint: disable=arguments-differ
735
- def pack(self, values):
736
- return super().pack(*(values[key] for key in self.keys))
737
-
738
- def unpack(self, buffer):
739
- return dict(zip(self.keys, super().unpack(buffer)))
740
-
741
-
742
- # -----------------------------------------------------------------------------
743
- class UTF8CharacteristicAdapter(CharacteristicAdapter):
744
- '''
745
- Adapter that converts strings to/from bytes using UTF-8 encoding
746
- '''
747
-
748
- def encode_value(self, value: str) -> bytes:
749
- return value.encode('utf-8')
750
-
751
- def decode_value(self, value: bytes) -> str:
752
- return value.decode('utf-8')
753
-
754
-
755
- # -----------------------------------------------------------------------------
756
- class SerializableCharacteristicAdapter(CharacteristicAdapter):
757
- '''
758
- Adapter that converts any class to/from bytes using the class'
759
- `to_bytes` and `__bytes__` methods, respectively.
760
- '''
761
-
762
- def __init__(self, characteristic, cls: Type[ByteSerializable]):
763
- super().__init__(characteristic)
764
- self.cls = cls
765
-
766
- def encode_value(self, value: SupportsBytes) -> bytes:
767
- return bytes(value)
768
-
769
- def decode_value(self, value: bytes) -> Any:
770
- return self.cls.from_bytes(value)
771
-
772
-
773
554
  # -----------------------------------------------------------------------------
774
555
  class Descriptor(Attribute):
775
556
  '''