bumble 0.0.202__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/hfp.py CHANGED
@@ -141,7 +141,7 @@ class HfFeature(enum.IntFlag):
141
141
  """
142
142
  HF supported features (AT+BRSF=) (normative).
143
143
 
144
- Hands-Free Profile v1.8, 4.34.2, AT Capabilities Re-Used from GSM 07.07 and 3GPP 27.007.
144
+ Hands-Free Profile v1.9, 4.34.2, AT Capabilities Re-Used from GSM 07.07 and 3GPP 27.007.
145
145
  """
146
146
 
147
147
  EC_NR = 0x001 # Echo Cancel & Noise reduction
@@ -155,14 +155,14 @@ class HfFeature(enum.IntFlag):
155
155
  HF_INDICATORS = 0x100
156
156
  ESCO_S4_SETTINGS_SUPPORTED = 0x200
157
157
  ENHANCED_VOICE_RECOGNITION_STATUS = 0x400
158
- VOICE_RECOGNITION_TEST = 0x800
158
+ VOICE_RECOGNITION_TEXT = 0x800
159
159
 
160
160
 
161
161
  class AgFeature(enum.IntFlag):
162
162
  """
163
163
  AG supported features (+BRSF:) (normative).
164
164
 
165
- Hands-Free Profile v1.8, 4.34.2, AT Capabilities Re-Used from GSM 07.07 and 3GPP 27.007.
165
+ Hands-Free Profile v1.9, 4.34.2, AT Capabilities Re-Used from GSM 07.07 and 3GPP 27.007.
166
166
  """
167
167
 
168
168
  THREE_WAY_CALLING = 0x001
@@ -178,7 +178,7 @@ class AgFeature(enum.IntFlag):
178
178
  HF_INDICATORS = 0x400
179
179
  ESCO_S4_SETTINGS_SUPPORTED = 0x800
180
180
  ENHANCED_VOICE_RECOGNITION_STATUS = 0x1000
181
- VOICE_RECOGNITION_TEST = 0x2000
181
+ VOICE_RECOGNITION_TEXT = 0x2000
182
182
 
183
183
 
184
184
  class AudioCodec(enum.IntEnum):
@@ -1390,6 +1390,7 @@ class AgProtocol(pyee.EventEmitter):
1390
1390
 
1391
1391
  def _on_bac(self, *args) -> None:
1392
1392
  self.supported_audio_codecs = [AudioCodec(int(value)) for value in args]
1393
+ self.emit('supported_audio_codecs', self.supported_audio_codecs)
1393
1394
  self.send_ok()
1394
1395
 
1395
1396
  def _on_bcs(self, codec: bytes) -> None:
@@ -1618,7 +1619,7 @@ class ProfileVersion(enum.IntEnum):
1618
1619
  """
1619
1620
  Profile version (normative).
1620
1621
 
1621
- Hands-Free Profile v1.8, 5.3 SDP Interoperability Requirements.
1622
+ Hands-Free Profile v1.8, 6.3 SDP Interoperability Requirements.
1622
1623
  """
1623
1624
 
1624
1625
  V1_5 = 0x0105
@@ -1632,7 +1633,7 @@ class HfSdpFeature(enum.IntFlag):
1632
1633
  """
1633
1634
  HF supported features (normative).
1634
1635
 
1635
- Hands-Free Profile v1.8, 5.3 SDP Interoperability Requirements.
1636
+ Hands-Free Profile v1.9, 6.3 SDP Interoperability Requirements.
1636
1637
  """
1637
1638
 
1638
1639
  EC_NR = 0x01 # Echo Cancel & Noise reduction
@@ -1640,16 +1641,17 @@ class HfSdpFeature(enum.IntFlag):
1640
1641
  CLI_PRESENTATION_CAPABILITY = 0x04
1641
1642
  VOICE_RECOGNITION_ACTIVATION = 0x08
1642
1643
  REMOTE_VOLUME_CONTROL = 0x10
1643
- WIDE_BAND = 0x20 # Wide band speech
1644
+ WIDE_BAND_SPEECH = 0x20
1644
1645
  ENHANCED_VOICE_RECOGNITION_STATUS = 0x40
1645
- VOICE_RECOGNITION_TEST = 0x80
1646
+ VOICE_RECOGNITION_TEXT = 0x80
1647
+ SUPER_WIDE_BAND = 0x100
1646
1648
 
