bumble 0.0.203__py3-none-any.whl → 0.0.204__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.py CHANGED
@@ -28,12 +28,15 @@ import functools
28
28
  import logging
29
29
  import struct
30
30
  from typing import (
31
+ Any,
31
32
  Callable,
32
33
  Dict,
33
34
  Iterable,
34
35
  List,
35
36
  Optional,
36
37
  Sequence,
38
+ SupportsBytes,
39
+ Type,
37
40
  Union,
38
41
  TYPE_CHECKING,
39
42
  )
@@ -41,6 +44,7 @@ from typing import (
41
44
  from bumble.colors import color
42
45
  from bumble.core import BaseBumbleError, UUID
43
46
  from bumble.att import Attribute, AttributeValue
47
+ from bumble.utils import ByteSerializable
44
48
 
45
49
  if TYPE_CHECKING:
46
50
  from bumble.gatt_client import AttributeProxy
@@ -343,7 +347,7 @@ class Service(Attribute):
343
347
  def __init__(
344
348
  self,
345
349
  uuid: Union[str, UUID],
346
- characteristics: List[Characteristic],
350
+ characteristics: Iterable[Characteristic],
347
351
  primary=True,
348
352
  included_services: Iterable[Service] = (),
349
353
  ) -> None:
@@ -362,7 +366,7 @@ class Service(Attribute):
362
366
  )
363
367
  self.uuid = uuid
364
368
  self.included_services = list(included_services)
365
- self.characteristics = characteristics[:]
369
+ self.characteristics = list(characteristics)
366
370
  self.primary = primary
367
371
 
368
372
  def get_advertising_data(self) -> Optional[bytes]:
@@ -393,7 +397,7 @@ class TemplateService(Service):
393
397
 
394
398
  def __init__(
395
399
  self,
396
- characteristics: List[Characteristic],
400
+ characteristics: Iterable[Characteristic],
397
401
  primary: bool = True,
398
402
  included_services: Iterable[Service] = (),
399
403
  ) -> None:
@@ -490,7 +494,7 @@ class Characteristic(Attribute):
490
494
  uuid: Union[str, bytes, UUID],
491
495
  properties: Characteristic.Properties,
492
496
  permissions: Union[str, Attribute.Permissions],
493
- value: Union[str, bytes, CharacteristicValue] = b'',
497
+ value: Any = b'',
494
498
  descriptors: Sequence[Descriptor] = (),
495
499
  ):
496
500
  super().__init__(uuid, permissions, value)
@@ -525,7 +529,11 @@ class CharacteristicDeclaration(Attribute):
525
529
 
526
530
  characteristic: Characteristic
527
531
 
