bumble 0.0.207__py3-none-any.whl → 0.0.209__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.
Files changed (47) hide show
  1. bumble/_version.py +9 -4
  2. bumble/apps/auracast.py +29 -35
  3. bumble/apps/bench.py +13 -10
  4. bumble/apps/console.py +19 -12
  5. bumble/apps/gg_bridge.py +1 -1
  6. bumble/att.py +61 -39
  7. bumble/controller.py +7 -8
  8. bumble/core.py +306 -159
  9. bumble/device.py +127 -82
  10. bumble/gatt.py +25 -228
  11. bumble/gatt_adapters.py +374 -0
  12. bumble/gatt_client.py +38 -31
  13. bumble/gatt_server.py +5 -5
  14. bumble/hci.py +76 -71
  15. bumble/host.py +19 -8
  16. bumble/l2cap.py +2 -2
  17. bumble/link.py +2 -2
  18. bumble/pairing.py +5 -5
  19. bumble/pandora/host.py +19 -23
  20. bumble/pandora/security.py +2 -3
  21. bumble/pandora/utils.py +2 -2
  22. bumble/profiles/aics.py +33 -23
  23. bumble/profiles/ancs.py +514 -0
  24. bumble/profiles/ascs.py +2 -1
  25. bumble/profiles/asha.py +11 -9
  26. bumble/profiles/bass.py +8 -5
  27. bumble/profiles/battery_service.py +13 -3
  28. bumble/profiles/device_information_service.py +16 -14
  29. bumble/profiles/gap.py +12 -8
  30. bumble/profiles/gatt_service.py +1 -0
  31. bumble/profiles/gmap.py +16 -11
  32. bumble/profiles/hap.py +8 -6
  33. bumble/profiles/heart_rate_service.py +20 -4
  34. bumble/profiles/mcp.py +11 -9
  35. bumble/profiles/pacs.py +37 -24
  36. bumble/profiles/tmap.py +6 -4
  37. bumble/profiles/vcs.py +6 -5
  38. bumble/profiles/vocs.py +49 -41
  39. bumble/smp.py +3 -3
  40. bumble/transport/usb.py +1 -3
  41. bumble/utils.py +10 -0
  42. {bumble-0.0.207.dist-info → bumble-0.0.209.dist-info}/METADATA +3 -3
  43. {bumble-0.0.207.dist-info → bumble-0.0.209.dist-info}/RECORD +47 -45
  44. {bumble-0.0.207.dist-info → bumble-0.0.209.dist-info}/WHEEL +1 -1
  45. {bumble-0.0.207.dist-info → bumble-0.0.209.dist-info}/LICENSE +0 -0
  46. {bumble-0.0.207.dist-info → bumble-0.0.209.dist-info}/entry_points.txt +0 -0
  47. {bumble-0.0.207.dist-info → bumble-0.0.209.dist-info}/top_level.txt +0 -0
bumble/_version.py CHANGED
@@ -1,8 +1,13 @@
1
- # file generated by setuptools_scm
1
+ # file generated by setuptools-scm
2
2
  # don't change, don't track in version control
3
+
4
+ __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
5
+
3
6
  TYPE_CHECKING = False
4
7
  if TYPE_CHECKING:
5
- from typing import Tuple, Union
8
+ from typing import Tuple
9
+ from typing import Union
10
+
6
11
  VERSION_TUPLE = Tuple[Union[int, str], ...]
7
12
  else:
8
13
  VERSION_TUPLE = object
@@ -12,5 +17,5 @@ __version__: str
12
17
  __version_tuple__: VERSION_TUPLE
13
18
  version_tuple: VERSION_TUPLE
14
19
 
15
- __version__ = version = '0.0.207'
16
- __version_tuple__ = version_tuple = (0, 0, 207)
20
+ __version__ = version = '0.0.209'
21
+ __version_tuple__ = version_tuple = (0, 0, 209)
bumble/apps/auracast.py CHANGED
@@ -27,7 +27,6 @@ import logging
27
27
  import os
