bumble 0.0.190__py3-none-any.whl → 0.0.192__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/gatt_client.py +16 -0
- bumble/hfp.py +735 -48
- bumble/host.py +1 -1
- bumble/profiles/device_information_service.py +12 -3
- bumble/transport/pyusb.py +95 -0
- bumble/transport/tcp_server.py +1 -0
- {bumble-0.0.190.dist-info → bumble-0.0.192.dist-info}/METADATA +4 -3
- {bumble-0.0.190.dist-info → bumble-0.0.192.dist-info}/RECORD +13 -13
- {bumble-0.0.190.dist-info → bumble-0.0.192.dist-info}/LICENSE +0 -0
- {bumble-0.0.190.dist-info → bumble-0.0.192.dist-info}/WHEEL +0 -0
- {bumble-0.0.190.dist-info → bumble-0.0.192.dist-info}/entry_points.txt +0 -0
- {bumble-0.0.190.dist-info → bumble-0.0.192.dist-info}/top_level.txt +0 -0
bumble/_version.py
CHANGED
bumble/gatt_client.py
CHANGED
|
@@ -90,6 +90,22 @@ if TYPE_CHECKING:
|
|
|
90
90
|
logger = logging.getLogger(__name__)
|
|
91
91
|
|
|
92
92
|
|
|
93
|
+
# -----------------------------------------------------------------------------
|
|
94
|
+
# Utils
|
|
95
|
+
# -----------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def show_services(services: Iterable[ServiceProxy]) -> None:
|
|
99
|
+
for service in services:
|
|
100
|
+
print(color(str(service), 'cyan'))
|
|
101
|
+
|
|
102
|
+
for characteristic in service.characteristics:
|
|
103
|
+
print(color(' ' + str(characteristic), 'magenta'))
|
|
104
|
+
|
|
105
|
+
for descriptor in characteristic.descriptors:
|
|
106
|
+
print(color(' ' + str(descriptor), 'green'))
|
|
107
|
+
|
|
108
|
+
|
|
93
109
|
# -----------------------------------------------------------------------------
|
|
94
110
|
# Proxies
|
|
95
111
|
# -----------------------------------------------------------------------------
|
bumble/hfp.py
CHANGED
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
# -----------------------------------------------------------------------------
|
|
16
16
|
# Imports
|
|
17
17
|
# -----------------------------------------------------------------------------
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import collections
|
|
18
21
|
import collections.abc
|
|
19
22
|
import logging
|
|
20
23
|
import asyncio
|
|
@@ -22,16 +25,32 @@ import dataclasses
|
|
|
22
25
|
import enum
|
|
23
26
|
import traceback
|
|
24
27
|
import pyee
|
|
25
|
-
|
|
28
|
+
import re
|
|
29
|
+
from typing import (
|
|
30
|
+
Dict,
|
|
31
|
+
List,
|
|
32
|
+
Union,
|
|
33
|
+
Set,
|
|
34
|
+
Any,
|
|
35
|
+
Optional,
|
|
36
|
+
Type,
|
|
37
|
+
Tuple,
|
|
38
|
+
ClassVar,
|
|
39
|
+
Iterable,
|
|
40
|
+
TYPE_CHECKING,
|
|
41
|
+
)
|
|
26
42
|
from typing_extensions import Self
|
|
27
43
|
|
|
28
44
|
from bumble import at
|
|
45
|
+
from bumble import device
|
|
29
46
|
from bumble import rfcomm
|
|
47
|
+
from bumble import sdp
|
|
30
48
|
from bumble.colors import color
|
|
31
49
|
from bumble.core import (
|
|
32
50
|
ProtocolError,
|
|
33
51
|
BT_GENERIC_AUDIO_SERVICE,
|
|
34
52
|
BT_HANDSFREE_SERVICE,
|
|
53
|
+
BT_HEADSET_AUDIO_GATEWAY_SERVICE,
|
|
35
54
|
BT_L2CAP_PROTOCOL_ID,
|
|
36
55
|
BT_RFCOMM_PROTOCOL_ID,
|
|
37
56
|
)
|
|
@@ -40,15 +59,6 @@ from bumble.hci import (
|
|
|
40
59
|
CodingFormat,
|
|
41
60
|
CodecID,
|
|
42
61
|
)
|
|
43
|
-
from bumble.sdp import (
|
|
44
|
-
DataElement,
|
|
45
|
-
ServiceAttribute,
|
|
46
|
-
SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
|
|
47
|
-
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
|
|
48
|
-
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
|
49
|
-
SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
|
50
|
-
SDP_SUPPORTED_FEATURES_ATTRIBUTE_ID,
|
|
51
|
-
)
|
|
52
62
|
|
|
53
63
|
|
|
54
64
|
# -----------------------------------------------------------------------------
|
|
@@ -329,6 +339,21 @@ class CallInfo:
|
|
|
329
339
|
type: Optional[int] = None
|
|
330
340
|
|
|
331
341
|
|
|
342
|
+
class CmeError(enum.IntEnum):
|
|
343
|
+
"""
|
|
344
|
+
CME ERROR codes (partial listed).
|
|
345
|
+
|
|
346
|
+
TS 127 007 - V6.8.0, 9.2.1 General errors
|
|
347
|
+
"""
|
|
348
|
+
|
|
349
|
+
PHONE_FAILURE = 0
|
|
350
|
+
OPERATION_NOT_ALLOWED = 3
|
|
351
|
+
OPERATION_NOT_SUPPORTED = 4
|
|
352
|
+
MEMORY_FULL = 20
|
|
353
|
+
INVALID_INDEX = 21
|
|
354
|
+
NOT_FOUND = 22
|
|
355
|
+
|
|
356
|
+
|
|
332
357
|
# -----------------------------------------------------------------------------
|
|
333
358
|
# Hands-Free Control Interoperability Requirements
|
|
334
359
|
# -----------------------------------------------------------------------------
|
|
@@ -402,12 +427,21 @@ STATUS_CODES = [
|
|
|
402
427
|
|
|
403
428
|
|
|
404
429
|
@dataclasses.dataclass
|
|
405
|
-
class
|
|
430
|
+
class HfConfiguration:
|
|
406
431
|
supported_hf_features: List[HfFeature]
|
|
407
432
|
supported_hf_indicators: List[HfIndicator]
|
|
408
433
|
supported_audio_codecs: List[AudioCodec]
|
|
409
434
|
|
|
410
435
|
|
|
436
|
+
@dataclasses.dataclass
|
|
437
|
+
class AgConfiguration:
|
|
438
|
+
supported_ag_features: Iterable[AgFeature]
|
|
439
|
+
supported_ag_indicators: collections.abc.Sequence[AgIndicatorState]
|
|
440
|
+
supported_hf_indicators: Iterable[HfIndicator]
|
|
441
|
+
supported_ag_call_hold_operations: Iterable[CallHoldOperation]
|
|
442
|
+
supported_audio_codecs: Iterable[AudioCodec]
|
|
443
|
+
|
|
444
|
+
|
|
411
445
|
class AtResponseType(enum.Enum):
|
|
412
446
|
"""
|
|
413
447
|
Indicates if a response is expected from an AT command, and if multiple responses are accepted.
|
|
@@ -435,18 +469,148 @@ class AtResponse:
|
|
|
435
469
|
)
|
|
436
470
|
|
|
437
471
|
|
|
472
|
+
@dataclasses.dataclass
|
|
473
|
+
class AtCommand:
|
|
474
|
+
class SubCode(str, enum.Enum):
|
|
475
|
+
NONE = ''
|
|
476
|
+
SET = '='
|
|
477
|
+
TEST = '=?'
|
|
478
|
+
READ = '?'
|
|
479
|
+
|
|
480
|
+
code: str
|
|
481
|
+
sub_code: SubCode
|
|
482
|
+
parameters: list
|
|
483
|
+
|
|
484
|
+
_PARSE_PATTERN: ClassVar[re.Pattern] = re.compile(
|
|
485
|
+
r'AT\+(?P<code>[A-Z]+)(?P<sub_code>=\?|=|\?)?(?P<parameters>.*)'
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
@classmethod
|
|
489
|
+
def parse_from(cls: Type[Self], buffer: bytearray) -> Self:
|
|
490
|
+
if not (match := cls._PARSE_PATTERN.fullmatch(buffer.decode())):
|
|
491
|
+
if buffer.startswith(b'ATA'):
|
|
492
|
+
return cls(code='A', sub_code=AtCommand.SubCode.NONE, parameters=[])
|
|
493
|
+
if buffer.startswith(b'ATD'):
|
|
494
|
+
return cls(
|
|
495
|
+
code='D', sub_code=AtCommand.SubCode.NONE, parameters=[buffer[3:]]
|
|
496
|
+
)
|
|
497
|
+
raise HfpProtocolError('Invalid command')
|
|
498
|
+
|
|
499
|
+
parameters = []
|
|
500
|
+
if parameters_text := match.group('parameters'):
|
|
501
|
+
parameters = at.parse_parameters(parameters_text.encode())
|
|
502
|
+
|
|
503
|
+
return cls(
|
|
504
|
+
code=match.group('code'),
|
|
505
|
+
sub_code=AtCommand.SubCode(match.group('sub_code') or ''),
|
|
506
|
+
parameters=parameters,
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
|
|
438
510
|
@dataclasses.dataclass
|
|
439
511
|
class AgIndicatorState:
|
|
440
|
-
|
|
441
|
-
|
|
512
|
+
"""State wrapper of AG indicator.
|
|
513
|
+
|
|
514
|
+
Attributes:
|
|
515
|
+
indicator: Indicator of this indicator state.
|
|
516
|
+
supported_values: Supported values of this indicator.
|
|
517
|
+
current_status: Current status of this indicator.
|
|
518
|
+
index: (HF only) Index of this indicator.
|
|
519
|
+
enabled: (AG only) Whether this indicator is enabled to report.
|
|
520
|
+
on_test_text: Text message reported in AT+CIND=? of this indicator.
|
|
521
|
+
"""
|
|
522
|
+
|
|
523
|
+
indicator: AgIndicator
|
|
442
524
|
supported_values: Set[int]
|
|
443
525
|
current_status: int
|
|
526
|
+
index: Optional[int] = None
|
|
527
|
+
enabled: bool = True
|
|
528
|
+
|
|
529
|
+
@property
|
|
530
|
+
def on_test_text(self) -> str:
|
|
531
|
+
min_value = min(self.supported_values)
|
|
532
|
+
max_value = max(self.supported_values)
|
|
533
|
+
if len(self.supported_values) == (max_value - min_value + 1):
|
|
534
|
+
supported_values_text = f'({min_value}-{max_value})'
|
|
535
|
+
else:
|
|
536
|
+
supported_values_text = (
|
|
537
|
+
f'({",".join(str(v) for v in self.supported_values)})'
|
|
538
|
+
)
|
|
539
|
+
return f'(\"{self.indicator.value}\",{supported_values_text})'
|
|
540
|
+
|
|
541
|
+
@classmethod
|
|
542
|
+
def call(cls: Type[Self]) -> Self:
|
|
543
|
+
"""Default call indicator state."""
|
|
544
|
+
return cls(
|
|
545
|
+
indicator=AgIndicator.CALL, supported_values={0, 1}, current_status=0
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
@classmethod
|
|
549
|
+
def callsetup(cls: Type[Self]) -> Self:
|
|
550
|
+
"""Default callsetup indicator state."""
|
|
551
|
+
return cls(
|
|
552
|
+
indicator=AgIndicator.CALL_SETUP,
|
|
553
|
+
supported_values={0, 1, 2, 3},
|
|
554
|
+
current_status=0,
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
@classmethod
|
|
558
|
+
def callheld(cls: Type[Self]) -> Self:
|
|
559
|
+
"""Default call indicator state."""
|
|
560
|
+
return cls(
|
|
561
|
+
indicator=AgIndicator.CALL_HELD,
|
|
562
|
+
supported_values={0, 1, 2},
|
|
563
|
+
current_status=0,
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
@classmethod
|
|
567
|
+
def service(cls: Type[Self]) -> Self:
|
|
568
|
+
"""Default service indicator state."""
|
|
569
|
+
return cls(
|
|
570
|
+
indicator=AgIndicator.SERVICE, supported_values={0, 1}, current_status=0
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
@classmethod
|
|
574
|
+
def signal(cls: Type[Self]) -> Self:
|
|
575
|
+
"""Default signal indicator state."""
|
|
576
|
+
return cls(
|
|
577
|
+
indicator=AgIndicator.SIGNAL,
|
|
578
|
+
supported_values={0, 1, 2, 3, 4, 5},
|
|
579
|
+
current_status=0,
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
@classmethod
|
|
583
|
+
def roam(cls: Type[Self]) -> Self:
|
|
584
|
+
"""Default roam indicator state."""
|
|
585
|
+
return cls(
|
|
586
|
+
indicator=AgIndicator.CALL, supported_values={0, 1}, current_status=0
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
@classmethod
|
|
590
|
+
def battchg(cls: Type[Self]) -> Self:
|
|
591
|
+
"""Default battery charge indicator state."""
|
|
592
|
+
return cls(
|
|
593
|
+
indicator=AgIndicator.BATTERY_CHARGE,
|
|
594
|
+
supported_values={0, 1, 2, 3, 4, 5},
|
|
595
|
+
current_status=0,
|
|
596
|
+
)
|
|
444
597
|
|
|
445
598
|
|
|
446
599
|
@dataclasses.dataclass
|
|
447
600
|
class HfIndicatorState:
|
|
601
|
+
"""State wrapper of HF indicator.
|
|
602
|
+
|
|
603
|
+
Attributes:
|
|
604
|
+
indicator: Indicator of this indicator state.
|
|
605
|
+
supported: Whether this indicator is supported.
|
|
606
|
+
enabled: Whether this indicator is enabled.
|
|
607
|
+
current_status: Current (last-reported) status value of this indicaotr.
|
|
608
|
+
"""
|
|
609
|
+
|
|
610
|
+
indicator: HfIndicator
|
|
448
611
|
supported: bool = False
|
|
449
612
|
enabled: bool = False
|
|
613
|
+
current_status: int = 0
|
|
450
614
|
|
|
451
615
|
|
|
452
616
|
class HfProtocol(pyee.EventEmitter):
|
|
@@ -464,6 +628,9 @@ class HfProtocol(pyee.EventEmitter):
|
|
|
464
628
|
ag_indicator: AgIndicator
|
|
465
629
|
"""
|
|
466
630
|
|
|
631
|
+
class HfLoopTermination(HfpProtocolError): ...
|
|
632
|
+
"""Termination signal for run() loop."""
|
|
633
|
+
|
|
467
634
|
supported_hf_features: int
|
|
468
635
|
supported_audio_codecs: List[AudioCodec]
|
|
469
636
|
|
|
@@ -477,14 +644,14 @@ class HfProtocol(pyee.EventEmitter):
|
|
|
477
644
|
command_lock: asyncio.Lock
|
|
478
645
|
if TYPE_CHECKING:
|
|
479
646
|
response_queue: asyncio.Queue[AtResponse]
|
|
480
|
-
unsolicited_queue: asyncio.Queue[AtResponse]
|
|
647
|
+
unsolicited_queue: asyncio.Queue[Optional[AtResponse]]
|
|
481
648
|
else:
|
|
482
649
|
response_queue: asyncio.Queue
|
|
483
650
|
unsolicited_queue: asyncio.Queue
|
|
484
651
|
read_buffer: bytearray
|
|
485
652
|
active_codec: AudioCodec
|
|
486
653
|
|
|
487
|
-
def __init__(self, dlc: rfcomm.DLC, configuration:
|
|
654
|
+
def __init__(self, dlc: rfcomm.DLC, configuration: HfConfiguration) -> None:
|
|
488
655
|
super().__init__()
|
|
489
656
|
|
|
490
657
|
# Configure internal state.
|
|
@@ -494,13 +661,14 @@ class HfProtocol(pyee.EventEmitter):
|
|
|
494
661
|
self.unsolicited_queue = asyncio.Queue()
|
|
495
662
|
self.read_buffer = bytearray()
|
|
496
663
|
self.active_codec = AudioCodec.CVSD
|
|
664
|
+
self._slc_initialized = False
|
|
497
665
|
|
|
498
666
|
# Build local features.
|
|
499
667
|
self.supported_hf_features = sum(configuration.supported_hf_features)
|
|
500
668
|
self.supported_audio_codecs = configuration.supported_audio_codecs
|
|
501
669
|
|
|
502
670
|
self.hf_indicators = {
|
|
503
|
-
indicator: HfIndicatorState()
|
|
671
|
+
indicator: HfIndicatorState(indicator=indicator)
|
|
504
672
|
for indicator in configuration.supported_hf_indicators
|
|
505
673
|
}
|
|
506
674
|
|
|
@@ -511,6 +679,10 @@ class HfProtocol(pyee.EventEmitter):
|
|
|
511
679
|
|
|
512
680
|
# Bind the AT reader to the RFCOMM channel.
|
|
513
681
|
self.dlc.sink = self._read_at
|
|
682
|
+
# Stop the run() loop when L2CAP is closed.
|
|
683
|
+
self.dlc.multiplexer.l2cap_channel.on(
|
|
684
|
+
'close', lambda: self.unsolicited_queue.put_nowait(None)
|
|
685
|
+
)
|
|
514
686
|
|
|
515
687
|
def supports_hf_feature(self, feature: HfFeature) -> bool:
|
|
516
688
|
return (self.supported_hf_features & feature) != 0
|
|
@@ -621,7 +793,7 @@ class HfProtocol(pyee.EventEmitter):
|
|
|
621
793
|
# If both the HF and AG do support the Codec Negotiation feature
|
|
622
794
|
# then the HF shall send the AT+BAC=<HF available codecs> command to
|
|
623
795
|
# the AG to notify the AG of the available codecs in the HF.
|
|
624
|
-
codecs = [str(c) for c in self.supported_audio_codecs]
|
|
796
|
+
codecs = [str(c.value) for c in self.supported_audio_codecs]
|
|
625
797
|
await self.execute_command(f"AT+BAC={','.join(codecs)}")
|
|
626
798
|
|
|
627
799
|
# 4.2.1.3 AG Indicators
|
|
@@ -639,7 +811,7 @@ class HfProtocol(pyee.EventEmitter):
|
|
|
639
811
|
|
|
640
812
|
self.ag_indicators = []
|
|
641
813
|
for index, indicator in enumerate(response.parameters):
|
|
642
|
-
description = indicator[0].decode()
|
|
814
|
+
description = AgIndicator(indicator[0].decode())
|
|
643
815
|
supported_values = []
|
|
644
816
|
for value in indicator[1]:
|
|
645
817
|
value = value.split(b'-')
|
|
@@ -697,7 +869,7 @@ class HfProtocol(pyee.EventEmitter):
|
|
|
697
869
|
# shall send the AT+BIND=<HF supported HF indicators> command to the AG
|
|
698
870
|
# to notify the AG of the supported indicators’ assigned numbers in the
|
|
699
871
|
# HF. The AG shall respond with OK
|
|
700
|
-
indicators = [str(i) for i in self.hf_indicators
|
|
872
|
+
indicators = [str(i.value) for i in self.hf_indicators]
|
|
701
873
|
await self.execute_command(f"AT+BIND={','.join(indicators)}")
|
|
702
874
|
|
|
703
875
|
# After having provided the AG with the HF indicators it supports,
|
|
@@ -733,6 +905,7 @@ class HfProtocol(pyee.EventEmitter):
|
|
|
733
905
|
self.hf_indicators[indicator].enabled = True
|
|
734
906
|
|
|
735
907
|
logger.info("SLC setup completed")
|
|
908
|
+
self._slc_initialized = True
|
|
736
909
|
|
|
737
910
|
async def setup_audio_connection(self):
|
|
738
911
|
"""4.11.2 Audio Connection Setup by HF."""
|
|
@@ -824,11 +997,13 @@ class HfProtocol(pyee.EventEmitter):
|
|
|
824
997
|
ag_indicator = self.ag_indicators[index - 1]
|
|
825
998
|
ag_indicator.current_status = value
|
|
826
999
|
self.emit('ag_indicator', ag_indicator)
|
|
827
|
-
logger.info(f"AG indicator updated: {ag_indicator.
|
|
1000
|
+
logger.info(f"AG indicator updated: {ag_indicator.indicator}, {value}")
|
|
828
1001
|
|
|
829
1002
|
async def handle_unsolicited(self):
|
|
830
1003
|
"""Handle unsolicited result codes sent by the audio gateway."""
|
|
831
1004
|
result = await self.unsolicited_queue.get()
|
|
1005
|
+
if not result:
|
|
1006
|
+
raise HfProtocol.HfLoopTermination()
|
|
832
1007
|
if result.code == "+BCS":
|
|
833
1008
|
await self.setup_codec_connection(int(result.parameters[0]))
|
|
834
1009
|
elif result.code == "+CIEV":
|
|
@@ -846,14 +1021,350 @@ class HfProtocol(pyee.EventEmitter):
|
|
|
846
1021
|
"""
|
|
847
1022
|
|
|
848
1023
|
try:
|
|
849
|
-
|
|
1024
|
+
if not self._slc_initialized:
|
|
1025
|
+
await self.initiate_slc()
|
|
850
1026
|
while True:
|
|
851
1027
|
await self.handle_unsolicited()
|
|
1028
|
+
except HfProtocol.HfLoopTermination:
|
|
1029
|
+
logger.info('Loop terminated')
|
|
852
1030
|
except Exception:
|
|
853
1031
|
logger.error("HFP-HF protocol failed with the following error:")
|
|
854
1032
|
logger.error(traceback.format_exc())
|
|
855
1033
|
|
|
856
1034
|
|
|
1035
|
+
class AgProtocol(pyee.EventEmitter):
|
|
1036
|
+
"""
|
|
1037
|
+
Implementation for the Audio-Gateway side of the Hands-Free profile.
|
|
1038
|
+
|
|
1039
|
+
Reference specification Hands-Free Profile v1.8.
|
|
1040
|
+
|
|
1041
|
+
Emitted events:
|
|
1042
|
+
slc_complete: Emit when SLC procedure is completed.
|
|
1043
|
+
codec_negotiation: When codec is renegotiated, notify the new codec.
|
|
1044
|
+
Args:
|
|
1045
|
+
active_codec: AudioCodec
|
|
1046
|
+
hf_indicator: When HF update their indicators, notify the new state.
|
|
1047
|
+
Args:
|
|
1048
|
+
hf_indicator: HfIndicator
|
|
1049
|
+
codec_connection_request: Emit when HF sends AT+BCC to request codec connection.
|
|
1050
|
+
answer: Emit when HF sends ATA to answer phone call.
|
|
1051
|
+
hang_up: Emit when HF sends AT+CHUP to hang up phone call.
|
|
1052
|
+
dial: Emit when HF sends ATD to dial phone call.
|
|
1053
|
+
"""
|
|
1054
|
+
|
|
1055
|
+
supported_hf_features: int
|
|
1056
|
+
supported_hf_indicators: Set[HfIndicator]
|
|
1057
|
+
supported_audio_codecs: List[AudioCodec]
|
|
1058
|
+
|
|
1059
|
+
supported_ag_features: int
|
|
1060
|
+
supported_ag_call_hold_operations: List[CallHoldOperation]
|
|
1061
|
+
|
|
1062
|
+
ag_indicators: List[AgIndicatorState]
|
|
1063
|
+
hf_indicators: collections.OrderedDict[HfIndicator, HfIndicatorState]
|
|
1064
|
+
|
|
1065
|
+
dlc: rfcomm.DLC
|
|
1066
|
+
|
|
1067
|
+
read_buffer: bytearray
|
|
1068
|
+
active_codec: AudioCodec
|
|
1069
|
+
|
|
1070
|
+
indicator_report_enabled: bool
|
|
1071
|
+
inband_ringtone_enabled: bool
|
|
1072
|
+
cme_error_enabled: bool
|
|
1073
|
+
_remained_slc_setup_features: Set[HfFeature]
|
|
1074
|
+
|
|
1075
|
+
def __init__(self, dlc: rfcomm.DLC, configuration: AgConfiguration) -> None:
|
|
1076
|
+
super().__init__()
|
|
1077
|
+
|
|
1078
|
+
# Configure internal state.
|
|
1079
|
+
self.dlc = dlc
|
|
1080
|
+
self.read_buffer = bytearray()
|
|
1081
|
+
self.active_codec = AudioCodec.CVSD
|
|
1082
|
+
|
|
1083
|
+
# Build local features.
|
|
1084
|
+
self.supported_ag_features = sum(configuration.supported_ag_features)
|
|
1085
|
+
self.supported_ag_call_hold_operations = list(
|
|
1086
|
+
configuration.supported_ag_call_hold_operations
|
|
1087
|
+
)
|
|
1088
|
+
self.ag_indicators = list(configuration.supported_ag_indicators)
|
|
1089
|
+
self.supported_hf_indicators = set(configuration.supported_hf_indicators)
|
|
1090
|
+
self.inband_ringtone_enabled = True
|
|
1091
|
+
self._remained_slc_setup_features = set()
|
|
1092
|
+
|
|
1093
|
+
# Clear remote features.
|
|
1094
|
+
self.supported_hf_features = 0
|
|
1095
|
+
self.supported_audio_codecs = []
|
|
1096
|
+
self.indicator_report_enabled = False
|
|
1097
|
+
self.cme_error_enabled = False
|
|
1098
|
+
|
|
1099
|
+
self.hf_indicators = collections.OrderedDict()
|
|
1100
|
+
|
|
1101
|
+
# Bind the AT reader to the RFCOMM channel.
|
|
1102
|
+
self.dlc.sink = self._read_at
|
|
1103
|
+
|
|
1104
|
+
def supports_hf_feature(self, feature: HfFeature) -> bool:
|
|
1105
|
+
return (self.supported_hf_features & feature) != 0
|
|
1106
|
+
|
|
1107
|
+
def supports_ag_feature(self, feature: AgFeature) -> bool:
|
|
1108
|
+
return (self.supported_ag_features & feature) != 0
|
|
1109
|
+
|
|
1110
|
+
def _read_at(self, data: bytes):
|
|
1111
|
+
"""
|
|
1112
|
+
Reads AT messages from the RFCOMM channel.
|
|
1113
|
+
"""
|
|
1114
|
+
# Append to the read buffer.
|
|
1115
|
+
self.read_buffer.extend(data)
|
|
1116
|
+
|
|
1117
|
+
# Locate the trailer.
|
|
1118
|
+
trailer = self.read_buffer.find(b'\r')
|
|
1119
|
+
if trailer == -1:
|
|
1120
|
+
return
|
|
1121
|
+
|
|
1122
|
+
# Isolate the AT response code and parameters.
|
|
1123
|
+
raw_command = self.read_buffer[:trailer]
|
|
1124
|
+
command = AtCommand.parse_from(raw_command)
|
|
1125
|
+
logger.debug(f"<<< {raw_command.decode()}")
|
|
1126
|
+
|
|
1127
|
+
# Consume the response bytes.
|
|
1128
|
+
self.read_buffer = self.read_buffer[trailer + 1 :]
|
|
1129
|
+
|
|
1130
|
+
if command.sub_code == AtCommand.SubCode.TEST:
|
|
1131
|
+
handler_name = f'_on_{command.code.lower()}_test'
|
|
1132
|
+
elif command.sub_code == AtCommand.SubCode.READ:
|
|
1133
|
+
handler_name = f'_on_{command.code.lower()}_read'
|
|
1134
|
+
else:
|
|
1135
|
+
handler_name = f'_on_{command.code.lower()}'
|
|
1136
|
+
|
|
1137
|
+
if handler := getattr(self, handler_name, None):
|
|
1138
|
+
handler(*command.parameters)
|
|
1139
|
+
else:
|
|
1140
|
+
logger.warning('Handler %s not found', handler_name)
|
|
1141
|
+
self.send_response('ERROR')
|
|
1142
|
+
|
|
1143
|
+
def send_response(self, response: str) -> None:
|
|
1144
|
+
"""Sends an AT response."""
|
|
1145
|
+
self.dlc.write(f'\r\n{response}\r\n')
|
|
1146
|
+
|
|
1147
|
+
def send_cme_error(self, error_code: CmeError) -> None:
|
|
1148
|
+
"""Sends an CME ERROR response.
|
|
1149
|
+
|
|
1150
|
+
If CME Error is not enabled by HF, sends ERROR instead.
|
|
1151
|
+
"""
|
|
1152
|
+
if self.cme_error_enabled:
|
|
1153
|
+
self.send_response(f'+CME ERROR: {error_code.value}')
|
|
1154
|
+
else:
|
|
1155
|
+
self.send_error()
|
|
1156
|
+
|
|
1157
|
+
def send_ok(self) -> None:
|
|
1158
|
+
"""Sends an OK response."""
|
|
1159
|
+
self.send_response('OK')
|
|
1160
|
+
|
|
1161
|
+
def send_error(self) -> None:
|
|
1162
|
+
"""Sends an ERROR response."""
|
|
1163
|
+
self.send_response('ERROR')
|
|
1164
|
+
|
|
1165
|
+
def set_inband_ringtone_enabled(self, enabled: bool) -> None:
|
|
1166
|
+
"""Enables or disables in-band ringtone."""
|
|
1167
|
+
|
|
1168
|
+
self.inband_ringtone_enabled = enabled
|
|
1169
|
+
self.send_response(f'+BSIR: {1 if enabled else 0}')
|
|
1170
|
+
|
|
1171
|
+
def update_ag_indicator(self, indicator: AgIndicator, value: int) -> None:
|
|
1172
|
+
"""Updates AG indicator.
|
|
1173
|
+
|
|
1174
|
+
Args:
|
|
1175
|
+
indicator: Name of the indicator.
|
|
1176
|
+
value: new value of the indicator.
|
|
1177
|
+
"""
|
|
1178
|
+
|
|
1179
|
+
search_result = next(
|
|
1180
|
+
(
|
|
1181
|
+
(index, state)
|
|
1182
|
+
for index, state in enumerate(self.ag_indicators)
|
|
1183
|
+
if state.indicator == indicator
|
|
1184
|
+
),
|
|
1185
|
+
None,
|
|
1186
|
+
)
|
|
1187
|
+
if not search_result:
|
|
1188
|
+
raise KeyError(f'{indicator} is not supported.')
|
|
1189
|
+
|
|
1190
|
+
index, indicator_state = search_result
|
|
1191
|
+
if not self.indicator_report_enabled:
|
|
1192
|
+
logger.warning('AG indicator report is disabled')
|
|
1193
|
+
if not indicator_state.enabled:
|
|
1194
|
+
logger.warning(f'AG indicator {indicator} is disabled')
|
|
1195
|
+
|
|
1196
|
+
indicator_state.current_status = value
|
|
1197
|
+
self.send_response(f'+CIEV: {index+1},{value}')
|
|
1198
|
+
|
|
1199
|
+
async def negotiate_codec(self, codec: AudioCodec) -> None:
|
|
1200
|
+
"""Starts codec negotiation."""
|
|
1201
|
+
|
|
1202
|
+
if not self.supports_ag_feature(AgFeature.CODEC_NEGOTIATION):
|
|
1203
|
+
logger.warning('Local does not support Codec Negotiation')
|
|
1204
|
+
if not self.supports_hf_feature(HfFeature.CODEC_NEGOTIATION):
|
|
1205
|
+
logger.warning('Peer does not support Codec Negotiation')
|
|
1206
|
+
if codec not in self.supported_audio_codecs:
|
|
1207
|
+
logger.warning(f'{codec} is not supported by peer')
|
|
1208
|
+
|
|
1209
|
+
at_bcs_future = asyncio.get_running_loop().create_future()
|
|
1210
|
+
self.once('codec_negotiation', at_bcs_future.set_result)
|
|
1211
|
+
self.send_response(f'+BCS: {codec.value}')
|
|
1212
|
+
if (new_codec := await at_bcs_future) != codec:
|
|
1213
|
+
raise HfpProtocolError(f'Expect codec: {codec}, but get {new_codec}')
|
|
1214
|
+
|
|
1215
|
+
def _check_remained_slc_commands(self) -> None:
|
|
1216
|
+
if not self._remained_slc_setup_features:
|
|
1217
|
+
self.emit('slc_complete')
|
|
1218
|
+
|
|
1219
|
+
def _on_brsf(self, hf_features: bytes) -> None:
|
|
1220
|
+
self.supported_hf_features = int(hf_features)
|
|
1221
|
+
self.send_response(f'+BRSF: {self.supported_ag_features}')
|
|
1222
|
+
self.send_ok()
|
|
1223
|
+
|
|
1224
|
+
if self.supports_hf_feature(
|
|
1225
|
+
HfFeature.HF_INDICATORS
|
|
1226
|
+
) and self.supports_ag_feature(AgFeature.HF_INDICATORS):
|
|
1227
|
+
self._remained_slc_setup_features.add(HfFeature.HF_INDICATORS)
|
|
1228
|
+
|
|
1229
|
+
if self.supports_hf_feature(
|
|
1230
|
+
HfFeature.THREE_WAY_CALLING
|
|
1231
|
+
) and self.supports_ag_feature(AgFeature.THREE_WAY_CALLING):
|
|
1232
|
+
self._remained_slc_setup_features.add(HfFeature.THREE_WAY_CALLING)
|
|
1233
|
+
|
|
1234
|
+
def _on_bac(self, *args) -> None:
|
|
1235
|
+
self.supported_audio_codecs = [AudioCodec(int(value)) for value in args]
|
|
1236
|
+
self.send_ok()
|
|
1237
|
+
|
|
1238
|
+
def _on_bcs(self, codec: bytes) -> None:
|
|
1239
|
+
self.active_codec = AudioCodec(int(codec))
|
|
1240
|
+
self.send_ok()
|
|
1241
|
+
self.emit('codec_negotiation', self.active_codec)
|
|
1242
|
+
|
|
1243
|
+
def _on_cind_test(self) -> None:
|
|
1244
|
+
if not self.ag_indicators:
|
|
1245
|
+
self.send_cme_error(CmeError.NOT_FOUND)
|
|
1246
|
+
return
|
|
1247
|
+
|
|
1248
|
+
indicator_list_str = ",".join(
|
|
1249
|
+
indicator.on_test_text for indicator in self.ag_indicators
|
|
1250
|
+
)
|
|
1251
|
+
self.send_response(f'+CIND: {indicator_list_str}')
|
|
1252
|
+
self.send_ok()
|
|
1253
|
+
|
|
1254
|
+
def _on_cind_read(self) -> None:
|
|
1255
|
+
if not self.ag_indicators:
|
|
1256
|
+
self.send_cme_error(CmeError.NOT_FOUND)
|
|
1257
|
+
return
|
|
1258
|
+
|
|
1259
|
+
indicator_list_str = ",".join(
|
|
1260
|
+
str(indicator.current_status) for indicator in self.ag_indicators
|
|
1261
|
+
)
|
|
1262
|
+
self.send_response(f'+CIND: {indicator_list_str}')
|
|
1263
|
+
self.send_ok()
|
|
1264
|
+
|
|
1265
|
+
self._check_remained_slc_commands()
|
|
1266
|
+
|
|
1267
|
+
def _on_cmer(
|
|
1268
|
+
self,
|
|
1269
|
+
mode: bytes,
|
|
1270
|
+
keypad: Optional[bytes] = None,
|
|
1271
|
+
display: Optional[bytes] = None,
|
|
1272
|
+
indicator: bytes = b'',
|
|
1273
|
+
) -> None:
|
|
1274
|
+
if int(mode) != 3 or keypad or display or int(indicator) not in (0, 1):
|
|
1275
|
+
logger.error(
|
|
1276
|
+
f'Unexpected values: mode={mode!r}, keypad={keypad!r}, '
|
|
1277
|
+
f'display={display!r}, indicator={indicator!r}'
|
|
1278
|
+
)
|
|
1279
|
+
self.send_cme_error(CmeError.INVALID_INDEX)
|
|
1280
|
+
|
|
1281
|
+
self.indicator_report_enabled = bool(int(indicator))
|
|
1282
|
+
self.send_ok()
|
|
1283
|
+
|
|
1284
|
+
def _on_cmee(self, enabled: bytes) -> None:
|
|
1285
|
+
self.cme_error_enabled = bool(int(enabled))
|
|
1286
|
+
self.send_ok()
|
|
1287
|
+
|
|
1288
|
+
def _on_bind(self, *args) -> None:
|
|
1289
|
+
if not self.supports_ag_feature(AgFeature.HF_INDICATORS):
|
|
1290
|
+
self.send_error()
|
|
1291
|
+
return
|
|
1292
|
+
|
|
1293
|
+
peer_supported_indicators = set(
|
|
1294
|
+
HfIndicator(int(indicator)) for indicator in args
|
|
1295
|
+
)
|
|
1296
|
+
self.hf_indicators = collections.OrderedDict(
|
|
1297
|
+
{
|
|
1298
|
+
indicator: HfIndicatorState(indicator=indicator)
|
|
1299
|
+
for indicator in self.supported_hf_indicators.intersection(
|
|
1300
|
+
peer_supported_indicators
|
|
1301
|
+
)
|
|
1302
|
+
}
|
|
1303
|
+
)
|
|
1304
|
+
self.send_ok()
|
|
1305
|
+
|
|
1306
|
+
def _on_bind_test(self) -> None:
|
|
1307
|
+
if not self.supports_ag_feature(AgFeature.HF_INDICATORS):
|
|
1308
|
+
self.send_error()
|
|
1309
|
+
return
|
|
1310
|
+
|
|
1311
|
+
hf_indicator_list_str = ",".join(
|
|
1312
|
+
str(indicator.value) for indicator in self.supported_hf_indicators
|
|
1313
|
+
)
|
|
1314
|
+
self.send_response(f'+BIND: ({hf_indicator_list_str})')
|
|
1315
|
+
self.send_ok()
|
|
1316
|
+
|
|
1317
|
+
def _on_bind_read(self) -> None:
|
|
1318
|
+
if not self.supports_ag_feature(AgFeature.HF_INDICATORS):
|
|
1319
|
+
self.send_error()
|
|
1320
|
+
return
|
|
1321
|
+
|
|
1322
|
+
for indicator in self.hf_indicators:
|
|
1323
|
+
self.send_response(f'+BIND: {indicator.value},1')
|
|
1324
|
+
|
|
1325
|
+
self.send_ok()
|
|
1326
|
+
|
|
1327
|
+
self._remained_slc_setup_features.remove(HfFeature.HF_INDICATORS)
|
|
1328
|
+
self._check_remained_slc_commands()
|
|
1329
|
+
|
|
1330
|
+
def _on_biev(self, index_bytes: bytes, value_bytes: bytes) -> None:
|
|
1331
|
+
if not self.supports_ag_feature(AgFeature.HF_INDICATORS):
|
|
1332
|
+
self.send_error()
|
|
1333
|
+
return
|
|
1334
|
+
|
|
1335
|
+
index = HfIndicator(int(index_bytes))
|
|
1336
|
+
if index not in self.hf_indicators:
|
|
1337
|
+
self.send_error()
|
|
1338
|
+
return
|
|
1339
|
+
|
|
1340
|
+
self.hf_indicators[index].current_status = int(value_bytes)
|
|
1341
|
+
self.emit('hf_indicator', self.hf_indicators[index])
|
|
1342
|
+
self.send_ok()
|
|
1343
|
+
|
|
1344
|
+
def _on_bia(self, *args) -> None:
|
|
1345
|
+
for enabled, state in zip(args, self.ag_indicators):
|
|
1346
|
+
state.enabled = bool(int(enabled))
|
|
1347
|
+
self.send_ok()
|
|
1348
|
+
|
|
1349
|
+
def _on_bcc(self) -> None:
|
|
1350
|
+
self.emit('codec_connection_request')
|
|
1351
|
+
self.send_ok()
|
|
1352
|
+
|
|
1353
|
+
def _on_a(self) -> None:
|
|
1354
|
+
"""ATA handler."""
|
|
1355
|
+
self.emit('answer')
|
|
1356
|
+
self.send_ok()
|
|
1357
|
+
|
|
1358
|
+
def _on_d(self, number: bytes) -> None:
|
|
1359
|
+
"""ATD handler."""
|
|
1360
|
+
self.emit('dial', number.decode())
|
|
1361
|
+
self.send_ok()
|
|
1362
|
+
|
|
1363
|
+
def _on_chup(self) -> None:
|
|
1364
|
+
self.emit('hang_up')
|
|
1365
|
+
self.send_ok()
|
|
1366
|
+
|
|
1367
|
+
|
|
857
1368
|
# -----------------------------------------------------------------------------
|
|
858
1369
|
# Normative SDP definitions
|
|
859
1370
|
# -----------------------------------------------------------------------------
|
|
@@ -907,9 +1418,12 @@ class AgSdpFeature(enum.IntFlag):
|
|
|
907
1418
|
VOICE_RECOGNITION_TEST = 0x80
|
|
908
1419
|
|
|
909
1420
|
|
|
910
|
-
def
|
|
911
|
-
service_record_handle: int,
|
|
912
|
-
|
|
1421
|
+
def make_hf_sdp_records(
|
|
1422
|
+
service_record_handle: int,
|
|
1423
|
+
rfcomm_channel: int,
|
|
1424
|
+
configuration: HfConfiguration,
|
|
1425
|
+
version: ProfileVersion = ProfileVersion.V1_8,
|
|
1426
|
+
) -> List[sdp.ServiceAttribute]:
|
|
913
1427
|
"""
|
|
914
1428
|
Generates the SDP record for HFP Hands-Free support.
|
|
915
1429
|
|
|
@@ -941,53 +1455,226 @@ def sdp_records(
|
|
|
941
1455
|
hf_supported_features |= HfSdpFeature.WIDE_BAND
|
|
942
1456
|
|
|
943
1457
|
return [
|
|
944
|
-
ServiceAttribute(
|
|
945
|
-
SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
|
|
946
|
-
DataElement.unsigned_integer_32(service_record_handle),
|
|
1458
|
+
sdp.ServiceAttribute(
|
|
1459
|
+
sdp.SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
|
|
1460
|
+
sdp.DataElement.unsigned_integer_32(service_record_handle),
|
|
1461
|
+
),
|
|
1462
|
+
sdp.ServiceAttribute(
|
|
1463
|
+
sdp.SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
|
|
1464
|
+
sdp.DataElement.sequence(
|
|
1465
|
+
[
|
|
1466
|
+
sdp.DataElement.uuid(BT_HANDSFREE_SERVICE),
|
|
1467
|
+
sdp.DataElement.uuid(BT_GENERIC_AUDIO_SERVICE),
|
|
1468
|
+
]
|
|
1469
|
+
),
|
|
1470
|
+
),
|
|
1471
|
+
sdp.ServiceAttribute(
|
|
1472
|
+
sdp.SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
|
1473
|
+
sdp.DataElement.sequence(
|
|
1474
|
+
[
|
|
1475
|
+
sdp.DataElement.sequence(
|
|
1476
|
+
[sdp.DataElement.uuid(BT_L2CAP_PROTOCOL_ID)]
|
|
1477
|
+
),
|
|
1478
|
+
sdp.DataElement.sequence(
|
|
1479
|
+
[
|
|
1480
|
+
sdp.DataElement.uuid(BT_RFCOMM_PROTOCOL_ID),
|
|
1481
|
+
sdp.DataElement.unsigned_integer_8(rfcomm_channel),
|
|
1482
|
+
]
|
|
1483
|
+
),
|
|
1484
|
+
]
|
|
1485
|
+
),
|
|
1486
|
+
),
|
|
1487
|
+
sdp.ServiceAttribute(
|
|
1488
|
+
sdp.SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
|
1489
|
+
sdp.DataElement.sequence(
|
|
1490
|
+
[
|
|
1491
|
+
sdp.DataElement.sequence(
|
|
1492
|
+
[
|
|
1493
|
+
sdp.DataElement.uuid(BT_HANDSFREE_SERVICE),
|
|
1494
|
+
sdp.DataElement.unsigned_integer_16(version),
|
|
1495
|
+
]
|
|
1496
|
+
)
|
|
1497
|
+
]
|
|
1498
|
+
),
|
|
1499
|
+
),
|
|
1500
|
+
sdp.ServiceAttribute(
|
|
1501
|
+
sdp.SDP_SUPPORTED_FEATURES_ATTRIBUTE_ID,
|
|
1502
|
+
sdp.DataElement.unsigned_integer_16(hf_supported_features),
|
|
1503
|
+
),
|
|
1504
|
+
]
|
|
1505
|
+
|
|
1506
|
+
|
|
1507
|
+
def make_ag_sdp_records(
|
|
1508
|
+
service_record_handle: int,
|
|
1509
|
+
rfcomm_channel: int,
|
|
1510
|
+
configuration: AgConfiguration,
|
|
1511
|
+
version: ProfileVersion = ProfileVersion.V1_8,
|
|
1512
|
+
) -> List[sdp.ServiceAttribute]:
|
|
1513
|
+
"""
|
|
1514
|
+
Generates the SDP record for HFP Audio-Gateway support.
|
|
1515
|
+
|
|
1516
|
+
The record exposes the features supported in the input configuration,
|
|
1517
|
+
and the allocated RFCOMM channel.
|
|
1518
|
+
"""
|
|
1519
|
+
|
|
1520
|
+
ag_supported_features = 0
|
|
1521
|
+
|
|
1522
|
+
if AgFeature.EC_NR in configuration.supported_ag_features:
|
|
1523
|
+
ag_supported_features |= AgSdpFeature.EC_NR
|
|
1524
|
+
if AgFeature.THREE_WAY_CALLING in configuration.supported_ag_features:
|
|
1525
|
+
ag_supported_features |= AgSdpFeature.THREE_WAY_CALLING
|
|
1526
|
+
if (
|
|
1527
|
+
AgFeature.ENHANCED_VOICE_RECOGNITION_STATUS
|
|
1528
|
+
in configuration.supported_ag_features
|
|
1529
|
+
):
|
|
1530
|
+
ag_supported_features |= AgSdpFeature.ENHANCED_VOICE_RECOGNITION_STATUS
|
|
1531
|
+
if AgFeature.VOICE_RECOGNITION_TEST in configuration.supported_ag_features:
|
|
1532
|
+
ag_supported_features |= AgSdpFeature.VOICE_RECOGNITION_TEST
|
|
1533
|
+
if AgFeature.IN_BAND_RING_TONE_CAPABILITY in configuration.supported_ag_features:
|
|
1534
|
+
ag_supported_features |= AgSdpFeature.IN_BAND_RING_TONE_CAPABILITY
|
|
1535
|
+
if AgFeature.VOICE_RECOGNITION_FUNCTION in configuration.supported_ag_features:
|
|
1536
|
+
ag_supported_features |= AgSdpFeature.VOICE_RECOGNITION_FUNCTION
|
|
1537
|
+
if AudioCodec.MSBC in configuration.supported_audio_codecs:
|
|
1538
|
+
ag_supported_features |= AgSdpFeature.WIDE_BAND
|
|
1539
|
+
|
|
1540
|
+
return [
|
|
1541
|
+
sdp.ServiceAttribute(
|
|
1542
|
+
sdp.SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
|
|
1543
|
+
sdp.DataElement.unsigned_integer_32(service_record_handle),
|
|
947
1544
|
),
|
|
948
|
-
ServiceAttribute(
|
|
949
|
-
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
|
|
950
|
-
DataElement.sequence(
|
|
1545
|
+
sdp.ServiceAttribute(
|
|
1546
|
+
sdp.SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
|
|
1547
|
+
sdp.DataElement.sequence(
|
|
951
1548
|
[
|
|
952
|
-
DataElement.uuid(
|
|
953
|
-
DataElement.uuid(BT_GENERIC_AUDIO_SERVICE),
|
|
1549
|
+
sdp.DataElement.uuid(BT_HEADSET_AUDIO_GATEWAY_SERVICE),
|
|
1550
|
+
sdp.DataElement.uuid(BT_GENERIC_AUDIO_SERVICE),
|
|
954
1551
|
]
|
|
955
1552
|
),
|
|
956
1553
|
),
|
|
957
|
-
ServiceAttribute(
|
|
958
|
-
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
|
959
|
-
DataElement.sequence(
|
|
1554
|
+
sdp.ServiceAttribute(
|
|
1555
|
+
sdp.SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
|
1556
|
+
sdp.DataElement.sequence(
|
|
960
1557
|
[
|
|
961
|
-
DataElement.sequence(
|
|
962
|
-
|
|
1558
|
+
sdp.DataElement.sequence(
|
|
1559
|
+
[sdp.DataElement.uuid(BT_L2CAP_PROTOCOL_ID)]
|
|
1560
|
+
),
|
|
1561
|
+
sdp.DataElement.sequence(
|
|
963
1562
|
[
|
|
964
|
-
DataElement.uuid(BT_RFCOMM_PROTOCOL_ID),
|
|
965
|
-
DataElement.unsigned_integer_8(rfcomm_channel),
|
|
1563
|
+
sdp.DataElement.uuid(BT_RFCOMM_PROTOCOL_ID),
|
|
1564
|
+
sdp.DataElement.unsigned_integer_8(rfcomm_channel),
|
|
966
1565
|
]
|
|
967
1566
|
),
|
|
968
1567
|
]
|
|
969
1568
|
),
|
|
970
1569
|
),
|
|
971
|
-
ServiceAttribute(
|
|
972
|
-
SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
|
973
|
-
DataElement.sequence(
|
|
1570
|
+
sdp.ServiceAttribute(
|
|
1571
|
+
sdp.SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
|
1572
|
+
sdp.DataElement.sequence(
|
|
974
1573
|
[
|
|
975
|
-
DataElement.sequence(
|
|
1574
|
+
sdp.DataElement.sequence(
|
|
976
1575
|
[
|
|
977
|
-
DataElement.uuid(
|
|
978
|
-
DataElement.unsigned_integer_16(
|
|
1576
|
+
sdp.DataElement.uuid(BT_HEADSET_AUDIO_GATEWAY_SERVICE),
|
|
1577
|
+
sdp.DataElement.unsigned_integer_16(version),
|
|
979
1578
|
]
|
|
980
1579
|
)
|
|
981
1580
|
]
|
|
982
1581
|
),
|
|
983
1582
|
),
|
|
984
|
-
ServiceAttribute(
|
|
985
|
-
SDP_SUPPORTED_FEATURES_ATTRIBUTE_ID,
|
|
986
|
-
DataElement.unsigned_integer_16(
|
|
1583
|
+
sdp.ServiceAttribute(
|
|
1584
|
+
sdp.SDP_SUPPORTED_FEATURES_ATTRIBUTE_ID,
|
|
1585
|
+
sdp.DataElement.unsigned_integer_16(ag_supported_features),
|
|
987
1586
|
),
|
|
988
1587
|
]
|
|
989
1588
|
|
|
990
1589
|
|
|
1590
|
+
async def find_hf_sdp_record(
|
|
1591
|
+
connection: device.Connection,
|
|
1592
|
+
) -> Optional[Tuple[int, ProfileVersion, HfSdpFeature]]:
|
|
1593
|
+
"""Searches a Hands-Free SDP record from remote device.
|
|
1594
|
+
|
|
1595
|
+
Args:
|
|
1596
|
+
connection: ACL connection to make SDP search.
|
|
1597
|
+
|
|
1598
|
+
Returns:
|
|
1599
|
+
Dictionary mapping from channel number to service class UUID list.
|
|
1600
|
+
"""
|
|
1601
|
+
async with sdp.Client(connection) as sdp_client:
|
|
1602
|
+
search_result = await sdp_client.search_attributes(
|
|
1603
|
+
uuids=[BT_HANDSFREE_SERVICE],
|
|
1604
|
+
attribute_ids=[
|
|
1605
|
+
sdp.SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
|
1606
|
+
sdp.SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
|
1607
|
+
sdp.SDP_SUPPORTED_FEATURES_ATTRIBUTE_ID,
|
|
1608
|
+
],
|
|
1609
|
+
)
|
|
1610
|
+
for attribute_lists in search_result:
|
|
1611
|
+
channel: Optional[int] = None
|
|
1612
|
+
version: Optional[ProfileVersion] = None
|
|
1613
|
+
features: Optional[HfSdpFeature] = None
|
|
1614
|
+
for attribute in attribute_lists:
|
|
1615
|
+
# The layout is [[L2CAP_PROTOCOL], [RFCOMM_PROTOCOL, RFCOMM_CHANNEL]].
|
|
1616
|
+
if attribute.id == sdp.SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID:
|
|
1617
|
+
protocol_descriptor_list = attribute.value.value
|
|
1618
|
+
channel = protocol_descriptor_list[1].value[1].value
|
|
1619
|
+
elif (
|
|
1620
|
+
attribute.id
|
|
1621
|
+
== sdp.SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID
|
|
1622
|
+
):
|
|
1623
|
+
profile_descriptor_list = attribute.value.value
|
|
1624
|
+
version = ProfileVersion(profile_descriptor_list[0].value[1].value)
|
|
1625
|
+
elif attribute.id == sdp.SDP_SUPPORTED_FEATURES_ATTRIBUTE_ID:
|
|
1626
|
+
features = HfSdpFeature(attribute.value.value)
|
|
1627
|
+
if not channel or not version or features is None:
|
|
1628
|
+
logger.warning(f"Bad result {attribute_lists}.")
|
|
1629
|
+
return None
|
|
1630
|
+
return (channel, version, features)
|
|
1631
|
+
return None
|
|
1632
|
+
|
|
1633
|
+
|
|
1634
|
+
async def find_ag_sdp_record(
|
|
1635
|
+
connection: device.Connection,
|
|
1636
|
+
) -> Optional[Tuple[int, ProfileVersion, AgSdpFeature]]:
|
|
1637
|
+
"""Searches an Audio-Gateway SDP record from remote device.
|
|
1638
|
+
|
|
1639
|
+
Args:
|
|
1640
|
+
connection: ACL connection to make SDP search.
|
|
1641
|
+
|
|
1642
|
+
Returns:
|
|
1643
|
+
Dictionary mapping from channel number to service class UUID list.
|
|
1644
|
+
"""
|
|
1645
|
+
async with sdp.Client(connection) as sdp_client:
|
|
1646
|
+
search_result = await sdp_client.search_attributes(
|
|
1647
|
+
uuids=[BT_HEADSET_AUDIO_GATEWAY_SERVICE],
|
|
1648
|
+
attribute_ids=[
|
|
1649
|
+
sdp.SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
|
1650
|
+
sdp.SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
|
1651
|
+
sdp.SDP_SUPPORTED_FEATURES_ATTRIBUTE_ID,
|
|
1652
|
+
],
|
|
1653
|
+
)
|
|
1654
|
+
for attribute_lists in search_result:
|
|
1655
|
+
channel: Optional[int] = None
|
|
1656
|
+
version: Optional[ProfileVersion] = None
|
|
1657
|
+
features: Optional[AgSdpFeature] = None
|
|
1658
|
+
for attribute in attribute_lists:
|
|
1659
|
+
# The layout is [[L2CAP_PROTOCOL], [RFCOMM_PROTOCOL, RFCOMM_CHANNEL]].
|
|
1660
|
+
if attribute.id == sdp.SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID:
|
|
1661
|
+
protocol_descriptor_list = attribute.value.value
|
|
1662
|
+
channel = protocol_descriptor_list[1].value[1].value
|
|
1663
|
+
elif (
|
|
1664
|
+
attribute.id
|
|
1665
|
+
== sdp.SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID
|
|
1666
|
+
):
|
|
1667
|
+
profile_descriptor_list = attribute.value.value
|
|
1668
|
+
version = ProfileVersion(profile_descriptor_list[0].value[1].value)
|
|
1669
|
+
elif attribute.id == sdp.SDP_SUPPORTED_FEATURES_ATTRIBUTE_ID:
|
|
1670
|
+
features = AgSdpFeature(attribute.value.value)
|
|
1671
|
+
if not channel or not version or features is None:
|
|
1672
|
+
logger.warning(f"Bad result {attribute_lists}.")
|
|
1673
|
+
return None
|
|
1674
|
+
return (channel, version, features)
|
|
1675
|
+
return None
|
|
1676
|
+
|
|
1677
|
+
|
|
991
1678
|
# -----------------------------------------------------------------------------
|
|
992
1679
|
# ESCO Codec Default Parameters
|
|
993
1680
|
# -----------------------------------------------------------------------------
|
bumble/host.py
CHANGED
|
@@ -184,7 +184,7 @@ class Host(AbortableEventEmitter):
|
|
|
184
184
|
self.long_term_key_provider = None
|
|
185
185
|
self.link_key_provider = None
|
|
186
186
|
self.pairing_io_capability_provider = None # Classic only
|
|
187
|
-
self.snooper = None
|
|
187
|
+
self.snooper: Optional[Snooper] = None
|
|
188
188
|
|
|
189
189
|
# Connect to the source and sink if specified
|
|
190
190
|
if controller_source:
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
import struct
|
|
20
20
|
from typing import Optional, Tuple
|
|
21
21
|
|
|
22
|
-
from
|
|
23
|
-
from
|
|
22
|
+
from bumble.gatt_client import ServiceProxy, ProfileServiceProxy, CharacteristicProxy
|
|
23
|
+
from bumble.gatt import (
|
|
24
24
|
GATT_DEVICE_INFORMATION_SERVICE,
|
|
25
25
|
GATT_FIRMWARE_REVISION_STRING_CHARACTERISTIC,
|
|
26
26
|
GATT_HARDWARE_REVISION_STRING_CHARACTERISTIC,
|
|
@@ -104,7 +104,16 @@ class DeviceInformationService(TemplateService):
|
|
|
104
104
|
class DeviceInformationServiceProxy(ProfileServiceProxy):
|
|
105
105
|
SERVICE_CLASS = DeviceInformationService
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
manufacturer_name: Optional[UTF8CharacteristicAdapter]
|
|
108
|
+
model_number: Optional[UTF8CharacteristicAdapter]
|
|
109
|
+
serial_number: Optional[UTF8CharacteristicAdapter]
|
|
110
|
+
hardware_revision: Optional[UTF8CharacteristicAdapter]
|
|
111
|
+
firmware_revision: Optional[UTF8CharacteristicAdapter]
|
|
112
|
+
software_revision: Optional[UTF8CharacteristicAdapter]
|
|
113
|
+
system_id: Optional[DelegatedCharacteristicAdapter]
|
|
114
|
+
ieee_regulatory_certification_data_list: Optional[CharacteristicProxy]
|
|
115
|
+
|
|
116
|
+
def __init__(self, service_proxy: ServiceProxy):
|
|
108
117
|
self.service_proxy = service_proxy
|
|
109
118
|
|
|
110
119
|
for field, uuid in (
|
bumble/transport/pyusb.py
CHANGED
|
@@ -23,11 +23,24 @@ import time
|
|
|
23
23
|
import usb.core
|
|
24
24
|
import usb.util
|
|
25
25
|
|
|
26
|
+
from typing import Optional
|
|
27
|
+
from usb.core import Device as UsbDevice
|
|
28
|
+
from usb.core import USBError
|
|
29
|
+
from usb.util import CTRL_TYPE_CLASS, CTRL_RECIPIENT_OTHER
|
|
30
|
+
from usb.legacy import REQ_SET_FEATURE, REQ_CLEAR_FEATURE, CLASS_HUB
|
|
31
|
+
|
|
26
32
|
from .common import Transport, ParserSource
|
|
27
33
|
from .. import hci
|
|
28
34
|
from ..colors import color
|
|
29
35
|
|
|
30
36
|
|
|
37
|
+
# -----------------------------------------------------------------------------
|
|
38
|
+
# Constant
|
|
39
|
+
# -----------------------------------------------------------------------------
|
|
40
|
+
USB_PORT_FEATURE_POWER = 8
|
|
41
|
+
POWER_CYCLE_DELAY = 1
|
|
42
|
+
RESET_DELAY = 3
|
|
43
|
+
|
|
31
44
|
# -----------------------------------------------------------------------------
|
|
32
45
|
# Logging
|
|
33
46
|
# -----------------------------------------------------------------------------
|
|
@@ -214,6 +227,10 @@ async def open_pyusb_transport(spec: str) -> Transport:
|
|
|
214
227
|
usb_find = libusb_package.find
|
|
215
228
|
|
|
216
229
|
# Find the device according to the spec moniker
|
|
230
|
+
power_cycle = False
|
|
231
|
+
if spec.startswith('!'):
|
|
232
|
+
power_cycle = True
|
|
233
|
+
spec = spec[1:]
|
|
217
234
|
if ':' in spec:
|
|
218
235
|
vendor_id, product_id = spec.split(':')
|
|
219
236
|
device = usb_find(idVendor=int(vendor_id, 16), idProduct=int(product_id, 16))
|
|
@@ -245,6 +262,14 @@ async def open_pyusb_transport(spec: str) -> Transport:
|
|
|
245
262
|
raise ValueError('device not found')
|
|
246
263
|
logger.debug(f'USB Device: {device}')
|
|
247
264
|
|
|
265
|
+
# Power Cycle the device
|
|
266
|
+
if power_cycle:
|
|
267
|
+
try:
|
|
268
|
+
device = await _power_cycle(device) # type: ignore
|
|
269
|
+
except Exception as e:
|
|
270
|
+
logging.debug(e)
|
|
271
|
+
logging.info(f"Unable to power cycle {hex(device.idVendor)} {hex(device.idProduct)}") # type: ignore
|
|
272
|
+
|
|
248
273
|
# Collect the metadata
|
|
249
274
|
device_metadata = {'vendor_id': device.idVendor, 'product_id': device.idProduct}
|
|
250
275
|
|
|
@@ -308,3 +333,73 @@ async def open_pyusb_transport(spec: str) -> Transport:
|
|
|
308
333
|
packet_sink.start()
|
|
309
334
|
|
|
310
335
|
return UsbTransport(device, packet_source, packet_sink)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
async def _power_cycle(device: UsbDevice) -> UsbDevice:
|
|
339
|
+
"""
|
|
340
|
+
For devices connected to compatible USB hubs: Performs a power cycle on a given USB device.
|
|
341
|
+
This involves temporarily disabling its port on the hub and then re-enabling it.
|
|
342
|
+
"""
|
|
343
|
+
device_path = f'{device.bus}-{".".join(map(str, device.port_numbers))}' # type: ignore
|
|
344
|
+
hub = _find_hub_by_device_path(device_path)
|
|
345
|
+
|
|
346
|
+
if hub:
|
|
347
|
+
try:
|
|
348
|
+
device_port = device.port_numbers[-1] # type: ignore
|
|
349
|
+
_set_port_status(hub, device_port, False)
|
|
350
|
+
await asyncio.sleep(POWER_CYCLE_DELAY)
|
|
351
|
+
_set_port_status(hub, device_port, True)
|
|
352
|
+
await asyncio.sleep(RESET_DELAY)
|
|
353
|
+
|
|
354
|
+
# Device needs to be find again otherwise it will appear as disconnected
|
|
355
|
+
return usb.core.find(idVendor=device.idVendor, idProduct=device.idProduct) # type: ignore
|
|
356
|
+
except USBError as e:
|
|
357
|
+
logger.error(f"Adjustment needed: Please revise the udev rule for device {hex(device.idVendor)}:{hex(device.idProduct)} for proper recognition.") # type: ignore
|
|
358
|
+
logger.error(e)
|
|
359
|
+
|
|
360
|
+
return device
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _set_port_status(device: UsbDevice, port: int, on: bool):
|
|
364
|
+
"""Sets the power status of a specific port on a USB hub."""
|
|
365
|
+
device.ctrl_transfer(
|
|
366
|
+
bmRequestType=CTRL_TYPE_CLASS | CTRL_RECIPIENT_OTHER,
|
|
367
|
+
bRequest=REQ_SET_FEATURE if on else REQ_CLEAR_FEATURE,
|
|
368
|
+
wIndex=port,
|
|
369
|
+
wValue=USB_PORT_FEATURE_POWER,
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _find_device_by_path(sys_path: str) -> Optional[UsbDevice]:
|
|
374
|
+
"""Finds a USB device based on its system path."""
|
|
375
|
+
bus_num, *port_parts = sys_path.split('-')
|
|
376
|
+
ports = [int(port) for port in port_parts[0].split('.')]
|
|
377
|
+
devices = usb.core.find(find_all=True, bus=int(bus_num))
|
|
378
|
+
if devices:
|
|
379
|
+
for device in devices:
|
|
380
|
+
if device.bus == int(bus_num) and list(device.port_numbers) == ports: # type: ignore
|
|
381
|
+
return device
|
|
382
|
+
|
|
383
|
+
return None
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _find_hub_by_device_path(sys_path: str) -> Optional[UsbDevice]:
|
|
387
|
+
"""Finds the USB hub associated with a specific device path."""
|
|
388
|
+
hub_sys_path = sys_path.rsplit('.', 1)[0]
|
|
389
|
+
hub_device = _find_device_by_path(hub_sys_path)
|
|
390
|
+
|
|
391
|
+
if hub_device is None:
|
|
392
|
+
return None
|
|
393
|
+
else:
|
|
394
|
+
return hub_device if _is_hub(hub_device) else None
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _is_hub(device: UsbDevice) -> bool:
|
|
398
|
+
"""Checks if a USB device is a hub"""
|
|
399
|
+
if device.bDeviceClass == CLASS_HUB: # type: ignore
|
|
400
|
+
return True
|
|
401
|
+
for config in device:
|
|
402
|
+
for interface in config:
|
|
403
|
+
if interface.bInterfaceClass == CLASS_HUB: # type: ignore
|
|
404
|
+
return True
|
|
405
|
+
return False
|
bumble/transport/tcp_server.py
CHANGED
|
@@ -30,6 +30,7 @@ logger = logging.getLogger(__name__)
|
|
|
30
30
|
|
|
31
31
|
# -----------------------------------------------------------------------------
|
|
32
32
|
|
|
33
|
+
|
|
33
34
|
# A pass-through function to ease mock testing.
|
|
34
35
|
async def _create_server(*args, **kw_args):
|
|
35
36
|
await asyncio.get_running_loop().create_server(*args, **kw_args)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: bumble
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.192
|
|
4
4
|
Summary: Bluetooth Stack for Apps, Emulation, Test and Experimentation
|
|
5
5
|
Home-page: https://github.com/google/bumble
|
|
6
6
|
Author: Google
|
|
@@ -11,7 +11,6 @@ License-File: LICENSE
|
|
|
11
11
|
Requires-Dist: pyee >=8.2.2
|
|
12
12
|
Requires-Dist: aiohttp ~=3.8 ; platform_system != "Emscripten"
|
|
13
13
|
Requires-Dist: appdirs >=1.4 ; platform_system != "Emscripten"
|
|
14
|
-
Requires-Dist: bt-test-interfaces >=0.0.6 ; platform_system != "Emscripten"
|
|
15
14
|
Requires-Dist: click >=8.1.3 ; platform_system != "Emscripten"
|
|
16
15
|
Requires-Dist: cryptography ==39 ; platform_system != "Emscripten"
|
|
17
16
|
Requires-Dist: grpcio >=1.62.1 ; platform_system != "Emscripten"
|
|
@@ -47,9 +46,11 @@ Provides-Extra: documentation
|
|
|
47
46
|
Requires-Dist: mkdocs >=1.4.0 ; extra == 'documentation'
|
|
48
47
|
Requires-Dist: mkdocs-material >=8.5.6 ; extra == 'documentation'
|
|
49
48
|
Requires-Dist: mkdocstrings[python] >=0.19.0 ; extra == 'documentation'
|
|
49
|
+
Provides-Extra: pandora
|
|
50
|
+
Requires-Dist: bt-test-interfaces >=0.0.6 ; extra == 'pandora'
|
|
50
51
|
Provides-Extra: test
|
|
51
52
|
Requires-Dist: pytest >=8.0 ; extra == 'test'
|
|
52
|
-
Requires-Dist: pytest-asyncio >=0.
|
|
53
|
+
Requires-Dist: pytest-asyncio >=0.23.5 ; extra == 'test'
|
|
53
54
|
Requires-Dist: pytest-html >=3.2.0 ; extra == 'test'
|
|
54
55
|
Requires-Dist: coverage >=6.4 ; extra == 'test'
|
|
55
56
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
bumble/__init__.py,sha256=Q8jkz6rgl95IMAeInQVt_2GLoJl3DcEP2cxtrQ-ho5c,110
|
|
2
|
-
bumble/_version.py,sha256=
|
|
2
|
+
bumble/_version.py,sha256=lSefnTH6R6u0mUz0L1w0kV_TidGT3hsmjlFRbEtBud0,415
|
|
3
3
|
bumble/a2dp.py,sha256=VEeAOCfT1ZqpwnEgel6DJ32vxR8jYX3IAaBfCqPdWO8,22675
|
|
4
4
|
bumble/at.py,sha256=kdrcsx2C8Rg61EWESD2QHwpZntkXkRBJLrPn9auv9K8,2961
|
|
5
5
|
bumble/att.py,sha256=TGzhhBKCQPA_P_eDDSNASJVfa3dCr-QzzrRB3GekrI0,32366
|
|
@@ -18,13 +18,13 @@ bumble/decoder.py,sha256=N9nMvuVhuwpnfw7EDVuNe9uYY6B6c3RY2dh8RhRPC1U,9608
|
|
|
18
18
|
bumble/device.py,sha256=XvAdIoGcYZIdlJzJNHRA_4nPYiJF1GRQZAp8HflKe60,167653
|
|
19
19
|
bumble/gap.py,sha256=dRU2_TWvqTDx80hxeSbXlWIeWvptWH4_XbItG5y948Q,2138
|
|
20
20
|
bumble/gatt.py,sha256=W7h8hEyxM8fu3HbAKYJ2HStb8NM7T98UICVnf4G9HDo,38447
|
|
21
|
-
bumble/gatt_client.py,sha256=
|
|
21
|
+
bumble/gatt_client.py,sha256=pJ29537m9L_cY2nrtEqZdssOEpg4147fF7vhz0BweyY,43071
|
|
22
22
|
bumble/gatt_server.py,sha256=uPYbn2-y0MLnyR8xxpOf18gPua_Q49pSlMR1zxEnU-Q,37118
|
|
23
23
|
bumble/hci.py,sha256=3MYwWLuuolxY9xMitm5tOHpqvGKqtsReZ6QUfI1DcVA,267323
|
|
24
24
|
bumble/helpers.py,sha256=m0w4UgFFNDEnXwHrDyfRlcBObdVed2fqXGL0lvR3c8s,12733
|
|
25
|
-
bumble/hfp.py,sha256=
|
|
25
|
+
bumble/hfp.py,sha256=zee-gfWdKK1gBEqF32u2BaMf5Uc6LsoepO-ZFV65bj8,66094
|
|
26
26
|
bumble/hid.py,sha256=Dd4rsmkRxcxt1IjoozJdu9Qd-QWruKJfsiYqTT89NDk,20590
|
|
27
|
-
bumble/host.py,sha256=
|
|
27
|
+
bumble/host.py,sha256=g9twZR9JapJdOD_HYFTbyA8U2dyUVKBEbq-2RtfSdQA,46928
|
|
28
28
|
bumble/keys.py,sha256=58BMWd8LocY0bazVQ-qw3DKrOxgilhYaKBkmJRcNPp4,12593
|
|
29
29
|
bumble/l2cap.py,sha256=8m_1Kv6Tk-M-DilkAz_OXx0XsiLUhXycEZjUkICwyj0,81064
|
|
30
30
|
bumble/link.py,sha256=QiiMSCZ0z0ko2oUEMYg6nbq-h5A_3DLN4pjqAx_E-SA,23980
|
|
@@ -79,7 +79,7 @@ bumble/profiles/bap.py,sha256=jY0wHBIlc_Qxv6j-3rF_4nI4uM2z4I8WT99Teu4o0S8,46006
|
|
|
79
79
|
bumble/profiles/battery_service.py,sha256=w-uF4jLoDozJOoykimb2RkrKjVyCke6ts2-h-F1PYyc,2292
|
|
80
80
|
bumble/profiles/cap.py,sha256=6gH7oOnUKjOggMPuB7rtbwj0AneoNmnWzQ_iR3io8e0,1945
|
|
81
81
|
bumble/profiles/csip.py,sha256=wzSpNRCOMWtKw2Yd9OTAzPoFDoQWG-KYwWdA6sUkwiI,10102
|
|
82
|
-
bumble/profiles/device_information_service.py,sha256=
|
|
82
|
+
bumble/profiles/device_information_service.py,sha256=RfqnXywcwcSTiFalxd1LVTTdeWLxHGsMvlvr9fI0GJI,6193
|
|
83
83
|
bumble/profiles/heart_rate_service.py,sha256=7V2LGcWLp6RurjWxsVgMWr3wPDt5aS9qjNxTbHcOK6o,8575
|
|
84
84
|
bumble/profiles/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
85
85
|
bumble/profiles/vcp.py,sha256=wkbTf2NRCbBtvpXplpNJq4dzXp6JGeaEHeeC1kHqW7s,7897
|
|
@@ -95,10 +95,10 @@ bumble/transport/file.py,sha256=eVM2V6Nk2nDAFdE7Rt01ZI3JdTovsH9OEU1gKYPJjpE,2010
|
|
|
95
95
|
bumble/transport/hci_socket.py,sha256=EdgWi3-O5yvYcH4R4BkPtG79pnUo7GQtXWawuUHDoDQ,6331
|
|
96
96
|
bumble/transport/pty.py,sha256=grTl-yvjMWHflNwuME4ccVqDbk6NIEgQMgH6Y9lf1fU,2732
|
|
97
97
|
bumble/transport/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
98
|
-
bumble/transport/pyusb.py,sha256=
|
|
98
|
+
bumble/transport/pyusb.py,sha256=Wa1QlfjwN_9bdisoaBVYXN0DfHQgwi4L7nWbUisZmVE,15504
|
|
99
99
|
bumble/transport/serial.py,sha256=loQxkeG7uE09enXWg2uGbxi6CeG70wn3kzPbEwULKw4,2446
|
|
100
100
|
bumble/transport/tcp_client.py,sha256=deyUJYpj04QE00Mw_PTU5PHPA6mr1Nui3f5-QCy2zOw,1854
|
|
101
|
-
bumble/transport/tcp_server.py,sha256=
|
|
101
|
+
bumble/transport/tcp_server.py,sha256=tvu7FuPeqiXfoj2HQU8wu4AiwKjDDDCKlKjgtqWc5hg,3779
|
|
102
102
|
bumble/transport/udp.py,sha256=di8I6HHACgBx3un-dzAahz9lTIUrh4LdeuYpeoifQEM,2239
|
|
103
103
|
bumble/transport/usb.py,sha256=dFNN-kGI3pMTXeT5Amwu2H6e4J48WAJotG_D18W3RBM,21399
|
|
104
104
|
bumble/transport/vhci.py,sha256=iI2WpighnvIP5zeyJUFSbjEdmCo24CWMdICamIcyJck,2250
|
|
@@ -137,9 +137,9 @@ bumble/vendor/android/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3h
|
|
|
137
137
|
bumble/vendor/android/hci.py,sha256=GZrkhaWmcMt1JpnRhv0NoySGkf2H4lNUV2f_omRZW0I,10741
|
|
138
138
|
bumble/vendor/zephyr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
139
139
|
bumble/vendor/zephyr/hci.py,sha256=d83bC0TvT947eN4roFjLkQefWtHOoNsr4xib2ctSkvA,3195
|
|
140
|
-
bumble-0.0.
|
|
141
|
-
bumble-0.0.
|
|
142
|
-
bumble-0.0.
|
|
143
|
-
bumble-0.0.
|
|
144
|
-
bumble-0.0.
|
|
145
|
-
bumble-0.0.
|
|
140
|
+
bumble-0.0.192.dist-info/LICENSE,sha256=FvaYh4NRWIGgS_OwoBs5gFgkCmAghZ-DYnIGBZPuw-s,12142
|
|
141
|
+
bumble-0.0.192.dist-info/METADATA,sha256=-PS1I7x-VWaCdUl1Wcgmnzd1W_uAtjjsuC1F_JPKL5Q,5695
|
|
142
|
+
bumble-0.0.192.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
143
|
+
bumble-0.0.192.dist-info/entry_points.txt,sha256=UkNj1KMZDhzOb7O4OU7Jn4YI5KaxJZgQF2GF64BwOlQ,883
|
|
144
|
+
bumble-0.0.192.dist-info/top_level.txt,sha256=tV6JJKaHPYMFiJYiBYFW24PCcfLxTJZdlu6BmH3Cb00,7
|
|
145
|
+
bumble-0.0.192.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|