528
- def __init__(self, characteristic: Characteristic, value_handle: int) -> None:
532
+ def __init__(
533
+ self,
534
+ characteristic: Characteristic,
535
+ value_handle: int,
536
+ ) -> None:
529
537
  declaration_bytes = (
530
538
  struct.pack('<BH', characteristic.properties, value_handle)
531
539
  + characteristic.uuid.to_pdu_bytes()
@@ -705,7 +713,7 @@ class MappedCharacteristicAdapter(PackedCharacteristicAdapter):
705
713
  '''
706
714
  Adapter that packs/unpacks characteristic values according to a standard
707
715
  Python `struct` format.
708
- The adapted `read_value` and `write_value` methods return/accept aa dictionary which
716
+ The adapted `read_value` and `write_value` methods return/accept a dictionary which
709
717
  is packed/unpacked according to format, with the arguments extracted from the
710
718
  dictionary by key, in the same order as they occur in the `keys` parameter.
711
719
  '''
@@ -735,6 +743,24 @@ class UTF8CharacteristicAdapter(CharacteristicAdapter):
735
743
  return value.decode('utf-8')
736
744
 
737
745
 
746
+ # -----------------------------------------------------------------------------
747
+ class SerializableCharacteristicAdapter(CharacteristicAdapter):
748
+ '''
749
+ Adapter that converts any class to/from bytes using the class'
750
+ `to_bytes` and `__bytes__` methods, respectively.
751
+ '''
752
+
753
+ def __init__(self, characteristic, cls: Type[ByteSerializable]):
754
+ super().__init__(characteristic)
755
+ self.cls = cls
756
+
757
+ def encode_value(self, value: SupportsBytes) -> bytes:
758
+ return bytes(value)
759
+
760
+ def decode_value(self, value: bytes) -> Any:
761
+ return self.cls.from_bytes(value)
762
+
763
+
738
764
  # -----------------------------------------------------------------------------
739
765
  class Descriptor(Attribute):
740
766
  '''
bumble/gatt_server.py CHANGED
@@ -28,7 +28,17 @@ import asyncio
28
28
  import logging
29
29
  from collections import defaultdict
30
30
  import struct
31
- from typing import List, Tuple, Optional, TypeVar, Type, Dict, Iterable, TYPE_CHECKING
31
+ from typing import (
32
+ Dict,
33
+ Iterable,
34
+ List,
35
+ Optional,
36
+ Tuple,
37
+ TypeVar,
38
+ Type,
39
+ Union,
40
+ TYPE_CHECKING,
41
+ )
32
42
  from pyee import EventEmitter
33
43
 
34
44
  from bumble.colors import color
@@ -68,6 +78,7 @@ from bumble.gatt import (
68
78
  GATT_REQUEST_TIMEOUT,
69
79
  GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE,
70
80
  Characteristic,
81
+ CharacteristicAdapter,
71
82
  CharacteristicDeclaration,
72
83
  CharacteristicValue,
73
84
  IncludedServiceDeclaration,
bumble/hci.py CHANGED
@@ -5068,6 +5068,7 @@ class HCI_Event(HCI_Packet):
5068
5068
  hci_packet_type = HCI_EVENT_PACKET
5069
5069
  event_names: Dict[int, str] = {}
5070
5070
  event_classes: Dict[int, Type[HCI_Event]] = {}
5071
+ vendor_factory: Optional[Callable[[bytes], Optional[HCI_Event]]] = None
5071
5072
 
5072
5073
  @staticmethod
5073
5074
  def event(fields=()):
@@ -5125,37 +5126,41 @@ class HCI_Event(HCI_Packet):
5125
5126
 
5126
5127
  return event_class
5127
5128
 
5128
- @staticmethod
5129
- def from_bytes(packet: bytes) -> HCI_Event:
5129
+ @classmethod
5130
+ def from_bytes(cls, packet: bytes) -> HCI_Event:
5130
5131
  event_code = packet[1]
5131
5132
  length = packet[2]
5132
5133
  parameters = packet[3:]
5133
5134
  if len(parameters) != length:
5134
5135
  raise InvalidPacketError('invalid packet length')
5135
5136
 
5136
- cls: Any
5137
+ subclass: Any
5137
5138
  if event_code == HCI_LE_META_EVENT:
5138
5139
  # We do this dispatch here and not in the subclass in order to avoid call
5139
5140
  # loops
5140
5141
  subevent_code = parameters[0]
5141
- cls = HCI_LE_Meta_Event.subevent_classes.get(subevent_code)
5142
- if cls is None:
5142
+ subclass = HCI_LE_Meta_Event.subevent_classes.get(subevent_code)
5143
+ if subclass is None:
5143
5144
  # No class registered, just use a generic class instance
5144
5145
  return HCI_LE_Meta_Event(subevent_code, parameters)
5145
5146
  elif event_code == HCI_VENDOR_EVENT:
5146
- subevent_code = parameters[0]
5147
- cls = HCI_Vendor_Event.subevent_classes.get(subevent_code)
5148
- if cls is None:
5149
- # No class registered, just use a generic class instance
5150
- return HCI_Vendor_Event(subevent_code, parameters)
5147
+ # Invoke all the registered factories to see if any of them can handle
5148
+ # the event
5149
+ if cls.vendor_factory:
5150
+ if event := cls.vendor_factory(parameters):
5151
+ return event
5152
+
5153
+ # No factory, or the factory could not create an instance,
5154
+ # return a generic vendor event
5155
+ return HCI_Event(event_code, parameters)
5151
5156
  else:
5152
- cls = HCI_Event.event_classes.get(event_code)
5153
- if cls is None:
5157
+ subclass = HCI_Event.event_classes.get(event_code)
5158
+ if subclass is None:
5154
5159
  # No class registered, just use a generic class instance
5155
5160
  return HCI_Event(event_code, parameters)
5156
5161
 
5157
5162
  # Invoke the factory to create a new instance
5158
- return cls.from_parameters(parameters) # type: ignore
5163
+ return subclass.from_parameters(parameters) # type: ignore
5159
5164
 
5160
5165
  @classmethod
5161
5166
  def from_parameters(cls, parameters):
@@ -5198,11 +5203,11 @@ HCI_Event.register_events(globals())
5198
5203
  # -----------------------------------------------------------------------------
5199
5204
  class HCI_Extended_Event(HCI_Event):
5200
5205
  '''
5201
- HCI_Event subclass for events that has a subevent code.
5206
+ HCI_Event subclass for events that have a subevent code.
5202
5207
  '''
5203
5208
 
5204
5209
  subevent_names: Dict[int, str] = {}
5205
- subevent_classes: Dict[int, Type[HCI_Extended_Event]]
5210
+ subevent_classes: Dict[int, Type[HCI_Extended_Event]] = {}
5206
5211
 
5207
5212
  @classmethod
5208
5213
  def event(cls, fields=()):
@@ -5253,7 +5258,22 @@ class HCI_Extended_Event(HCI_Event):
5253
5258
  cls.subevent_names.update(cls.subevent_map(symbols))
5254
5259
 
5255
5260
  @classmethod
5256
- def from_parameters(cls, parameters):
5261
+ def subclass_from_parameters(
5262
+ cls, parameters: bytes
5263
+ ) -> Optional[HCI_Extended_Event]:
5264
+ """
5265
+ Factory method that parses the subevent code, finds a registered subclass,
5266
+ and creates an instance if found.
5267
+ """
5268
+ subevent_code = parameters[0]
5269
+ if subclass := cls.subevent_classes.get(subevent_code):
5270
+ return subclass.from_parameters(parameters)
5271
+
5272
+ return None
5273
+
5274
+ @classmethod
5275
+ def from_parameters(cls, parameters: bytes) -> HCI_Extended_Event:
5276
+ """Factory method for subclasses (the subevent code has already been parsed)"""
5257
5277
  self = cls.__new__(cls)
5258
5278
  HCI_Extended_Event.__init__(self, self.subevent_code, parameters)
5259
5279
  if fields := getattr(self, 'fields', None):
@@ -5294,12 +5314,6 @@ class HCI_LE_Meta_Event(HCI_Extended_Event):
5294
5314
  HCI_LE_Meta_Event.register_subevents(globals())
5295
5315
 
5296
5316
 
5297
- # -----------------------------------------------------------------------------
5298
- class HCI_Vendor_Event(HCI_Extended_Event):
5299
- event_code: int = HCI_VENDOR_EVENT
5300
- subevent_classes = {}
5301
-
5302
-
5303
5317
  # -----------------------------------------------------------------------------
5304
5318
  @HCI_LE_Meta_Event.event(
5305
5319
  [
@@ -6173,8 +6187,9 @@ class HCI_Command_Complete_Event(HCI_Event):
6173
6187
  See Bluetooth spec @ 7.7.14 Command Complete Event
6174
6188
  '''
6175
6189
 
6176
- return_parameters = b''
6190
+ num_hci_command_packets: int
6177
6191
  command_opcode: int
6192
+ return_parameters = b''
6178
6193
 
6179
6194
  def map_return_parameters(self, return_parameters):
6180
6195
  '''Map simple 'status' return parameters to their named constant form'''
@@ -6710,6 +6725,14 @@ class HCI_Remote_Host_Supported_Features_Notification_Event(HCI_Event):
6710
6725
  '''
6711
6726
 
6712
6727
 
6728
+ # -----------------------------------------------------------------------------
6729
+ @HCI_Event.event([('data', "*")])
6730
+ class HCI_Vendor_Event(HCI_Event):
6731
+ '''
6732
+ See Bluetooth spec @ 5.4.4 HCI Event packet
6733
+ '''
6734
+
6735
+
6713
6736
  # -----------------------------------------------------------------------------
6714
6737
  class HCI_AclDataPacket(HCI_Packet):
6715
6738
  '''
bumble/host.py CHANGED
@@ -552,7 +552,7 @@ class Host(AbortableEventEmitter):
552
552
 
553
553
  return response
554
554
  except Exception as error:
555
- logger.warning(
555
+ logger.exception(
556
556
  f'{color("!!! Exception while sending command:", "red")} {error}'
557
557
  )
558
558
  raise error
@@ -1248,3 +1248,6 @@ class Host(AbortableEventEmitter):
1248
1248
  event.connection_handle,
1249
1249
  int.from_bytes(event.le_features, 'little'),
1250
1250
  )
1251
+
1252
+ def on_hci_vendor_event(self, event):
1253
+ self.emit('vendor_event', event)
bumble/pairing.py CHANGED
@@ -139,16 +139,19 @@ class PairingDelegate:
139
139
  io_capability: IoCapability
140
140
  local_initiator_key_distribution: KeyDistribution
141
141
  local_responder_key_distribution: KeyDistribution
142
+ maximum_encryption_key_size: int
142
143
 
143
144
  def __init__(
144
145
  self,
145
146
  io_capability: IoCapability = NO_OUTPUT_NO_INPUT,
146
147
  local_initiator_key_distribution: KeyDistribution = DEFAULT_KEY_DISTRIBUTION,
147
148
  local_responder_key_distribution: KeyDistribution = DEFAULT_KEY_DISTRIBUTION,
149
+ maximum_encryption_key_size: int = 16,
148
150
  ) -> None:
149
151
  self.io_capability = io_capability
150
152
  self.local_initiator_key_distribution = local_initiator_key_distribution
151
153
  self.local_responder_key_distribution = local_responder_key_distribution
154
+ self.maximum_encryption_key_size = maximum_encryption_key_size
152
155
 
153
156
  @property
154
157
  def classic_io_capability(self) -> int:
bumble/profiles/aics.py CHANGED
@@ -17,6 +17,7 @@
17
17
  # -----------------------------------------------------------------------------
18
18
  # Imports
19
19
  # -----------------------------------------------------------------------------
20
+ from __future__ import annotations
20
21
  import logging
21
22
  import struct
22
23
 
@@ -28,10 +29,11 @@ from bumble.device import Connection
28
29
  from bumble.att import ATT_Error
29
30
  from bumble.gatt import (
30
31
  Characteristic,
31
- DelegatedCharacteristicAdapter,
32
+ SerializableCharacteristicAdapter,
33
+ PackedCharacteristicAdapter,
32
34
  TemplateService,
33
35
  CharacteristicValue,
34
- PackedCharacteristicAdapter,
36
+ UTF8CharacteristicAdapter,
35
37
  GATT_AUDIO_INPUT_CONTROL_SERVICE,
36
38
  GATT_AUDIO_INPUT_STATE_CHARACTERISTIC,
37
39
  GATT_GAIN_SETTINGS_ATTRIBUTE_CHARACTERISTIC,
@@ -154,9 +156,6 @@ class AudioInputState:
154
156
  attribute=self.attribute_value, value=bytes(self)
155
157
  )
156
158
 
157
- def on_read(self, _connection: Optional[Connection]) -> bytes:
158
- return bytes(self)
159
-
160
159
 
161
160
  @dataclass
162
161
  class GainSettingsProperties:
@@ -173,7 +172,7 @@ class GainSettingsProperties:
173
172
  (gain_settings_unit, gain_settings_minimum, gain_settings_maximum) = (
174
173
  struct.unpack('BBB', data)
175
174
  )
176
- GainSettingsProperties(
175
+ return GainSettingsProperties(
177
176
  gain_settings_unit, gain_settings_minimum, gain_settings_maximum
178
177
  )
179
178
 
@@ -186,9 +185,6 @@ class GainSettingsProperties:
186
185
  ]
187
186
  )