1647
1649
 
1648
1650
  class AgSdpFeature(enum.IntFlag):
1649
1651
  """
1650
1652
  AG supported features (normative).
1651
1653
 
1652
- Hands-Free Profile v1.8, 5.3 SDP Interoperability Requirements.
1654
+ Hands-Free Profile v1.9, 6.3 SDP Interoperability Requirements.
1653
1655
  """
1654
1656
 
1655
1657
  THREE_WAY_CALLING = 0x01
@@ -1657,9 +1659,10 @@ class AgSdpFeature(enum.IntFlag):
1657
1659
  VOICE_RECOGNITION_FUNCTION = 0x04
1658
1660
  IN_BAND_RING_TONE_CAPABILITY = 0x08
1659
1661
  VOICE_TAG = 0x10 # Attach a number to voice tag
1660
- WIDE_BAND = 0x20 # Wide band speech
1662
+ WIDE_BAND_SPEECH = 0x20
1661
1663
  ENHANCED_VOICE_RECOGNITION_STATUS = 0x40
1662
- VOICE_RECOGNITION_TEST = 0x80
1664
+ VOICE_RECOGNITION_TEXT = 0x80
1665
+ SUPER_WIDE_BAND_SPEED_SPEECH = 0x100
1663
1666
 
1664
1667
 
