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/_version.py +2 -2
- bumble/apps/auracast.py +22 -13
- bumble/apps/bench.py +2 -1
- bumble/apps/hci_bridge.py +1 -1
- bumble/apps/lea_unicast/app.py +24 -6
- bumble/apps/pair.py +13 -8
- bumble/apps/show.py +6 -6
- bumble/att.py +11 -15
- bumble/controller.py +58 -2
- bumble/device.py +454 -494
- bumble/drivers/common.py +2 -0
- bumble/drivers/intel.py +593 -24
- bumble/gatt.py +33 -7
- bumble/gatt_client.py +3 -3
- bumble/gatt_server.py +14 -3
- bumble/hci.py +139 -56
- bumble/hfp.py +20 -17
- bumble/host.py +5 -2
- bumble/l2cap.py +2 -8
- bumble/pairing.py +3 -0
- bumble/pandora/host.py +1 -1
- bumble/profiles/aics.py +37 -53
- bumble/profiles/bap.py +114 -42
- bumble/profiles/bass.py +4 -7
- bumble/profiles/device_information_service.py +4 -1
- bumble/profiles/heart_rate_service.py +5 -6
- bumble/profiles/vocs.py +330 -0
- bumble/sdp.py +1 -7
- bumble/smp.py +9 -7
- bumble/tools/intel_fw_download.py +130 -0
- bumble/tools/intel_util.py +154 -0
- bumble/transport/usb.py +8 -2
- bumble/utils.py +20 -5
- bumble/vendor/android/hci.py +29 -4
- {bumble-0.0.202.dist-info → bumble-0.0.204.dist-info}/METADATA +15 -15
- {bumble-0.0.202.dist-info → bumble-0.0.204.dist-info}/RECORD +40 -37
- {bumble-0.0.202.dist-info → bumble-0.0.204.dist-info}/WHEEL +1 -1
- {bumble-0.0.202.dist-info → bumble-0.0.204.dist-info}/entry_points.txt +2 -0
- {bumble-0.0.202.dist-info → bumble-0.0.204.dist-info}/LICENSE +0 -0
- {bumble-0.0.202.dist-info → bumble-0.0.204.dist-info}/top_level.txt +0 -0
bumble/_version.py
CHANGED
bumble/apps/auracast.py
CHANGED
|
@@ -60,7 +60,7 @@ AURACAST_DEFAULT_ATT_MTU = 256
|
|
|
60
60
|
class BroadcastScanner(pyee.EventEmitter):
|
|
61
61
|
@dataclasses.dataclass
|
|
62
62
|
class Broadcast(pyee.EventEmitter):
|
|
63
|
-
name: str
|
|
63
|
+
name: str | None
|
|
64
64
|
sync: bumble.device.PeriodicAdvertisingSync
|
|
65
65
|
rssi: int = 0
|
|
66
66
|
public_broadcast_announcement: Optional[
|
|
@@ -135,7 +135,8 @@ class BroadcastScanner(pyee.EventEmitter):
|
|
|
135
135
|
self.sync.advertiser_address,
|
|
136
136
|
color(self.sync.state.name, 'green'),
|
|
137
137
|
)
|
|
138
|
-
|
|
138
|
+
if self.name is not None:
|
|
139
|
+
print(f' {color("Name", "cyan")}: {self.name}')
|
|
139
140
|
if self.appearance:
|
|
140
141
|
print(f' {color("Appearance", "cyan")}: {str(self.appearance)}')
|
|
141
142
|
print(f' {color("RSSI", "cyan")}: {self.rssi}')
|
|
@@ -174,7 +175,7 @@ class BroadcastScanner(pyee.EventEmitter):
|
|
|
174
175
|
print(color(' Codec ID:', 'yellow'))
|
|
175
176
|
print(
|
|
176
177
|
color(' Coding Format: ', 'green'),
|
|
177
|
-
subgroup.codec_id.
|
|
178
|
+
subgroup.codec_id.codec_id.name,
|
|
178
179
|
)
|
|
179
180
|
print(
|
|
180
181
|
color(' Company ID: ', 'green'),
|
|
@@ -274,13 +275,24 @@ class BroadcastScanner(pyee.EventEmitter):
|
|
|
274
275
|
await self.device.stop_scanning()
|
|
275
276
|
|
|
276
277
|
def on_advertisement(self, advertisement: bumble.device.Advertisement) -> None:
|
|
277
|
-
if (
|
|
278
|
-
|
|
279
|
-
bumble.core.AdvertisingData.
|
|
278
|
+
if not (
|
|
279
|
+
ads := advertisement.data.get_all(
|
|
280
|
+
bumble.core.AdvertisingData.SERVICE_DATA_16_BIT_UUID
|
|
280
281
|
)
|
|
281
|
-
)
|
|
282
|
+
) or not (
|
|
283
|
+
any(
|
|
284
|
+
ad
|
|
285
|
+
for ad in ads
|
|
286
|
+
if isinstance(ad, tuple)
|
|
287
|
+
and ad[0] == bumble.gatt.GATT_BROADCAST_AUDIO_ANNOUNCEMENT_SERVICE
|
|
288
|
+
)
|
|
289
|
+
):
|
|
282
290
|
return
|
|
283
|
-
|
|
291
|
+
|
|
292
|
+
broadcast_name = advertisement.data.get(
|
|
293
|
+
bumble.core.AdvertisingData.BROADCAST_NAME
|
|
294
|
+
)
|
|
295
|
+
assert isinstance(broadcast_name, str) or broadcast_name is None
|
|
284
296
|
|
|
285
297
|
if broadcast := self.broadcasts.get(advertisement.address):
|
|
286
298
|
broadcast.update(advertisement)
|
|
@@ -291,7 +303,7 @@ class BroadcastScanner(pyee.EventEmitter):
|
|
|
291
303
|
)
|
|
292
304
|
|
|
293
305
|
async def on_new_broadcast(
|
|
294
|
-
self, name: str, advertisement: bumble.device.Advertisement
|
|
306
|
+
self, name: str | None, advertisement: bumble.device.Advertisement
|
|
295
307
|
) -> None:
|
|
296
308
|
periodic_advertising_sync = await self.device.create_periodic_advertising_sync(
|
|
297
309
|
advertiser_address=advertisement.address,
|
|
@@ -299,10 +311,7 @@ class BroadcastScanner(pyee.EventEmitter):
|
|
|
299
311
|
sync_timeout=self.sync_timeout,
|
|
300
312
|
filter_duplicates=self.filter_duplicates,
|
|
301
313
|
)
|
|
302
|
-
broadcast = self.Broadcast(
|
|
303
|
-
name,
|
|
304
|
-
periodic_advertising_sync,
|
|
305
|
-
)
|
|
314
|
+
broadcast = self.Broadcast(name, periodic_advertising_sync)
|
|
306
315
|
broadcast.update(advertisement)
|
|
307
316
|
self.broadcasts[advertisement.address] = broadcast
|
|
308
317
|
periodic_advertising_sync.on('loss', lambda: self.on_broadcast_loss(broadcast))
|
bumble/apps/bench.py
CHANGED
|
@@ -199,7 +199,7 @@ def log_stats(title, stats, precision=2):
|
|
|
199
199
|
stats_min = min(stats)
|
|
200
200
|
stats_max = max(stats)
|
|
201
201
|
stats_avg = statistics.mean(stats)
|
|
202
|
-
stats_stdev = statistics.stdev(stats)
|
|
202
|
+
stats_stdev = statistics.stdev(stats) if len(stats) >= 2 else 0
|
|
203
203
|
logging.info(
|
|
204
204
|
color(
|
|
205
205
|
(
|
|
@@ -468,6 +468,7 @@ class Ping:
|
|
|
468
468
|
|
|
469
469
|
for run in range(self.repeat + 1):
|
|
470
470
|
self.done.clear()
|
|
471
|
+
self.ping_times = []
|
|
471
472
|
|
|
472
473
|
if run > 0 and self.repeat and self.repeat_delay:
|
|
473
474
|
logging.info(color(f'*** Repeat delay: {self.repeat_delay}', 'green'))
|
bumble/apps/hci_bridge.py
CHANGED
bumble/apps/lea_unicast/app.py
CHANGED
|
@@ -486,7 +486,12 @@ class Speaker:
|
|
|
486
486
|
|
|
487
487
|
def on_pdu(pdu: HCI_IsoDataPacket, ase: ascs.AseStateMachine):
|
|
488
488
|
codec_config = ase.codec_specific_configuration
|
|
489
|
-
|
|
489
|
+
if (
|
|
490
|
+
not isinstance(codec_config, bap.CodecSpecificConfiguration)
|
|
491
|
+
or codec_config.frame_duration is None
|
|
492
|
+
or codec_config.audio_channel_allocation is None
|
|
493
|
+
):
|
|
494
|
+
return
|
|
490
495
|
pcm = decode(
|
|
491
496
|
codec_config.frame_duration.us,
|
|
492
497
|
codec_config.audio_channel_allocation.channel_count,
|
|
@@ -495,11 +500,17 @@ class Speaker:
|
|
|
495
500
|
self.device.abort_on('disconnection', self.ui_server.send_audio(pcm))
|
|
496
501
|
|
|
497
502
|
def on_ase_state_change(ase: ascs.AseStateMachine) -> None:
|
|
503
|
+
codec_config = ase.codec_specific_configuration
|
|
498
504
|
if ase.state == ascs.AseStateMachine.State.STREAMING:
|
|
499
|
-
codec_config = ase.codec_specific_configuration
|
|
500
|
-
assert isinstance(codec_config, bap.CodecSpecificConfiguration)
|
|
501
|
-
assert ase.cis_link
|
|
502
505
|
if ase.role == ascs.AudioRole.SOURCE:
|
|
506
|
+
if (
|
|
507
|
+
not isinstance(codec_config, bap.CodecSpecificConfiguration)
|
|
508
|
+
or ase.cis_link is None
|
|
509
|
+
or codec_config.octets_per_codec_frame is None
|
|
510
|
+
or codec_config.frame_duration is None
|
|
511
|
+
or codec_config.codec_frames_per_sdu is None
|
|
512
|
+
):
|
|
513
|
+
return
|
|
503
514
|
ase.cis_link.abort_on(
|
|
504
515
|
'disconnection',
|
|
505
516
|
lc3_source_task(
|
|
@@ -514,10 +525,17 @@ class Speaker:
|
|
|
514
525
|
),
|
|
515
526
|
)
|
|
516
527
|
else:
|
|
528
|
+
if not ase.cis_link:
|
|
529
|
+
return
|
|
517
530
|
ase.cis_link.sink = functools.partial(on_pdu, ase=ase)
|
|
518
531
|
elif ase.state == ascs.AseStateMachine.State.CODEC_CONFIGURED:
|
|
519
|
-
|
|
520
|
-
|
|
532
|
+
if (
|
|
533
|
+
not isinstance(codec_config, bap.CodecSpecificConfiguration)
|
|
534
|
+
or codec_config.sampling_frequency is None
|
|
535
|
+
or codec_config.frame_duration is None
|
|
536
|
+
or codec_config.audio_channel_allocation is None
|
|
537
|
+
):
|
|
538
|
+
return
|
|
521
539
|
if ase.role == ascs.AudioRole.SOURCE:
|
|
522
540
|
setup_encoders(
|
|
523
541
|
codec_config.sampling_frequency.hz,
|
bumble/apps/pair.py
CHANGED
|
@@ -373,7 +373,9 @@ async def pair(
|
|
|
373
373
|
shared_data = (
|
|
374
374
|
None
|
|
375
375
|
if oob == '-'
|
|
376
|
-
else OobData.from_ad(
|
|
376
|
+
else OobData.from_ad(
|
|
377
|
+
AdvertisingData.from_bytes(bytes.fromhex(oob))
|
|
378
|
+
).shared_data
|
|
377
379
|
)
|
|
378
380
|
legacy_context = OobLegacyContext()
|
|
379
381
|
oob_contexts = PairingConfig.OobConfig(
|
|
@@ -381,16 +383,19 @@ async def pair(
|
|
|
381
383
|
peer_data=shared_data,
|
|
382
384
|
legacy_context=legacy_context,
|
|
383
385
|
)
|
|
384
|
-
oob_data = OobData(
|
|
385
|
-
address=device.random_address,
|
|
386
|
-
shared_data=shared_data,
|
|
387
|
-
legacy_context=legacy_context,
|
|
388
|
-
)
|
|
389
386
|
print(color('@@@-----------------------------------', 'yellow'))
|
|
390
387
|
print(color('@@@ OOB Data:', 'yellow'))
|
|
391
|
-
|
|
388
|
+
if shared_data is None:
|
|
389
|
+
oob_data = OobData(
|
|
390
|
+
address=device.random_address, shared_data=our_oob_context.share()
|
|
391
|
+
)
|
|
392
|
+
print(
|
|
393
|
+
color(
|
|
394
|
+
f'@@@ SHARE: {bytes(oob_data.to_ad()).hex()}',
|
|
395
|
+
'yellow',
|
|
396
|
+
)
|
|
397
|
+
)
|
|
392
398
|
print(color(f'@@@ TK={legacy_context.tk.hex()}', 'yellow'))
|
|
393
|
-
print(color(f'@@@ HEX: ({bytes(oob_data.to_ad()).hex()})', 'yellow'))
|
|
394
399
|
print(color('@@@-----------------------------------', 'yellow'))
|
|
395
400
|
else:
|
|
396
401
|
oob_contexts = None
|
bumble/apps/show.py
CHANGED
|
@@ -144,18 +144,18 @@ class Printer:
|
|
|
144
144
|
help='Format of the input file',
|
|
145
145
|
)
|
|
146
146
|
@click.option(
|
|
147
|
-
'--
|
|
147
|
+
'--vendor',
|
|
148
148
|
type=click.Choice(['android', 'zephyr']),
|
|
149
149
|
multiple=True,
|
|
150
150
|
help='Support vendor-specific commands (list one or more)',
|
|
151
151
|
)
|
|
152
152
|
@click.argument('filename')
|
|
153
153
|
# pylint: disable=redefined-builtin
|
|
154
|
-
def main(format,
|
|
155
|
-
for
|
|
156
|
-
if
|
|
154
|
+
def main(format, vendor, filename):
|
|
155
|
+
for vendor_name in vendor:
|
|
156
|
+
if vendor_name == 'android':
|
|
157
157
|
import bumble.vendor.android.hci
|
|
158
|
-
elif
|
|
158
|
+
elif vendor_name == 'zephyr':
|
|
159
159
|
import bumble.vendor.zephyr.hci
|
|
160
160
|
|
|
161
161
|
input = open(filename, 'rb')
|
|
@@ -180,7 +180,7 @@ def main(format, vendors, filename):
|
|
|
180
180
|
else:
|
|
181
181
|
printer.print(color("[TRUNCATED]", "red"))
|
|
182
182
|
except Exception as error:
|
|
183
|
-
logger.exception()
|
|
183
|
+
logger.exception('')
|
|
184
184
|
print(color(f'!!! {error}', 'red'))
|
|
185
185
|
|
|
186
186
|
|
bumble/att.py
CHANGED
|
@@ -57,6 +57,7 @@ if TYPE_CHECKING:
|
|
|
57
57
|
# pylint: disable=line-too-long
|
|
58
58
|
|
|
59
59
|
ATT_CID = 0x04
|
|
60
|
+
ATT_PSM = 0x001F
|
|
60
61
|
|
|
61
62
|
ATT_ERROR_RESPONSE = 0x01
|
|
62
63
|
ATT_EXCHANGE_MTU_REQUEST = 0x02
|
|
@@ -291,9 +292,6 @@ class ATT_PDU:
|
|
|
291
292
|
def init_from_bytes(self, pdu, offset):
|
|
292
293
|
return HCI_Object.init_from_bytes(self, pdu, offset, self.fields)
|
|
293
294
|
|
|
294
|
-
def to_bytes(self):
|
|
295
|
-
return self.pdu
|
|
296
|
-
|
|
297
295
|
@property
|
|
298
296
|
def is_command(self):
|
|
299
297
|
return ((self.op_code >> 6) & 1) == 1
|
|
@@ -303,7 +301,7 @@ class ATT_PDU:
|
|
|
303
301
|
return ((self.op_code >> 7) & 1) == 1
|
|
304
302
|
|
|
305
303
|
def __bytes__(self):
|
|
306
|
-
return self.
|
|
304
|
+
return self.pdu
|
|
307
305
|
|
|
308
306
|
def __str__(self):
|
|
309
307
|
result = color(self.name, 'yellow')
|
|
@@ -759,13 +757,13 @@ class AttributeValue:
|
|
|
759
757
|
def __init__(
|
|
760
758
|
self,
|
|
761
759
|
read: Union[
|
|
762
|
-
Callable[[Optional[Connection]],
|
|
763
|
-
Callable[[Optional[Connection]], Awaitable[
|
|
760
|
+
Callable[[Optional[Connection]], Any],
|
|
761
|
+
Callable[[Optional[Connection]], Awaitable[Any]],
|
|
764
762
|
None,
|
|
765
763
|
] = None,
|
|
766
764
|
write: Union[
|
|
767
|
-
Callable[[Optional[Connection],
|
|
768
|
-
Callable[[Optional[Connection],
|
|
765
|
+
Callable[[Optional[Connection], Any], None],
|
|
766
|
+
Callable[[Optional[Connection], Any], Awaitable[None]],
|
|
769
767
|
None,
|
|
770
768
|
] = None,
|
|
771
769
|
):
|
|
@@ -824,13 +822,13 @@ class Attribute(EventEmitter):
|
|
|
824
822
|
READ_REQUIRES_AUTHORIZATION = Permissions.READ_REQUIRES_AUTHORIZATION
|
|
825
823
|
WRITE_REQUIRES_AUTHORIZATION = Permissions.WRITE_REQUIRES_AUTHORIZATION
|
|
826
824
|
|
|
827
|
-
value:
|
|
825
|
+
value: Any
|
|
828
826
|
|
|
829
827
|
def __init__(
|
|
830
828
|
self,
|
|
831
829
|
attribute_type: Union[str, bytes, UUID],
|
|
832
830
|
permissions: Union[str, Attribute.Permissions],
|
|
833
|
-
value:
|
|
831
|
+
value: Any = b'',
|
|
834
832
|
) -> None:
|
|
835
833
|
EventEmitter.__init__(self)
|
|
836
834
|
self.handle = 0
|
|
@@ -848,11 +846,7 @@ class Attribute(EventEmitter):
|
|
|
848
846
|
else:
|
|
849
847
|
self.type = attribute_type
|
|
850
848
|
|
|
851
|
-
|
|
852
|
-
if isinstance(value, str):
|
|
853
|
-
self.value = bytes(value, 'utf-8')
|
|
854
|
-
else:
|
|
855
|
-
self.value = value
|
|
849
|
+
self.value = value
|
|
856
850
|
|
|
857
851
|
def encode_value(self, value: Any) -> bytes:
|
|
858
852
|
return value
|
|
@@ -895,6 +889,8 @@ class Attribute(EventEmitter):
|
|
|
895
889
|
else:
|
|
896
890
|
value = self.value
|
|
897
891
|
|
|
892
|
+
self.emit('read', connection, value)
|
|
893
|
+
|
|
898
894
|
return self.encode_value(value)
|
|
899
895
|
|
|
900
896
|
async def write_value(self, connection: Connection, value_bytes: bytes) -> None:
|
bumble/controller.py
CHANGED
|
@@ -314,7 +314,7 @@ class Controller:
|
|
|
314
314
|
f'{color("CONTROLLER -> HOST", "green")}: {packet}'
|
|
315
315
|
)
|
|
316
316
|
if self.host:
|
|
317
|
-
self.host.on_packet(packet
|
|
317
|
+
self.host.on_packet(bytes(packet))
|
|
318
318
|
|
|
319
319
|
# This method allows the controller to emulate the same API as a transport source
|
|
320
320
|
async def wait_for_termination(self):
|
|
@@ -1192,7 +1192,7 @@ class Controller:
|
|
|
1192
1192
|
See Bluetooth spec Vol 4, Part E - 7.4.6 Read BD_ADDR Command
|
|
1193
1193
|
'''
|
|
1194
1194
|
bd_addr = (
|
|
1195
|
-
self._public_address
|
|
1195
|
+
bytes(self._public_address)
|
|
1196
1196
|
if self._public_address is not None
|
|
1197
1197
|
else bytes(6)
|
|
1198
1198
|
)
|
|
@@ -1543,6 +1543,41 @@ class Controller:
|
|
|
1543
1543
|
}
|
|
1544
1544
|
return bytes([HCI_SUCCESS])
|
|
1545
1545
|
|
|
1546
|
+
def on_hci_le_set_advertising_set_random_address_command(self, _command):
|
|
1547
|
+
'''
|
|
1548
|
+
See Bluetooth spec Vol 4, Part E - 7.8.52 LE Set Advertising Set Random Address
|
|
1549
|
+
Command
|
|
1550
|
+
'''
|
|
1551
|
+
return bytes([HCI_SUCCESS])
|
|
1552
|
+
|
|
1553
|
+
def on_hci_le_set_extended_advertising_parameters_command(self, _command):
|
|
1554
|
+
'''
|
|
1555
|
+
See Bluetooth spec Vol 4, Part E - 7.8.53 LE Set Extended Advertising Parameters
|
|
1556
|
+
Command
|
|
1557
|
+
'''
|
|
1558
|
+
return bytes([HCI_SUCCESS, 0])
|
|
1559
|
+
|
|
1560
|
+
def on_hci_le_set_extended_advertising_data_command(self, _command):
|
|
1561
|
+
'''
|
|
1562
|
+
See Bluetooth spec Vol 4, Part E - 7.8.54 LE Set Extended Advertising Data
|
|
1563
|
+
Command
|
|
1564
|
+
'''
|
|
1565
|
+
return bytes([HCI_SUCCESS])
|
|
1566
|
+
|
|
1567
|
+
def on_hci_le_set_extended_scan_response_data_command(self, _command):
|
|
1568
|
+
'''
|
|
1569
|
+
See Bluetooth spec Vol 4, Part E - 7.8.55 LE Set Extended Scan Response Data
|
|
1570
|
+
Command
|
|
1571
|
+
'''
|
|
1572
|
+
return bytes([HCI_SUCCESS])
|
|
1573
|
+
|
|
1574
|
+
def on_hci_le_set_extended_advertising_enable_command(self, _command):
|
|
1575
|
+
'''
|
|
1576
|
+
See Bluetooth spec Vol 4, Part E - 7.8.56 LE Set Extended Advertising Enable
|
|
1577
|
+
Command
|
|
1578
|
+
'''
|
|
1579
|
+
return bytes([HCI_SUCCESS])
|
|
1580
|
+
|
|
1546
1581
|
def on_hci_le_read_maximum_advertising_data_length_command(self, _command):
|
|
1547
1582
|
'''
|
|
1548
1583
|
See Bluetooth spec Vol 4, Part E - 7.8.57 LE Read Maximum Advertising Data
|
|
@@ -1557,6 +1592,27 @@ class Controller:
|
|
|
1557
1592
|
'''
|
|
1558
1593
|
return struct.pack('<BB', HCI_SUCCESS, 0xF0)
|
|
1559
1594
|
|
|
1595
|
+
def on_hci_le_set_periodic_advertising_parameters_command(self, _command):
|
|
1596
|
+
'''
|
|
1597
|
+
See Bluetooth spec Vol 4, Part E - 7.8.61 LE Set Periodic Advertising Parameters
|
|
1598
|
+
Command
|
|
1599
|
+
'''
|
|
1600
|
+
return bytes([HCI_SUCCESS])
|
|
1601
|
+
|
|
1602
|
+
def on_hci_le_set_periodic_advertising_data_command(self, _command):
|
|
1603
|
+
'''
|
|
1604
|
+
See Bluetooth spec Vol 4, Part E - 7.8.62 LE Set Periodic Advertising Data
|
|
1605
|
+
Command
|
|
1606
|
+
'''
|
|
1607
|
+
return bytes([HCI_SUCCESS])
|
|
1608
|
+
|
|
1609
|
+
def on_hci_le_set_periodic_advertising_enable_command(self, _command):
|
|
1610
|
+
'''
|
|
1611
|
+
See Bluetooth spec Vol 4, Part E - 7.8.63 LE Set Periodic Advertising Enable
|
|
1612
|
+
Command
|
|
1613
|
+
'''
|
|
1614
|
+
return bytes([HCI_SUCCESS])
|
|
1615
|
+
|
|
1560
1616
|
def on_hci_le_read_transmit_power_command(self, _command):
|
|
1561
1617
|
'''
|
|
1562
1618
|
See Bluetooth spec Vol 4, Part E - 7.8.74 LE Read Transmit Power Command
|