188
187
 
189
- def on_read(self, _connection: Optional[Connection]) -> bytes:
190
- return bytes(self)
191
-
192
188
 
193
189
  @dataclass
194
190
  class AudioInputControlPoint:
@@ -321,21 +317,14 @@ class AudioInputDescription:
321
317
  audio_input_description: str = "Bluetooth"
322
318
  attribute_value: Optional[CharacteristicValue] = None
323
319
 
324
- @classmethod
325
- def from_bytes(cls, data: bytes):
326
- return cls(audio_input_description=data.decode('utf-8'))
327
-
328
- def __bytes__(self) -> bytes:
329
- return self.audio_input_description.encode('utf-8')
330
-
331
- def on_read(self, _connection: Optional[Connection]) -> bytes:
332
- return self.audio_input_description.encode('utf-8')
320
+ def on_read(self, _connection: Optional[Connection]) -> str:
321
+ return self.audio_input_description
333
322
 
334
- async def on_write(self, connection: Optional[Connection], value: bytes) -> None:
323
+ async def on_write(self, connection: Optional[Connection], value: str) -> None:
335
324
  assert connection
336
325
  assert self.attribute_value
337
326
 
338
- self.audio_input_description = value.decode('utf-8')
327
+ self.audio_input_description = value
339
328
  await connection.device.notify_subscribers(
340
329
  attribute=self.attribute_value, value=value
341
330
  )