28
28
  import struct
29
29
  from typing import (
30
- cast,
31
30
  Any,
32
31
  AsyncGenerator,
33
32
  Coroutine,
@@ -127,11 +126,9 @@ class BroadcastScanner(pyee.EventEmitter):
127
126
  def update(self, advertisement: bumble.device.Advertisement) -> None:
128
127
  self.rssi = advertisement.rssi
129
128
  for service_data in advertisement.data.get_all(
130
- core.AdvertisingData.SERVICE_DATA
129
+ core.AdvertisingData.Type.SERVICE_DATA_16_BIT_UUID
131
130
  ):
132
- assert isinstance(service_data, tuple)
133
131
  service_uuid, data = service_data
134
- assert isinstance(data, bytes)
135
132
 
136
133
  if service_uuid == gatt.GATT_PUBLIC_BROADCAST_ANNOUNCEMENT_SERVICE:
137
134
  self.public_broadcast_announcement = (
@@ -145,16 +142,14 @@ class BroadcastScanner(pyee.EventEmitter):
145
142
  )
146
143
  continue
147
144
 
148
- self.appearance = advertisement.data.get( # type: ignore[assignment]
149
- core.AdvertisingData.APPEARANCE
145
+ self.appearance = advertisement.data.get(
146
+ core.AdvertisingData.Type.APPEARANCE
150
147
  )
151
148
 
152
149
  if manufacturer_data := advertisement.data.get(
153
- core.AdvertisingData.MANUFACTURER_SPECIFIC_DATA
150
+ core.AdvertisingData.Type.MANUFACTURER_SPECIFIC_DATA
154
151
  ):
155
- assert isinstance(manufacturer_data, tuple)
156
- company_id = cast(int, manufacturer_data[0])
157
- data = cast(bytes, manufacturer_data[1])
152
+ company_id, data = manufacturer_data
158
153
  self.manufacturer_data = (
159
154
  company_ids.COMPANY_IDENTIFIERS.get(
160
155
  company_id, f'0x{company_id:04X}'
@@ -271,11 +266,9 @@ class BroadcastScanner(pyee.EventEmitter):
271
266
  return
272
267
 
273
268
  for service_data in advertisement.data.get_all(
274
- core.AdvertisingData.SERVICE_DATA
269
+ core.AdvertisingData.Type.SERVICE_DATA_16_BIT_UUID
275
270
  ):
276
- assert isinstance(service_data, tuple)
277
271
  service_uuid, data = service_data
278
- assert isinstance(data, bytes)
279
272
 
280
273
  if service_uuid == gatt.GATT_BASIC_AUDIO_ANNOUNCEMENT_SERVICE:
281
274
  self.basic_audio_announcement = (
@@ -316,24 +309,23 @@ class BroadcastScanner(pyee.EventEmitter):
316
309
  def on_advertisement(self, advertisement: bumble.device.Advertisement) -> None:
317
310
  if not (
318
311
  ads := advertisement.data.get_all(
319
- core.AdvertisingData.SERVICE_DATA_16_BIT_UUID
312
+ core.AdvertisingData.Type.SERVICE_DATA_16_BIT_UUID
320
313
  )
321
314
  ) or not (
322
315
  broadcast_audio_announcement := next(
323
316
  (
324
317
  ad
325
318
  for ad in ads
326
- if isinstance(ad, tuple)
327
- and ad[0] == gatt.GATT_BROADCAST_AUDIO_ANNOUNCEMENT_SERVICE
319
+ if ad[0] == gatt.GATT_BROADCAST_AUDIO_ANNOUNCEMENT_SERVICE
328
320
  ),
329
321
  None,
330
322
  )
331
323
  ):
332
324
  return
333
325
 
334
- broadcast_name = advertisement.data.get(core.AdvertisingData.BROADCAST_NAME)
335
- assert isinstance(broadcast_name, str) or broadcast_name is None
336
- assert isinstance(broadcast_audio_announcement[1], bytes)
326
+ broadcast_name = advertisement.data.get_all(
327
+ core.AdvertisingData.Type.BROADCAST_NAME
328
+ )
337
329
 
338
330
  if broadcast := self.broadcasts.get(advertisement.address):
339
331
  broadcast.update(advertisement)
@@ -341,7 +333,7 @@ class BroadcastScanner(pyee.EventEmitter):
341
333
 
342
334
  bumble.utils.AsyncRunner.spawn(
343
335
  self.on_new_broadcast(
344
- broadcast_name,
336
+ broadcast_name[0] if broadcast_name else None,
345
337
  advertisement,
346
338
  bap.BroadcastAudioAnnouncement.from_bytes(
347
339
  broadcast_audio_announcement[1]
@@ -522,14 +514,19 @@ async def run_assist(
522
514
  return
523
515
 
524
516
  # Subscribe to and read the broadcast receive state characteristics
517
+ def on_broadcast_receive_state_update(
518
+ value: bass.BroadcastReceiveState, index: int
519
+ ) -> None:
520
+ print(
521
+ f"{color(f'Broadcast Receive State Update [{index}]:', 'green')} {value}"
522
+ )
523
+
525
524
  for i, broadcast_receive_state in enumerate(
526
525
  bass_client.broadcast_receive_states
527
526
  ):
528
527
  try:
529
528
  await broadcast_receive_state.subscribe(
530
- lambda value, i=i: print(
531
- f"{color(f'Broadcast Receive State Update [{i}]:', 'green')} {value}"
532
- )
529
+ functools.partial(on_broadcast_receive_state_update, index=i)
533
530
  )
534
531
  except core.ProtocolError as error:
535
532
  print(
@@ -790,16 +787,8 @@ async def run_receive(
790
787
  for i, bis_link in enumerate(big_sync.bis_links):
791
788
  print(f'Setup ISO for BIS {bis_link.handle}')
792
789
  bis_link.sink = functools.partial(sink, lc3_queues[i])
793
- await device.send_command(
794
- hci.HCI_LE_Setup_ISO_Data_Path_Command(
795
- connection_handle=bis_link.handle,
796
- data_path_direction=hci.HCI_LE_Setup_ISO_Data_Path_Command.Direction.CONTROLLER_TO_HOST,
797
- data_path_id=0,
798
- codec_id=hci.CodingFormat(codec_id=hci.CodecID.TRANSPARENT),
799
- controller_delay=0,
800
- codec_configuration=b'',
801
- ),
802
- check_result=True,
790
+ await bis_link.setup_data_path(
791
+ direction=bis_link.Direction.CONTROLLER_TO_HOST
803
792
  )
804
793
 
805
794
  terminated = asyncio.Event()
@@ -959,10 +948,15 @@ async def run_transmit(
959
948
  ),
960
949
  ),
961
950
  )
951
+ for bis_link in big.bis_links:
952
+ print(f'Setup ISO for BIS {bis_link.handle}')
953
+ await bis_link.setup_data_path(
954
+ direction=bis_link.Direction.HOST_TO_CONTROLLER
955
+ )
962
956
 
963
957
  iso_queues = [
964
- bumble.device.IsoPacketStream(big.bis_links[0], 64),
965
- bumble.device.IsoPacketStream(big.bis_links[1], 64),
958
+ bumble.device.IsoPacketStream(bis_link, 64)
959
+ for bis_link in big.bis_links
966
960
  ]
967
961
 
968
962
  def on_flow():
bumble/apps/bench.py CHANGED
@@ -104,15 +104,16 @@ def le_phy_name(phy_id):
104
104
  )
105
105
 
106
106
 
107
+ def print_connection_phy(phy):
108
+ logging.info(
109
+ color('@@@ PHY: ', 'yellow') + f'TX:{le_phy_name(phy.tx_phy)}/'
110
+ f'RX:{le_phy_name(phy.rx_phy)}'
111
+ )
112
+
113
+
107
114
  def print_connection(connection):
108
115
  params = []
109
116
  if connection.transport == BT_LE_TRANSPORT:
110
- params.append(
111
- 'PHY='
112
- f'TX:{le_phy_name(connection.phy.tx_phy)}/'
113
- f'RX:{le_phy_name(connection.phy.rx_phy)}'
114
- )
115
-
116
117
  params.append(
117
118
  'DL=('
118
119
  f'TX:{connection.data_length[0]}/{connection.data_length[1]},'
@@ -1288,6 +1289,8 @@ class Central(Connection.Listener):
1288
1289
  logging.info(color('### Connected', 'cyan'))
1289
1290
  self.connection.listener = self
1290
1291
  print_connection(self.connection)
1292
+ phy = await self.connection.get_phy()
1293
+ print_connection_phy(phy)
1291
1294
 
1292
1295
  # Switch roles if needed.
1293
1296
  if self.role_switch:
@@ -1345,8 +1348,8 @@ class Central(Connection.Listener):
1345
1348
  def on_connection_parameters_update(self):
1346
1349
  print_connection(self.connection)
1347
1350
 
1348
- def on_connection_phy_update(self):
1349
- print_connection(self.connection)
1351
+ def on_connection_phy_update(self, phy):
1352
+ print_connection_phy(phy)
1350
1353
 
1351
1354
  def on_connection_att_mtu_update(self):
1352
1355
  print_connection(self.connection)
@@ -1472,8 +1475,8 @@ class Peripheral(Device.Listener, Connection.Listener):
1472
1475
  def on_connection_parameters_update(self):
1473
1476
  print_connection(self.connection)
1474
1477
 
1475
- def on_connection_phy_update(self):
1476
- print_connection(self.connection)
1478
+ def on_connection_phy_update(self, phy):
1479
+ print_connection_phy(phy)
1477
1480
 
1478
1481
  def on_connection_att_mtu_update(self):
1479
1482
  print_connection(self.connection)
bumble/apps/console.py CHANGED
@@ -22,7 +22,6 @@
22
22
  import asyncio
23
23
  import logging
24
24
  import os
25
- import random
26
25
  import re
27
26
  import humanize
28
27
  from typing import Optional, Union
@@ -57,7 +56,13 @@ from bumble import __version__
57
56
  import bumble.core
58
57
  from bumble import colors
59
58
  from bumble.core import UUID, AdvertisingData, BT_LE_TRANSPORT
60
- from bumble.device import ConnectionParametersPreferences, Device, Connection, Peer
59
+ from bumble.device import (
60
+ ConnectionParametersPreferences,
61
+ ConnectionPHY,
62
+ Device,
63
+ Connection,
64
+ Peer,
65
+ )
61
66
  from bumble.utils import AsyncRunner
62
67
  from bumble.transport import open_transport_or_link
63
68
  from bumble.gatt import Characteristic, Service, CharacteristicDeclaration, Descriptor
@@ -125,6 +130,7 @@ def parse_phys(phys):
125
130
  # -----------------------------------------------------------------------------
126
131
  class ConsoleApp:
127
132
  connected_peer: Optional[Peer]
133
+ connection_phy: Optional[ConnectionPHY]
128
134
 
129
135
  def __init__(self):
130
136
  self.known_addresses = set()
@@ -132,6 +138,7 @@ class ConsoleApp:
132
138
  self.known_local_attributes = []
133
139
  self.device = None
134
140
  self.connected_peer = None
141
+ self.connection_phy = None
135
142
  self.top_tab = 'device'
136
143
  self.monitor_rssi = False
137
144
  self.connection_rssi = None
@@ -332,10 +339,10 @@ class ConsoleApp:
332
339
  f'{connection.parameters.peripheral_latency}/'
333
340
  f'{connection.parameters.supervision_timeout}'
334
341
  )
335
- if connection.transport == BT_LE_TRANSPORT:
342
+ if self.connection_phy is not None:
336
343
  phy_state = (
337
- f' RX={le_phy_name(connection.phy.rx_phy)}/'
338
- f'TX={le_phy_name(connection.phy.tx_phy)}'
344
+ f' RX={le_phy_name(self.connection_phy.rx_phy)}/'
345
+ f'TX={le_phy_name(self.connection_phy.tx_phy)}'
339
346
  )
340
347
  else:
341
348
  phy_state = ''
@@ -654,11 +661,12 @@ class ConsoleApp:
654
661
  self.append_to_output('connecting...')
655
662
 
656
663
  try:
657
- await self.device.connect(
664
+ connection = await self.device.connect(
658
665
  params[0],
659
666
  connection_parameters_preferences=connection_parameters_preferences,
660
667
  timeout=DEFAULT_CONNECTION_TIMEOUT,
661
668
  )
669
+ self.connection_phy = await connection.get_phy()
662
670
  self.top_tab = 'services'
663
671
  except bumble.core.TimeoutError:
664
672
  self.show_error('connection timed out')
@@ -838,8 +846,8 @@ class ConsoleApp:
838
846
 
839
847
  phy = await self.connected_peer.connection.get_phy()
840
848
  self.append_to_output(
841
- f'PHY: RX={HCI_Constant.le_phy_name(phy[0])}, '
842
- f'TX={HCI_Constant.le_phy_name(phy[1])}'
849
+ f'PHY: RX={HCI_Constant.le_phy_name(phy.rx_phy)}, '
850
+ f'TX={HCI_Constant.le_phy_name(phy.tx_phy)}'
843
851
  )
844
852
 
845
853
  async def do_request_mtu(self, params):
@@ -1076,10 +1084,9 @@ class DeviceListener(Device.Listener, Connection.Listener):
1076
1084
  f'{self.app.connected_peer.connection.parameters}'
1077
1085
  )
1078
1086
 
1079
- def on_connection_phy_update(self):
1080
- self.app.append_to_output(
1081
- f'connection phy update: {self.app.connected_peer.connection.phy}'
1082
- )
1087
+ def on_connection_phy_update(self, phy):
1088
+ self.app.connection_phy = phy
1089
+ self.app.append_to_output(f'connection phy update: {phy}')
1083
1090
 
1084
1091
  def on_connection_att_mtu_update(self):
1085
1092
  self.app.append_to_output(
bumble/apps/gg_bridge.py CHANGED
@@ -234,7 +234,7 @@ class GattlinkNodeBridge(GattlinkL2capEndpoint, Device.Listener):
234
234
  Characteristic.WRITEABLE,
235
235
  CharacteristicValue(write=self.on_rx_write),
236
236
  )
237
- self.tx_characteristic = Characteristic(
237
+ self.tx_characteristic: Characteristic[bytes] = Characteristic(
238
238
  GG_GATTLINK_TX_CHARACTERISTIC_UUID,
239
239
  Characteristic.Properties.NOTIFY,
240
240
  Characteristic.READABLE,
bumble/att.py CHANGED
@@ -29,13 +29,14 @@ import functools
29
29
  import inspect
30
30
  import struct
31
31
  from typing import (
32
- Any,
33
32
  Awaitable,
34
33
  Callable,
34
+ Generic,
35
35
  Dict,
36
36
  List,
37
37
  Optional,
38
38
  Type,
39
+ TypeVar,
39
40
  Union,
40
41
  TYPE_CHECKING,
41
42
  )
@@ -43,13 +44,18 @@ from typing import (
43
44
  from pyee import EventEmitter
44
45
 
45
46
  from bumble import utils
46
- from bumble.core import UUID, name_or_number, ProtocolError
47
+ from bumble.core import UUID, name_or_number, InvalidOperationError, ProtocolError
47
48
  from bumble.hci import HCI_Object, key_with_value
48
49
  from bumble.colors import color
49
50
 
51
+ # -----------------------------------------------------------------------------
52
+ # Typing
53
+ # -----------------------------------------------------------------------------
50
54
  if TYPE_CHECKING:
51
55
  from bumble.device import Connection
52
56
 
57
+ _T = TypeVar('_T')
58
+
53
59
  # -----------------------------------------------------------------------------
54
60
  # Constants
55
61
  # -----------------------------------------------------------------------------
@@ -217,7 +223,12 @@ UUID_2_FIELD_SPEC = lambda x, y: UUID.parse_uuid_2(x, y) # noqa: E731
217
223
  # Exceptions
218
224
  # -----------------------------------------------------------------------------
219
225
  class ATT_Error(ProtocolError):
220
- def __init__(self, error_code, att_handle=0x0000, message=''):
226
+ error_code: int
227
+ att_handle: int
228
+
229
+ def __init__(
230
+ self, error_code: int, att_handle: int = 0x0000, message: str = ''
231
+ ) -> None:
221
232
  super().__init__(
222
233
  error_code,
223
234
  error_namespace='att',
@@ -227,7 +238,10 @@ class ATT_Error(ProtocolError):
227
238
  self.message = message
228
239
 
229
240
  def __str__(self):
230
- return f'ATT_Error(error={self.error_name}, handle={self.att_handle:04X}): {self.message}'
241
+ return (
242
+ f'ATT_Error(error={self.error_name}, '
243
+ f'handle={self.att_handle:04X}): {self.message}'
244
+ )
231
245
 
232
246
 
233
247
  # -----------------------------------------------------------------------------
@@ -748,7 +762,7 @@ class ATT_Handle_Value_Confirmation(ATT_PDU):
748
762
 
749
763
 
750
764
  # -----------------------------------------------------------------------------
751
- class AttributeValue:
765
+ class AttributeValue(Generic[_T]):
752
766
  '''
753
767
  Attribute value where reading and/or writing is delegated to functions
754
768
  passed as arguments to the constructor.
@@ -757,33 +771,34 @@ class AttributeValue:
757
771
  def __init__(
758
772
  self,
759
773
  read: Union[
760
- Callable[[Optional[Connection]], Any],
761
- Callable[[Optional[Connection]], Awaitable[Any]],
774
+ Callable[[Optional[Connection]], _T],
775
+ Callable[[Optional[Connection]], Awaitable[_T]],
762
776
  None,
763
777
  ] = None,
764
778
  write: Union[
765
- Callable[[Optional[Connection], Any], None],
766
- Callable[[Optional[Connection], Any], Awaitable[None]],
779
+ Callable[[Optional[Connection], _T], None],
780
+ Callable[[Optional[Connection], _T], Awaitable[None]],
767
781
  None,
768
782
  ] = None,
769
783
  ):
770
784
  self._read = read
771
785
  self._write = write
772
786
 
773
- def read(self, connection: Optional[Connection]) -> Union[bytes, Awaitable[bytes]]:
774
- return self._read(connection) if self._read else b''
787
+ def read(self, connection: Optional[Connection]) -> Union[_T, Awaitable[_T]]:
788
+ if self._read is None:
789
+ raise InvalidOperationError('AttributeValue has no read function')
790
+ return self._read(connection)
775
791
 
776
792
  def write(
777
- self, connection: Optional[Connection], value: bytes
793
+ self, connection: Optional[Connection], value: _T
778
794
  ) -> Union[Awaitable[None], None]:
779
- if self._write:
780
- return self._write(connection, value)
781
-
782
- return None
795
+ if self._write is None:
796
+ raise InvalidOperationError('AttributeValue has no write function')
797
+ return self._write(connection, value)
783
798
 
784
799
 
785
800
  # -----------------------------------------------------------------------------
786
- class Attribute(EventEmitter):
801
+ class Attribute(EventEmitter, Generic[_T]):
787
802
  class Permissions(enum.IntFlag):
788
803
  READABLE = 0x01
789
804
  WRITEABLE = 0x02
@@ -822,13 +837,13 @@ class Attribute(EventEmitter):
822
837
  READ_REQUIRES_AUTHORIZATION = Permissions.READ_REQUIRES_AUTHORIZATION
823
838
  WRITE_REQUIRES_AUTHORIZATION = Permissions.WRITE_REQUIRES_AUTHORIZATION
824
839
 
825
- value: Any
840
+ value: Union[AttributeValue[_T], _T, None]
826
841
 
827
842
  def __init__(
828
843
  self,
829
844
  attribute_type: Union[str, bytes, UUID],
830
845
  permissions: Union[str, Attribute.Permissions],
831
- value: Any = b'',
846
+ value: Union[AttributeValue[_T], _T, None] = None,
832
847
  ) -> None:
833
848
  EventEmitter.__init__(self)
834
849
  self.handle = 0
@@ -848,11 +863,11 @@ class Attribute(EventEmitter):
848
863
 
849
864
  self.value = value
850
865
 
851
- def encode_value(self, value: Any) -> bytes:
852
- return value
866
+ def encode_value(self, value: _T) -> bytes:
867
+ return value # type: ignore
853
868
 
854
- def decode_value(self, value_bytes: bytes) -> Any:
855
- return value_bytes
869
+ def decode_value(self, value: bytes) -> _T:
870
+ return value # type: ignore
856
871
 
857
872
  async def read_value(self, connection: Optional[Connection]) -> bytes:
858
873
  if (
@@ -877,11 +892,14 @@ class Attribute(EventEmitter):
877
892
  error_code=ATT_INSUFFICIENT_AUTHORIZATION_ERROR, att_handle=self.handle
878
893
  )
879
894
 
880
- if hasattr(self.value, 'read'):
895
+ value: Union[_T, None]
896
+ if isinstance(self.value, AttributeValue):
881
897
  try:
882
- value = self.value.read(connection)
883
- if inspect.isawaitable(value):
884
- value = await value
898
+ read_value = self.value.read(connection)
899
+ if inspect.isawaitable(read_value):
900
+ value = await read_value
901
+ else:
902
+ value = read_value
885
903
  except ATT_Error as error:
886
904
  raise ATT_Error(
887
905
  error_code=error.error_code, att_handle=self.handle
@@ -889,20 +907,24 @@ class Attribute(EventEmitter):
889
907
  else:
890
908
  value = self.value
891
909
 
892
- self.emit('read', connection, value)
910
+ self.emit('read', connection, b'' if value is None else value)
893
911
 
894
- return self.encode_value(value)
912
+ return b'' if value is None else self.encode_value(value)
895
913
 
896
- async def write_value(self, connection: Connection, value_bytes: bytes) -> None:
914
+ async def write_value(self, connection: Optional[Connection], value: bytes) -> None:
897
915
  if (
898
- self.permissions & self.WRITE_REQUIRES_ENCRYPTION
899
- ) and not connection.encryption:
916
+ (self.permissions & self.WRITE_REQUIRES_ENCRYPTION)
917
+ and connection is not None
918
+ and not connection.encryption
919
+ ):
900
920
  raise ATT_Error(
901
921
  error_code=ATT_INSUFFICIENT_ENCRYPTION_ERROR, att_handle=self.handle
902
922
  )
903
923
  if (
904
- self.permissions & self.WRITE_REQUIRES_AUTHENTICATION
905
- ) and not connection.authenticated:
924
+ (self.permissions & self.WRITE_REQUIRES_AUTHENTICATION)
925
+ and connection is not None
926
+ and not connection.authenticated
927
+ ):
906
928
  raise ATT_Error(
907
929
  error_code=ATT_INSUFFICIENT_AUTHENTICATION_ERROR, att_handle=self.handle
908
930
  )
@@ -912,11 +934,11 @@ class Attribute(EventEmitter):
912
934
  error_code=ATT_INSUFFICIENT_AUTHORIZATION_ERROR, att_handle=self.handle
913
935
  )
914
936
 
915
- value = self.decode_value(value_bytes)
937
+ decoded_value = self.decode_value(value)
916
938
 
917
- if hasattr(self.value, 'write'):
939
+ if isinstance(self.value, AttributeValue):
918
940
  try:
919
- result = self.value.write(connection, value)
941
+ result = self.value.write(connection, decoded_value)
920
942
  if inspect.isawaitable(result):
921
943
  await result
922
944
  except ATT_Error as error:
@@ -924,9 +946,9 @@ class Attribute(EventEmitter):
924
946
  error_code=error.error_code, att_handle=self.handle
925
947
  ) from error
926
948
  else:
927
- self.value = value
949
+ self.value = decoded_value
928
950
 
929
- self.emit('write', connection, value)
951
+ self.emit('write', connection, decoded_value)
930
952
 
931
953
  def __repr__(self):
932
954
  if isinstance(self.value, bytes):
bumble/controller.py CHANGED
@@ -25,8 +25,6 @@ import random
25
25
  import struct
26
26
  from bumble.colors import color
27
27
  from bumble.core import (
28
- BT_CENTRAL_ROLE,
29
- BT_PERIPHERAL_ROLE,
30
28
  BT_LE_TRANSPORT,
31
29
  BT_BR_EDR_TRANSPORT,
32
30
  )
@@ -47,6 +45,7 @@ from bumble.hci import (
47
45
  HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR,
48
46
  HCI_VERSION_BLUETOOTH_CORE_5_0,
49
47
  Address,
48
+ Role,
50
49
  HCI_AclDataPacket,
51
50
  HCI_AclDataPacketAssembler,
52
51
  HCI_Command_Complete_Event,
@@ -98,7 +97,7 @@ class CisLink:
98
97
  class Connection:
99
98
  controller: Controller
100
99
  handle: int
101
- role: int
100
+ role: Role
102
101
  peer_address: Address
103
102
  link: Any
104
103
  transport: int
@@ -390,7 +389,7 @@ class Controller:
390
389
  connection = Connection(
391
390
  controller=self,
392
391
  handle=connection_handle,
393
- role=BT_PERIPHERAL_ROLE,
392
+ role=Role.PERIPHERAL,
394
393
  peer_address=peer_address,
395
394
  link=self.link,
396
395
  transport=BT_LE_TRANSPORT,
@@ -450,7 +449,7 @@ class Controller:
450
449
  connection = Connection(
451
450
  controller=self,
452
451
  handle=connection_handle,
453
- role=BT_CENTRAL_ROLE,
452
+ role=Role.CENTRAL,
454
453
  peer_address=peer_address,
455
454
  link=self.link,
456
455
  transport=BT_LE_TRANSPORT,
@@ -469,7 +468,7 @@ class Controller:
469
468
  HCI_LE_Connection_Complete_Event(
470
469
  status=status,
471
470
  connection_handle=connection.handle if connection else 0,
472
- role=BT_CENTRAL_ROLE,
471
+ role=Role.CENTRAL,
473
472
  peer_address_type=le_create_connection_command.peer_address_type,
474
473
  peer_address=le_create_connection_command.peer_address,
475
474
  connection_interval=le_create_connection_command.connection_interval_min,
@@ -693,7 +692,7 @@ class Controller:
693
692
  controller=self,
694
693
  handle=connection_handle,
695
694
  # Role doesn't matter in Classic because they are managed by HCI_Role_Change and HCI_Role_Discovery
696
- role=BT_CENTRAL_ROLE,
695
+ role=Role.CENTRAL,
697
696
  peer_address=peer_address,
698
697
  link=self.link,
699
698
  transport=BT_BR_EDR_TRANSPORT,
@@ -761,7 +760,7 @@ class Controller:
761
760
  controller=self,
762
761
  handle=connection_handle,
763
762
  # Role doesn't matter in SCO.
764
- role=BT_CENTRAL_ROLE,
763
+ role=Role.CENTRAL,
765
764
  peer_address=peer_address,
766
765
  link=self.link,
767
766
  transport=BT_BR_EDR_TRANSPORT,