1665
1668
  def make_hf_sdp_records(
@@ -1692,11 +1695,11 @@ def make_hf_sdp_records(
1692
1695
  in configuration.supported_hf_features
1693
1696
  ):
1694
1697
  hf_supported_features |= HfSdpFeature.ENHANCED_VOICE_RECOGNITION_STATUS
1695
- if HfFeature.VOICE_RECOGNITION_TEST in configuration.supported_hf_features:
1696
- hf_supported_features |= HfSdpFeature.VOICE_RECOGNITION_TEST
1698
+ if HfFeature.VOICE_RECOGNITION_TEXT in configuration.supported_hf_features:
1699
+ hf_supported_features |= HfSdpFeature.VOICE_RECOGNITION_TEXT
1697
1700
 
1698
1701
  if AudioCodec.MSBC in configuration.supported_audio_codecs:
1699
- hf_supported_features |= HfSdpFeature.WIDE_BAND
1702
+ hf_supported_features |= HfSdpFeature.WIDE_BAND_SPEECH
1700
1703
 
1701
1704
  return [
1702
1705
  sdp.ServiceAttribute(
@@ -1772,14 +1775,14 @@ def make_ag_sdp_records(
1772
1775
  in configuration.supported_ag_features
1773
1776
  ):
1774
1777
  ag_supported_features |= AgSdpFeature.ENHANCED_VOICE_RECOGNITION_STATUS
1775
- if AgFeature.VOICE_RECOGNITION_TEST in configuration.supported_ag_features:
1776
- ag_supported_features |= AgSdpFeature.VOICE_RECOGNITION_TEST
1778
+ if AgFeature.VOICE_RECOGNITION_TEXT in configuration.supported_ag_features:
1779
+ ag_supported_features |= AgSdpFeature.VOICE_RECOGNITION_TEXT
1777
1780
  if AgFeature.IN_BAND_RING_TONE_CAPABILITY in configuration.supported_ag_features:
1778
1781
  ag_supported_features |= AgSdpFeature.IN_BAND_RING_TONE_CAPABILITY
1779
1782
  if AgFeature.VOICE_RECOGNITION_FUNCTION in configuration.supported_ag_features:
1780
1783
  ag_supported_features |= AgSdpFeature.VOICE_RECOGNITION_FUNCTION
1781
1784
  if AudioCodec.MSBC in configuration.supported_audio_codecs:
1782
- ag_supported_features |= AgSdpFeature.WIDE_BAND
1785
+ ag_supported_features |= AgSdpFeature.WIDE_BAND_SPEECH
1783
1786
 
1784
1787
  return [
1785
1788
  sdp.ServiceAttribute(
bumble/host.py CHANGED
@@ -199,7 +199,7 @@ class Host(AbortableEventEmitter):
199
199
  check_address_type: bool = False,
200
200
  ) -> Optional[Connection]:
201
201
  for connection in self.connections.values():
202
- if connection.peer_address.to_bytes() == bd_addr.to_bytes():
202
+ if bytes(connection.peer_address) == bytes(bd_addr):
203
203
  if (
204
204
  check_address_type
205
205
  and connection.peer_address.address_type != bd_addr.address_type
@@ -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/l2cap.py CHANGED
@@ -225,7 +225,7 @@ class L2CAP_PDU:
225
225
 
226
226
  return L2CAP_PDU(l2cap_pdu_cid, l2cap_pdu_payload)
227
227
 
228
- def to_bytes(self) -> bytes:
228
+ def __bytes__(self) -> bytes:
229
229
  header = struct.pack('<HH', len(self.payload), self.cid)
230
230
  return header + self.payload
231
231
 
@@ -233,9 +233,6 @@ class L2CAP_PDU:
233
233
  self.cid = cid
234
234
  self.payload = payload
235
235
 
236
- def __bytes__(self) -> bytes:
237
- return self.to_bytes()
238
-
239
236
  def __str__(self) -> str:
240
237
  return f'{color("L2CAP", "green")} [CID={self.cid}]: {self.payload.hex()}'
241
238
 
@@ -333,11 +330,8 @@ class L2CAP_Control_Frame:
333
330
  def init_from_bytes(self, pdu, offset):
334
331
  return HCI_Object.init_from_bytes(self, pdu, offset, self.fields)
335
332
 
336
- def to_bytes(self) -> bytes:
337
- return self.pdu
338
-
339
333
  def __bytes__(self) -> bytes:
340
- return self.to_bytes()
334
+ return self.pdu
341
335
 
342
336
  def __str__(self) -> str:
343
337
  result = f'{color(self.name, "yellow")} [ID={self.identifier}]'
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/pandora/host.py CHANGED
@@ -39,7 +39,6 @@ from bumble.device import (
39
39
  AdvertisingEventProperties,
40
40
  AdvertisingType,
41
41
  Device,
42
- Phy,
43
42
  )
44
43
  from bumble.gatt import Service
45
44
  from bumble.hci import (
@@ -47,6 +46,7 @@ from bumble.hci import (
47
46
  HCI_PAGE_TIMEOUT_ERROR,
48
47
  HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR,
49
48
  Address,
49
+ Phy,
50
50
  )
51
51
  from google.protobuf import any_pb2 # pytype: disable=pyi-error
52
52
  from google.protobuf import empty_pb2 # pytype: disable=pyi-error
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,
@@ -95,7 +97,7 @@ class AudioInputStatus(OpenIntEnum):
95
97
  Cf. 3.4 Audio Input Status
96
98
  '''
97
99
 
98
- INATIVE = 0x00
100
+ INACTIVE = 0x00
99
101
  ACTIVE = 0x01
100
102
 
101
103
 
@@ -104,7 +106,7 @@ class AudioInputControlPointOpCode(OpenIntEnum):
104
106
  Cf. 3.5.1 Audio Input Control Point procedure requirements
105
107
  '''
106
108
 
107
- SET_GAIN_SETTING = 0x00
109
+ SET_GAIN_SETTING = 0x01
108
110
  UNMUTE = 0x02
109
111
  MUTE = 0x03
110
112
  SET_MANUAL_GAIN_MODE = 0x04
@@ -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:
@@ -239,7 +235,7 @@ class AudioInputControlPoint:
239
235
  or gain_settings_operand
240
236
  > self.gain_settings_properties.gain_settings_maximum
241
237
  ):
242
- logger.error("gain_seetings value out of range")
238
+ logger.error("gain_settings value out of range")
243
239
  raise ATT_Error(ErrorCode.VALUE_OUT_OF_RANGE)
244
240
 
245
241
  if self.audio_input_state.gain_settings != gain_settings_operand:
@@ -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/bap.py CHANGED
@@ -265,7 +265,7 @@ class UnicastServerAdvertisingData:
265
265
  core.AdvertisingData.SERVICE_DATA_16_BIT_UUID,
266
266
  struct.pack(
267
267
  '<2sBIB',
268
- gatt.GATT_AUDIO_STREAM_CONTROL_SERVICE.to_bytes(),
268
+ bytes(gatt.GATT_AUDIO_STREAM_CONTROL_SERVICE),
269
269
  self.announcement_type,
270
270
  self.available_audio_contexts,
271
271
  len(self.metadata),
@@ -398,18 +398,21 @@ class CodecSpecificConfiguration:
398
398
  OCTETS_PER_FRAME = 0x04
399
399
  CODEC_FRAMES_PER_SDU = 0x05
400
400
 
401
- sampling_frequency: SamplingFrequency
402
- frame_duration: FrameDuration
403
- audio_channel_allocation: AudioLocation
404
- octets_per_codec_frame: int
405
- codec_frames_per_sdu: int
401
+ sampling_frequency: SamplingFrequency | None = None
402
+ frame_duration: FrameDuration | None = None
403
+ audio_channel_allocation: AudioLocation | None = None
404
+ octets_per_codec_frame: int | None = None
405
+ codec_frames_per_sdu: int | None = None
406
406
 
407
407
  @classmethod
408
408
  def from_bytes(cls, data: bytes) -> CodecSpecificConfiguration:
409
409
  offset = 0
410
- # Allowed default values.
411
- audio_channel_allocation = AudioLocation.NOT_ALLOWED
412
- codec_frames_per_sdu = 1
410
+ sampling_frequency: SamplingFrequency | None = None
411
+ frame_duration: FrameDuration | None = None
412
+ audio_channel_allocation: AudioLocation | None = None
413
+ octets_per_codec_frame: int | None = None
414
+ codec_frames_per_sdu: int | None = None
415
+
413
416
  while offset < len(data):
414
417
  length, type = struct.unpack_from('BB', data, offset)
415
418
  offset += 2
@@ -427,8 +430,6 @@ class CodecSpecificConfiguration:
427
430
  elif type == CodecSpecificConfiguration.Type.CODEC_FRAMES_PER_SDU:
428
431
  codec_frames_per_sdu = value
429
432
 
430
- # It is expected here that if some fields are missing, an error should be raised.
431
- # pylint: disable=possibly-used-before-assignment,used-before-assignment
432
433
  return CodecSpecificConfiguration(
433
434
  sampling_frequency=sampling_frequency,
434
435
  frame_duration=frame_duration,
@@ -438,23 +439,43 @@ class CodecSpecificConfiguration:
438
439
  )
439
440
 
440
441
  def __bytes__(self) -> bytes:
441
- return struct.pack(
442
- '<BBBBBBBBIBBHBBB',
443
- 2,
444
- CodecSpecificConfiguration.Type.SAMPLING_FREQUENCY,
445
- self.sampling_frequency,
446
- 2,
447
- CodecSpecificConfiguration.Type.FRAME_DURATION,
448
- self.frame_duration,
449
- 5,
450
- CodecSpecificConfiguration.Type.AUDIO_CHANNEL_ALLOCATION,
451
- self.audio_channel_allocation,
452
- 3,
453
- CodecSpecificConfiguration.Type.OCTETS_PER_FRAME,
454
- self.octets_per_codec_frame,
455
- 2,
456
- CodecSpecificConfiguration.Type.CODEC_FRAMES_PER_SDU,
457
- self.codec_frames_per_sdu,
442
+ return b''.join(
443
+ [
444
+ struct.pack(fmt, length, tag, value)
445
+ for fmt, length, tag, value in [
446
+ (
447
+ '<BBB',
448
+ 2,
449
+ CodecSpecificConfiguration.Type.SAMPLING_FREQUENCY,
450
+ self.sampling_frequency,
451
+ ),
452
+ (
453
+ '<BBB',
454
+ 2,
455
+ CodecSpecificConfiguration.Type.FRAME_DURATION,
456
+ self.frame_duration,
457
+ ),
458
+ (
459
+ '<BBI',
460
+ 5,
461
+ CodecSpecificConfiguration.Type.AUDIO_CHANNEL_ALLOCATION,
462
+ self.audio_channel_allocation,
463
+ ),
464
+ (
465
+ '<BBH',
466
+ 3,
467
+ CodecSpecificConfiguration.Type.OCTETS_PER_FRAME,
468
+ self.octets_per_codec_frame,
469
+ ),
470
+ (
471
+ '<BBB',
472
+ 2,
473
+ CodecSpecificConfiguration.Type.CODEC_FRAMES_PER_SDU,
474
+ self.codec_frames_per_sdu,
475
+ ),
476
+ ]
477
+ if value is not None
478
+ ]
458
479
  )
459
480
 
460
481
 
@@ -466,6 +487,24 @@ class BroadcastAudioAnnouncement:
466
487
  def from_bytes(cls, data: bytes) -> Self:
467
488
  return cls(int.from_bytes(data[:3], 'little'))
468
489
 
490
+ def __bytes__(self) -> bytes:
491
+ return self.broadcast_id.to_bytes(3, 'little')
492
+
493
+ def get_advertising_data(self) -> bytes:
494
+ return bytes(
495
+ core.AdvertisingData(
496
+ [
497
+ (
498
+ core.AdvertisingData.SERVICE_DATA_16_BIT_UUID,
499
+ (
500
+ bytes(gatt.GATT_BROADCAST_AUDIO_ANNOUNCEMENT_SERVICE)
501
+ + bytes(self)
502
+ ),
503
+ )
504
+ ]
505
+ )
506
+ )
507
+
469
508
 
470
509
  @dataclasses.dataclass
471
510
  class BasicAudioAnnouncement:
@@ -474,26 +513,37 @@ class BasicAudioAnnouncement:
474
513
  index: int
475
514
  codec_specific_configuration: CodecSpecificConfiguration
476
515
 
477
- @dataclasses.dataclass
478
- class CodecInfo:
479
- coding_format: hci.CodecID
480
- company_id: int
481
- vendor_specific_codec_id: int
482
-
483
- @classmethod
484
- def from_bytes(cls, data: bytes) -> Self:
485
- coding_format = hci.CodecID(data[0])
486
- company_id = int.from_bytes(data[1:3], 'little')
487
- vendor_specific_codec_id = int.from_bytes(data[3:5], 'little')
488
- return cls(coding_format, company_id, vendor_specific_codec_id)
516
+ def __bytes__(self) -> bytes:
517
+ codec_specific_configuration_bytes = bytes(
518
+ self.codec_specific_configuration
519
+ )
520
+ return (
521
+ bytes([self.index, len(codec_specific_configuration_bytes)])
522
+ + codec_specific_configuration_bytes
523
+ )
489
524
 
490
525
  @dataclasses.dataclass
491
526
  class Subgroup:
492
- codec_id: BasicAudioAnnouncement.CodecInfo
527
+ codec_id: hci.CodingFormat
493
528
  codec_specific_configuration: CodecSpecificConfiguration
494
529
  metadata: le_audio.Metadata
495
530
  bis: List[BasicAudioAnnouncement.BIS]
496
531
 
532
+ def __bytes__(self) -> bytes:
533
+ metadata_bytes = bytes(self.metadata)
534
+ codec_specific_configuration_bytes = bytes(
535
+ self.codec_specific_configuration
536
+ )
537
+ return (
538
+ bytes([len(self.bis)])
539
+ + bytes(self.codec_id)
540
+ + bytes([len(codec_specific_configuration_bytes)])
541
+ + codec_specific_configuration_bytes
542
+ + bytes([len(metadata_bytes)])
543
+ + metadata_bytes
544
+ + b''.join(map(bytes, self.bis))
545
+ )
546
+
497
547
  presentation_delay: int
498
548
  subgroups: List[BasicAudioAnnouncement.Subgroup]
499
549
 
@@ -505,7 +555,7 @@ class BasicAudioAnnouncement:
505
555
  for _ in range(data[3]):
506
556
  num_bis = data[offset]
507
557
  offset += 1
508
- codec_id = cls.CodecInfo.from_bytes(data[offset : offset + 5])
558
+ codec_id = hci.CodingFormat.from_bytes(data[offset : offset + 5])
509
559
  offset += 5
510
560
  codec_specific_configuration_length = data[offset]
511
561
  offset += 1
@@ -549,3 +599,25 @@ class BasicAudioAnnouncement:
549
599
  )
550
600
 
551
601
  return cls(presentation_delay, subgroups)
602
+
603
+ def __bytes__(self) -> bytes:
604
+ return (
605
+ self.presentation_delay.to_bytes(3, 'little')
606
+ + bytes([len(self.subgroups)])
607
+ + b''.join(map(bytes, self.subgroups))
608
+ )
609
+
610
+ def get_advertising_data(self) -> bytes:
611
+ return bytes(
612
+ core.AdvertisingData(
613
+ [
614
+ (
615
+ core.AdvertisingData.SERVICE_DATA_16_BIT_UUID,
616
+ (
617
+ bytes(gatt.GATT_BASIC_AUDIO_ANNOUNCEMENT_SERVICE)
618
+ + bytes(self)
619
+ ),
620
+ )
621
+ ]
622
+ )
623
+ )