@@ -375,26 +364,29 @@ class AICSService(TemplateService):
375
364
  self.audio_input_state, self.gain_settings_properties
376
365
  )
377
366
 
378
- self.audio_input_state_characteristic = DelegatedCharacteristicAdapter(
367
+ self.audio_input_state_characteristic = SerializableCharacteristicAdapter(
379
368
  Characteristic(
380
369
  uuid=GATT_AUDIO_INPUT_STATE_CHARACTERISTIC,
381
370
  properties=Characteristic.Properties.READ
382
371
  | Characteristic.Properties.NOTIFY,
383
372
  permissions=Characteristic.Permissions.READ_REQUIRES_ENCRYPTION,
384
- value=CharacteristicValue(read=self.audio_input_state.on_read),
373
+ value=self.audio_input_state,
385
374
  ),
386
- encode=lambda value: bytes(value),
375
+ AudioInputState,
387
376
  )
388
377
  self.audio_input_state.attribute_value = (
389
378
  self.audio_input_state_characteristic.value
390
379
  )
391
380
 
392
- self.gain_settings_properties_characteristic = DelegatedCharacteristicAdapter(
393
- Characteristic(
394
- uuid=GATT_GAIN_SETTINGS_ATTRIBUTE_CHARACTERISTIC,
395
- properties=Characteristic.Properties.READ,
396
- permissions=Characteristic.Permissions.READ_REQUIRES_ENCRYPTION,
397
- value=CharacteristicValue(read=self.gain_settings_properties.on_read),
381
+ self.gain_settings_properties_characteristic = (
382
+ SerializableCharacteristicAdapter(
383
+ Characteristic(
384
+ uuid=GATT_GAIN_SETTINGS_ATTRIBUTE_CHARACTERISTIC,
385
+ properties=Characteristic.Properties.READ,
386
+ permissions=Characteristic.Permissions.READ_REQUIRES_ENCRYPTION,
387
+ value=self.gain_settings_properties,
388
+ ),
389
+ GainSettingsProperties,
398
390
  )
399
391
  )
400
392
 
@@ -402,7 +394,7 @@ class AICSService(TemplateService):
402
394
  uuid=GATT_AUDIO_INPUT_TYPE_CHARACTERISTIC,
403
395
  properties=Characteristic.Properties.READ,
404
396
  permissions=Characteristic.Permissions.READ_REQUIRES_ENCRYPTION,
405
- value=audio_input_type,
397
+ value=bytes(audio_input_type, 'utf-8'),
406
398
  )
407
399
 
408
400
  self.audio_input_status_characteristic = Characteristic(
@@ -412,18 +404,14 @@ class AICSService(TemplateService):
412
404
  value=bytes([self.audio_input_status]),
413
405
  )
414
406
 
415
- self.audio_input_control_point_characteristic = DelegatedCharacteristicAdapter(
416
- Characteristic(
417
- uuid=GATT_AUDIO_INPUT_CONTROL_POINT_CHARACTERISTIC,
418
- properties=Characteristic.Properties.WRITE,
419
- permissions=Characteristic.Permissions.WRITE_REQUIRES_ENCRYPTION,
420
- value=CharacteristicValue(
421
- write=self.audio_input_control_point.on_write
422
- ),
423
- )
407
+ self.audio_input_control_point_characteristic = Characteristic(
408
+ uuid=GATT_AUDIO_INPUT_CONTROL_POINT_CHARACTERISTIC,
409
+ properties=Characteristic.Properties.WRITE,
410
+ permissions=Characteristic.Permissions.WRITE_REQUIRES_ENCRYPTION,
411
+ value=CharacteristicValue(write=self.audio_input_control_point.on_write),
424
412
  )
425
413
 
426
- self.audio_input_description_characteristic = DelegatedCharacteristicAdapter(
414
+ self.audio_input_description_characteristic = UTF8CharacteristicAdapter(
427
415
  Characteristic(
428
416
  uuid=GATT_AUDIO_INPUT_DESCRIPTION_CHARACTERISTIC,
429
417
  properties=Characteristic.Properties.READ
@@ -469,8 +457,8 @@ class AICSServiceProxy(ProfileServiceProxy):
469
457
  )
470
458
  ):
471
459
  raise gatt.InvalidServiceError("Audio Input State Characteristic not found")
472
- self.audio_input_state = DelegatedCharacteristicAdapter(
473
- characteristic=characteristics[0], decode=AudioInputState.from_bytes
460
+ self.audio_input_state = SerializableCharacteristicAdapter(
461
+ characteristics[0], AudioInputState
474
462
  )
475
463
 
476
464
  if not (
@@ -481,9 +469,8 @@ class AICSServiceProxy(ProfileServiceProxy):
481
469
  raise gatt.InvalidServiceError(
482
470
  "Gain Settings Attribute Characteristic not found"
483
471
  )
484
- self.gain_settings_properties = PackedCharacteristicAdapter(
485
- characteristics[0],
486
- 'BBB',
472
+ self.gain_settings_properties = SerializableCharacteristicAdapter(
473
+ characteristics[0], GainSettingsProperties
487
474
  )
488
475
 
489
476
  if not (
@@ -494,10 +481,7 @@ class AICSServiceProxy(ProfileServiceProxy):
494
481
  raise gatt.InvalidServiceError(
495
482
  "Audio Input Status Characteristic not found"
496
483
  )
497
- self.audio_input_status = PackedCharacteristicAdapter(
498
- characteristics[0],
499
- 'B',
500
- )
484
+ self.audio_input_status = PackedCharacteristicAdapter(characteristics[0], 'B')
501
485
 
502
486
  if not (
503
487
  characteristics := service_proxy.get_characteristics_by_uuid(
@@ -517,4 +501,4 @@ class AICSServiceProxy(ProfileServiceProxy):
517
501
  raise gatt.InvalidServiceError(
518
502
  "Audio Input Description Characteristic not found"
519
503
  )
520
- self.audio_input_description = characteristics[0]
504
+ self.audio_input_description = UTF8CharacteristicAdapter(characteristics[0])
bumble/profiles/bass.py CHANGED
@@ -276,10 +276,7 @@ class BroadcastReceiveState:
276
276
  subgroups: List[SubgroupInfo]
277
277
 
278
278
  @classmethod
279
- def from_bytes(cls, data: bytes) -> Optional[BroadcastReceiveState]:
280
- if not data:
281
- return None
282
-
279
+ def from_bytes(cls, data: bytes) -> BroadcastReceiveState:
283
280
  source_id = data[0]
284
281
  _, source_address = hci.Address.parse_address_preceded_by_type(data, 2)
285
282
  source_adv_sid = data[8]
@@ -357,7 +354,7 @@ class BroadcastAudioScanServiceProxy(gatt_client.ProfileServiceProxy):
357
354
  SERVICE_CLASS = BroadcastAudioScanService
358
355
 
359
356
  broadcast_audio_scan_control_point: gatt_client.CharacteristicProxy
360
- broadcast_receive_states: List[gatt.DelegatedCharacteristicAdapter]
357
+ broadcast_receive_states: List[gatt.SerializableCharacteristicAdapter]
361
358
 
362
359
  def __init__(self, service_proxy: gatt_client.ServiceProxy):
363
360
  self.service_proxy = service_proxy
@@ -381,8 +378,8 @@ class BroadcastAudioScanServiceProxy(gatt_client.ProfileServiceProxy):
381
378
  "Broadcast Receive State characteristic not found"
382
379
  )
383
380
  self.broadcast_receive_states = [
384
- gatt.DelegatedCharacteristicAdapter(
385
- characteristic, decode=BroadcastReceiveState.from_bytes
381
+ gatt.SerializableCharacteristicAdapter(
382
+ characteristic, BroadcastReceiveState
386
383
  )
387
384
  for characteristic in characteristics
388
385
  ]
@@ -64,7 +64,10 @@ class DeviceInformationService(TemplateService):
64
64
  ):
65
65
  characteristics = [
66
66
  Characteristic(
67
- uuid, Characteristic.Properties.READ, Characteristic.READABLE, field
67
+ uuid,
68
+ Characteristic.Properties.READ,
69
+ Characteristic.READABLE,
70
+ bytes(field, 'utf-8'),
68
71
  )
69
72
  for (field, uuid) in (
70
73
  (manufacturer_name, GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC),
@@ -30,6 +30,7 @@ from ..gatt import (
30
30
  TemplateService,
31
31
  Characteristic,
32
32
  CharacteristicValue,
33
+ SerializableCharacteristicAdapter,
33
34
  DelegatedCharacteristicAdapter,
34
35
  PackedCharacteristicAdapter,
35
36
  )
@@ -150,15 +151,14 @@ class HeartRateService(TemplateService):
150
151
  body_sensor_location=None,
151
152
  reset_energy_expended=None,
152
153
  ):
153
- self.heart_rate_measurement_characteristic = DelegatedCharacteristicAdapter(
154
+ self.heart_rate_measurement_characteristic = SerializableCharacteristicAdapter(
154
155
  Characteristic(
155
156
  GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC,
156
157
  Characteristic.Properties.NOTIFY,
157
158
  0,
158
159
  CharacteristicValue(read=read_heart_rate_measurement),
159
160
  ),
160
- # pylint: disable=unnecessary-lambda
161
- encode=lambda value: bytes(value),
161
+ HeartRateService.HeartRateMeasurement,
162
162
  )
163
163
  characteristics = [self.heart_rate_measurement_characteristic]
164
164
 
@@ -204,9 +204,8 @@ class HeartRateServiceProxy(ProfileServiceProxy):
204
204
  if characteristics := service_proxy.get_characteristics_by_uuid(
205
205
  GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC
206
206
  ):
207
- self.heart_rate_measurement = DelegatedCharacteristicAdapter(
208
- characteristics[0],
209
- decode=HeartRateService.HeartRateMeasurement.from_bytes,
207
+ self.heart_rate_measurement = SerializableCharacteristicAdapter(
208
+ characteristics[0], HeartRateService.HeartRateMeasurement
210
209
  )
211
210
  else:
212
211
  self.heart_rate_measurement = None