bumble 0.0.220__py3-none-any.whl → 0.0.222__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/a2dp.py +5 -5
- bumble/apps/auracast.py +746 -473
- bumble/apps/bench.py +4 -5
- bumble/apps/console.py +5 -10
- bumble/apps/controller_info.py +12 -7
- bumble/apps/controller_loopback.py +1 -2
- bumble/apps/device_info.py +2 -3
- bumble/apps/gatt_dump.py +0 -1
- bumble/apps/lea_unicast/app.py +1 -1
- bumble/apps/pair.py +49 -46
- bumble/apps/pandora_server.py +2 -2
- bumble/apps/player/player.py +10 -12
- bumble/apps/rfcomm_bridge.py +10 -11
- bumble/apps/scan.py +1 -3
- bumble/apps/speaker/speaker.py +3 -4
- bumble/at.py +4 -5
- bumble/att.py +91 -25
- bumble/audio/io.py +5 -3
- bumble/avc.py +1 -2
- bumble/avctp.py +2 -3
- bumble/avdtp.py +53 -57
- bumble/avrcp.py +25 -27
- bumble/codecs.py +15 -15
- bumble/colors.py +7 -8
- bumble/controller.py +663 -391
- bumble/core.py +41 -49
- bumble/crypto/__init__.py +2 -1
- bumble/crypto/builtin.py +2 -8
- bumble/data_types.py +2 -1
- bumble/decoder.py +2 -3
- bumble/device.py +171 -142
- bumble/drivers/__init__.py +3 -2
- bumble/drivers/intel.py +6 -8
- bumble/drivers/rtk.py +1 -1
- bumble/gatt.py +9 -9
- bumble/gatt_adapters.py +6 -6
- bumble/gatt_client.py +110 -60
- bumble/gatt_server.py +209 -139
- bumble/hci.py +87 -74
- bumble/helpers.py +5 -5
- bumble/hfp.py +27 -26
- bumble/hid.py +9 -9
- bumble/host.py +44 -50
- bumble/keys.py +17 -17
- bumble/l2cap.py +1070 -218
- bumble/link.py +26 -159
- bumble/ll.py +200 -0
- bumble/pairing.py +14 -15
- bumble/pandora/__init__.py +2 -2
- bumble/pandora/device.py +6 -4
- bumble/pandora/host.py +19 -10
- bumble/pandora/l2cap.py +8 -9
- bumble/pandora/security.py +18 -16
- bumble/pandora/utils.py +4 -4
- bumble/profiles/aics.py +6 -8
- bumble/profiles/ams.py +3 -5
- bumble/profiles/ancs.py +11 -11
- bumble/profiles/ascs.py +5 -5
- bumble/profiles/asha.py +10 -9
- bumble/profiles/bass.py +9 -3
- bumble/profiles/battery_service.py +1 -2
- bumble/profiles/csip.py +9 -10
- bumble/profiles/device_information_service.py +16 -17
- bumble/profiles/gap.py +3 -4
- bumble/profiles/gatt_service.py +0 -1
- bumble/profiles/gmap.py +12 -13
- bumble/profiles/hap.py +3 -3
- bumble/profiles/heart_rate_service.py +7 -8
- bumble/profiles/le_audio.py +1 -1
- bumble/profiles/mcp.py +28 -28
- bumble/profiles/pacs.py +13 -17
- bumble/profiles/pbp.py +16 -0
- bumble/profiles/vcs.py +2 -2
- bumble/profiles/vocs.py +6 -9
- bumble/rfcomm.py +19 -18
- bumble/sdp.py +12 -11
- bumble/smp.py +20 -30
- bumble/snoop.py +2 -1
- bumble/tools/generate_company_id_list.py +1 -1
- bumble/tools/intel_util.py +2 -2
- bumble/tools/rtk_fw_download.py +1 -1
- bumble/tools/rtk_util.py +1 -1
- bumble/transport/__init__.py +1 -2
- bumble/transport/android_emulator.py +2 -3
- bumble/transport/android_netsim.py +49 -40
- bumble/transport/common.py +9 -9
- bumble/transport/file.py +1 -2
- bumble/transport/hci_socket.py +2 -3
- bumble/transport/pty.py +3 -5
- bumble/transport/pyusb.py +8 -5
- bumble/transport/serial.py +1 -2
- bumble/transport/vhci.py +1 -2
- bumble/transport/ws_server.py +2 -3
- bumble/utils.py +22 -9
- bumble/vendor/android/hci.py +4 -2
- {bumble-0.0.220.dist-info → bumble-0.0.222.dist-info}/METADATA +3 -2
- {bumble-0.0.220.dist-info → bumble-0.0.222.dist-info}/RECORD +102 -101
- {bumble-0.0.220.dist-info → bumble-0.0.222.dist-info}/WHEEL +0 -0
- {bumble-0.0.220.dist-info → bumble-0.0.222.dist-info}/entry_points.txt +0 -0
- {bumble-0.0.220.dist-info → bumble-0.0.222.dist-info}/licenses/LICENSE +0 -0
- {bumble-0.0.220.dist-info → bumble-0.0.222.dist-info}/top_level.txt +0 -0
bumble/smp.py
CHANGED
|
@@ -27,17 +27,9 @@ from __future__ import annotations
|
|
|
27
27
|
import asyncio
|
|
28
28
|
import enum
|
|
29
29
|
import logging
|
|
30
|
+
from collections.abc import Awaitable, Callable
|
|
30
31
|
from dataclasses import dataclass, field
|
|
31
|
-
from typing import
|
|
32
|
-
TYPE_CHECKING,
|
|
33
|
-
Any,
|
|
34
|
-
Awaitable,
|
|
35
|
-
Callable,
|
|
36
|
-
ClassVar,
|
|
37
|
-
Optional,
|
|
38
|
-
TypeVar,
|
|
39
|
-
cast,
|
|
40
|
-
)
|
|
32
|
+
from typing import TYPE_CHECKING, ClassVar, TypeVar, cast
|
|
41
33
|
|
|
42
34
|
from bumble import crypto, utils
|
|
43
35
|
from bumble.colors import color
|
|
@@ -213,10 +205,10 @@ class SMP_Command:
|
|
|
213
205
|
fields: ClassVar[Fields]
|
|
214
206
|
code: int = field(default=0, init=False)
|
|
215
207
|
name: str = field(default='', init=False)
|
|
216
|
-
_payload:
|
|
208
|
+
_payload: bytes | None = field(default=None, init=False)
|
|
217
209
|
|
|
218
210
|
@classmethod
|
|
219
|
-
def from_bytes(cls, pdu: bytes) ->
|
|
211
|
+
def from_bytes(cls, pdu: bytes) -> SMP_Command:
|
|
220
212
|
code = pdu[0]
|
|
221
213
|
|
|
222
214
|
subclass = SMP_Command.smp_classes.get(code)
|
|
@@ -554,7 +546,7 @@ class OobContext:
|
|
|
554
546
|
r: bytes
|
|
555
547
|
|
|
556
548
|
def __init__(
|
|
557
|
-
self, ecc_key:
|
|
549
|
+
self, ecc_key: crypto.EccKey | None = None, r: bytes | None = None
|
|
558
550
|
) -> None:
|
|
559
551
|
self.ecc_key = crypto.EccKey.generate() if ecc_key is None else ecc_key
|
|
560
552
|
self.r = crypto.r() if r is None else r
|
|
@@ -570,7 +562,7 @@ class OobLegacyContext:
|
|
|
570
562
|
|
|
571
563
|
tk: bytes
|
|
572
564
|
|
|
573
|
-
def __init__(self, tk:
|
|
565
|
+
def __init__(self, tk: bytes | None = None) -> None:
|
|
574
566
|
self.tk = crypto.r() if tk is None else tk
|
|
575
567
|
|
|
576
568
|
|
|
@@ -677,31 +669,31 @@ class Session:
|
|
|
677
669
|
self.stk = None
|
|
678
670
|
self.ltk_ediv = 0
|
|
679
671
|
self.ltk_rand = bytes(8)
|
|
680
|
-
self.link_key:
|
|
672
|
+
self.link_key: bytes | None = None
|
|
681
673
|
self.maximum_encryption_key_size: int = 0
|
|
682
674
|
self.initiator_key_distribution: int = 0
|
|
683
675
|
self.responder_key_distribution: int = 0
|
|
684
|
-
self.peer_random_value:
|
|
676
|
+
self.peer_random_value: bytes | None = None
|
|
685
677
|
self.peer_public_key_x: bytes = bytes(32)
|
|
686
678
|
self.peer_public_key_y = bytes(32)
|
|
687
679
|
self.peer_ltk = None
|
|
688
680
|
self.peer_ediv = None
|
|
689
|
-
self.peer_rand:
|
|
681
|
+
self.peer_rand: bytes | None = None
|
|
690
682
|
self.peer_identity_resolving_key = None
|
|
691
|
-
self.peer_bd_addr:
|
|
683
|
+
self.peer_bd_addr: Address | None = None
|
|
692
684
|
self.peer_signature_key = None
|
|
693
685
|
self.peer_expected_distributions: list[type[SMP_Command]] = []
|
|
694
686
|
self.dh_key = b''
|
|
695
687
|
self.confirm_value = None
|
|
696
|
-
self.passkey:
|
|
688
|
+
self.passkey: int | None = None
|
|
697
689
|
self.passkey_ready = asyncio.Event()
|
|
698
690
|
self.passkey_step = 0
|
|
699
691
|
self.passkey_display = False
|
|
700
692
|
self.pairing_method: PairingMethod = PairingMethod.JUST_WORKS
|
|
701
693
|
self.pairing_config = pairing_config
|
|
702
|
-
self.wait_before_continuing:
|
|
694
|
+
self.wait_before_continuing: asyncio.Future[None] | None = None
|
|
703
695
|
self.completed = False
|
|
704
|
-
self.ctkd_task:
|
|
696
|
+
self.ctkd_task: Awaitable[None] | None = None
|
|
705
697
|
|
|
706
698
|
# Decide if we're the initiator or the responder
|
|
707
699
|
self.is_initiator = is_initiator
|
|
@@ -720,7 +712,7 @@ class Session:
|
|
|
720
712
|
|
|
721
713
|
# Create a future that can be used to wait for the session to complete
|
|
722
714
|
if self.is_initiator:
|
|
723
|
-
self.pairing_result:
|
|
715
|
+
self.pairing_result: asyncio.Future[None] | None = (
|
|
724
716
|
asyncio.get_running_loop().create_future()
|
|
725
717
|
)
|
|
726
718
|
else:
|
|
@@ -828,7 +820,7 @@ class Session:
|
|
|
828
820
|
def auth_req(self) -> int:
|
|
829
821
|
return smp_auth_req(self.bonding, self.mitm, self.sc, self.keypress, self.ct2)
|
|
830
822
|
|
|
831
|
-
def get_long_term_key(self, rand: bytes, ediv: int) ->
|
|
823
|
+
def get_long_term_key(self, rand: bytes, ediv: int) -> bytes | None:
|
|
832
824
|
if not self.sc and not self.completed:
|
|
833
825
|
if rand == self.ltk_rand and ediv == self.ltk_ediv:
|
|
834
826
|
return self.stk
|
|
@@ -939,7 +931,7 @@ class Session:
|
|
|
939
931
|
self.pairing_config.delegate.display_number(self.passkey, digits=6)
|
|
940
932
|
)
|
|
941
933
|
|
|
942
|
-
def input_passkey(self, next_steps:
|
|
934
|
+
def input_passkey(self, next_steps: Callable[[], None] | None = None) -> None:
|
|
943
935
|
# Prompt the user for the passkey displayed on the peer
|
|
944
936
|
def after_input(passkey: int) -> None:
|
|
945
937
|
self.passkey = passkey
|
|
@@ -956,7 +948,7 @@ class Session:
|
|
|
956
948
|
self.prompt_user_for_number(after_input)
|
|
957
949
|
|
|
958
950
|
def display_or_input_passkey(
|
|
959
|
-
self, next_steps:
|
|
951
|
+
self, next_steps: Callable[[], None] | None = None
|
|
960
952
|
) -> None:
|
|
961
953
|
if self.passkey_display:
|
|
962
954
|
|
|
@@ -1006,7 +998,6 @@ class Session:
|
|
|
1006
998
|
self.send_command(response)
|
|
1007
999
|
|
|
1008
1000
|
def send_pairing_confirm_command(self) -> None:
|
|
1009
|
-
|
|
1010
1001
|
if self.pairing_method != PairingMethod.OOB:
|
|
1011
1002
|
self.r = crypto.r()
|
|
1012
1003
|
logger.debug(f'generated random: {self.r.hex()}')
|
|
@@ -1842,7 +1833,6 @@ class Session:
|
|
|
1842
1833
|
self.send_public_key_command()
|
|
1843
1834
|
|
|
1844
1835
|
def next_steps() -> None:
|
|
1845
|
-
|
|
1846
1836
|
if self.pairing_method in (
|
|
1847
1837
|
PairingMethod.JUST_WORKS,
|
|
1848
1838
|
PairingMethod.NUMERIC_COMPARISON,
|
|
@@ -1929,7 +1919,7 @@ class Manager(utils.EventEmitter):
|
|
|
1929
1919
|
sessions: dict[int, Session]
|
|
1930
1920
|
pairing_config_factory: Callable[[Connection], PairingConfig]
|
|
1931
1921
|
session_proxy: type[Session]
|
|
1932
|
-
_ecc_key:
|
|
1922
|
+
_ecc_key: crypto.EccKey | None
|
|
1933
1923
|
|
|
1934
1924
|
def __init__(
|
|
1935
1925
|
self,
|
|
@@ -2022,7 +2012,7 @@ class Manager(utils.EventEmitter):
|
|
|
2022
2012
|
self.device.on_pairing_start(session.connection)
|
|
2023
2013
|
|
|
2024
2014
|
async def on_pairing(
|
|
2025
|
-
self, session: Session, identity_address:
|
|
2015
|
+
self, session: Session, identity_address: Address | None, keys: PairingKeys
|
|
2026
2016
|
) -> None:
|
|
2027
2017
|
# Store the keys in the key store
|
|
2028
2018
|
if self.device.keystore and identity_address is not None:
|
|
@@ -2041,7 +2031,7 @@ class Manager(utils.EventEmitter):
|
|
|
2041
2031
|
|
|
2042
2032
|
def get_long_term_key(
|
|
2043
2033
|
self, connection: Connection, rand: bytes, ediv: int
|
|
2044
|
-
) ->
|
|
2034
|
+
) -> bytes | None:
|
|
2045
2035
|
if session := self.sessions.get(connection.handle):
|
|
2046
2036
|
return session.get_long_term_key(rand, ediv)
|
|
2047
2037
|
|
bumble/snoop.py
CHANGED
|
@@ -16,13 +16,14 @@ import datetime
|
|
|
16
16
|
import logging
|
|
17
17
|
import os
|
|
18
18
|
import struct
|
|
19
|
+
from collections.abc import Generator
|
|
19
20
|
|
|
20
21
|
# -----------------------------------------------------------------------------
|
|
21
22
|
# Imports
|
|
22
23
|
# -----------------------------------------------------------------------------
|
|
23
24
|
from contextlib import contextmanager
|
|
24
25
|
from enum import IntEnum
|
|
25
|
-
from typing import BinaryIO
|
|
26
|
+
from typing import BinaryIO
|
|
26
27
|
|
|
27
28
|
from bumble import core
|
|
28
29
|
from bumble.hci import HCI_COMMAND_PACKET, HCI_EVENT_PACKET
|
|
@@ -27,7 +27,7 @@ import sys
|
|
|
27
27
|
import yaml
|
|
28
28
|
|
|
29
29
|
# -----------------------------------------------------------------------------
|
|
30
|
-
with open(sys.argv[1]
|
|
30
|
+
with open(sys.argv[1]) as yaml_file:
|
|
31
31
|
root = yaml.safe_load(yaml_file)
|
|
32
32
|
companies = {}
|
|
33
33
|
for company in root["company_identifiers"]:
|
bumble/tools/intel_util.py
CHANGED
|
@@ -18,7 +18,7 @@ import asyncio
|
|
|
18
18
|
# Imports
|
|
19
19
|
# -----------------------------------------------------------------------------
|
|
20
20
|
import logging
|
|
21
|
-
from typing import Any
|
|
21
|
+
from typing import Any
|
|
22
22
|
|
|
23
23
|
import click
|
|
24
24
|
|
|
@@ -47,7 +47,7 @@ def print_device_info(device_info: dict[intel.ValueType, Any]) -> None:
|
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
# -----------------------------------------------------------------------------
|
|
50
|
-
async def get_driver(host: Host, force: bool) ->
|
|
50
|
+
async def get_driver(host: Host, force: bool) -> intel.Driver | None:
|
|
51
51
|
# Create a driver
|
|
52
52
|
driver = await intel.Driver.for_host(host, force)
|
|
53
53
|
if driver is None:
|
bumble/tools/rtk_fw_download.py
CHANGED
|
@@ -21,11 +21,11 @@ import urllib.error
|
|
|
21
21
|
import urllib.request
|
|
22
22
|
|
|
23
23
|
import click
|
|
24
|
+
from bumble.tools import rtk_util
|
|
24
25
|
|
|
25
26
|
import bumble.logging
|
|
26
27
|
from bumble.colors import color
|
|
27
28
|
from bumble.drivers import rtk
|
|
28
|
-
from bumble.tools import rtk_util
|
|
29
29
|
|
|
30
30
|
# -----------------------------------------------------------------------------
|
|
31
31
|
# Logging
|
bumble/tools/rtk_util.py
CHANGED
bumble/transport/__init__.py
CHANGED
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
import logging
|
|
19
19
|
import os
|
|
20
20
|
import re
|
|
21
|
-
from typing import Optional
|
|
22
21
|
|
|
23
22
|
from bumble import utils
|
|
24
23
|
from bumble.snoop import create_snooper
|
|
@@ -111,7 +110,7 @@ async def open_transport(name: str) -> Transport:
|
|
|
111
110
|
|
|
112
111
|
|
|
113
112
|
# -----------------------------------------------------------------------------
|
|
114
|
-
async def _open_transport(scheme: str, spec:
|
|
113
|
+
async def _open_transport(scheme: str, spec: str | None) -> Transport:
|
|
115
114
|
# pylint: disable=import-outside-toplevel
|
|
116
115
|
# pylint: disable=too-many-return-statements
|
|
117
116
|
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
# Imports
|
|
17
17
|
# -----------------------------------------------------------------------------
|
|
18
18
|
import logging
|
|
19
|
-
from typing import Optional, Union
|
|
20
19
|
|
|
21
20
|
import grpc.aio
|
|
22
21
|
|
|
@@ -44,7 +43,7 @@ logger = logging.getLogger(__name__)
|
|
|
44
43
|
|
|
45
44
|
|
|
46
45
|
# -----------------------------------------------------------------------------
|
|
47
|
-
async def open_android_emulator_transport(spec:
|
|
46
|
+
async def open_android_emulator_transport(spec: str | None) -> Transport:
|
|
48
47
|
'''
|
|
49
48
|
Open a transport connection to an Android emulator via its gRPC interface.
|
|
50
49
|
The parameter string has this syntax:
|
|
@@ -89,7 +88,7 @@ async def open_android_emulator_transport(spec: Optional[str]) -> Transport:
|
|
|
89
88
|
logger.debug('connecting to gRPC server at %s', server_address)
|
|
90
89
|
channel = grpc.aio.insecure_channel(server_address)
|
|
91
90
|
|
|
92
|
-
service:
|
|
91
|
+
service: EmulatedBluetoothServiceStub | VhciForwardingServiceStub
|
|
93
92
|
if mode == 'host':
|
|
94
93
|
# Connect as a host
|
|
95
94
|
service = EmulatedBluetoothServiceStub(channel)
|
|
@@ -22,7 +22,6 @@ import os
|
|
|
22
22
|
import pathlib
|
|
23
23
|
import platform
|
|
24
24
|
import sys
|
|
25
|
-
from typing import Optional
|
|
26
25
|
|
|
27
26
|
import grpc.aio
|
|
28
27
|
|
|
@@ -66,7 +65,7 @@ DEFAULT_VARIANT = ''
|
|
|
66
65
|
|
|
67
66
|
|
|
68
67
|
# -----------------------------------------------------------------------------
|
|
69
|
-
def get_ini_dir() ->
|
|
68
|
+
def get_ini_dir() -> pathlib.Path | None:
|
|
70
69
|
if sys.platform == 'darwin':
|
|
71
70
|
if tmpdir := os.getenv('TMPDIR', None):
|
|
72
71
|
return pathlib.Path(tmpdir)
|
|
@@ -100,7 +99,7 @@ def find_grpc_port(instance_number: int) -> int:
|
|
|
100
99
|
ini_file = ini_dir / ini_file_name(instance_number)
|
|
101
100
|
logger.debug(f'Looking for .ini file at {ini_file}')
|
|
102
101
|
if ini_file.is_file():
|
|
103
|
-
with open(ini_file
|
|
102
|
+
with open(ini_file) as ini_file_data:
|
|
104
103
|
for line in ini_file_data.readlines():
|
|
105
104
|
if '=' in line:
|
|
106
105
|
key, value = line.split('=')
|
|
@@ -146,7 +145,7 @@ def publish_grpc_port(grpc_port: int, instance_number: int) -> bool:
|
|
|
146
145
|
|
|
147
146
|
# -----------------------------------------------------------------------------
|
|
148
147
|
async def open_android_netsim_controller_transport(
|
|
149
|
-
server_host:
|
|
148
|
+
server_host: str | None, server_port: int, options: dict[str, str]
|
|
150
149
|
) -> Transport:
|
|
151
150
|
if server_host == '_' or not server_host:
|
|
152
151
|
server_host = 'localhost'
|
|
@@ -156,21 +155,26 @@ async def open_android_netsim_controller_transport(
|
|
|
156
155
|
logger.warning("unable to publish gRPC port")
|
|
157
156
|
|
|
158
157
|
class HciDevice:
|
|
159
|
-
def __init__(self, context,
|
|
158
|
+
def __init__(self, context, server):
|
|
160
159
|
self.context = context
|
|
161
|
-
self.
|
|
160
|
+
self.server = server
|
|
162
161
|
self.name = None
|
|
162
|
+
self.sink = None
|
|
163
163
|
self.loop = asyncio.get_running_loop()
|
|
164
164
|
self.done = self.loop.create_future()
|
|
165
|
-
self.task = self.loop.create_task(self.pump())
|
|
166
165
|
|
|
167
166
|
async def pump(self):
|
|
168
167
|
try:
|
|
169
168
|
await self.pump_loop()
|
|
170
169
|
except asyncio.CancelledError:
|
|
171
170
|
logger.debug('Pump task canceled')
|
|
172
|
-
|
|
173
|
-
|
|
171
|
+
finally:
|
|
172
|
+
if self.sink:
|
|
173
|
+
logger.debug('Releasing sink')
|
|
174
|
+
self.server.release_sink()
|
|
175
|
+
self.sink = None
|
|
176
|
+
|
|
177
|
+
logger.debug('Pump task terminated')
|
|
174
178
|
|
|
175
179
|
async def pump_loop(self):
|
|
176
180
|
while True:
|
|
@@ -186,15 +190,26 @@ async def open_android_netsim_controller_transport(
|
|
|
186
190
|
if request.WhichOneof('request_type') == 'initial_info':
|
|
187
191
|
logger.debug(f'Received initial info: {request}')
|
|
188
192
|
|
|
193
|
+
self.name = request.initial_info.name
|
|
194
|
+
|
|
189
195
|
# We only accept BLUETOOTH
|
|
190
196
|
if request.initial_info.chip.kind != ChipKind.BLUETOOTH:
|
|
191
197
|
logger.warning('Unsupported chip type')
|
|
192
198
|
error = PacketResponse(error='Unsupported chip type')
|
|
193
199
|
await self.context.write(error)
|
|
194
|
-
return
|
|
200
|
+
# return
|
|
201
|
+
continue
|
|
202
|
+
|
|
203
|
+
# Lease the sink so that no other device can send
|
|
204
|
+
self.sink = self.server.lease_sink(self)
|
|
205
|
+
if self.sink is None:
|
|
206
|
+
logger.warning('Another device is already connected')
|
|
207
|
+
error = PacketResponse(error='Device busy')
|
|
208
|
+
await self.context.write(error)
|
|
209
|
+
# return
|
|
210
|
+
continue
|
|
195
211
|
|
|
196
|
-
|
|
197
|
-
continue
|
|
212
|
+
continue
|
|
198
213
|
|
|
199
214
|
# Expect a data packet
|
|
200
215
|
request_type = request.WhichOneof('request_type')
|
|
@@ -205,10 +220,10 @@ async def open_android_netsim_controller_transport(
|
|
|
205
220
|
continue
|
|
206
221
|
|
|
207
222
|
# Process the packet
|
|
208
|
-
|
|
223
|
+
assert self.sink is not None
|
|
224
|
+
self.sink(
|
|
209
225
|
bytes([request.hci_packet.packet_type]) + request.hci_packet.packet
|
|
210
226
|
)
|
|
211
|
-
self.on_data_received(data)
|
|
212
227
|
|
|
213
228
|
async def send_packet(self, data):
|
|
214
229
|
return await self.context.write(
|
|
@@ -217,12 +232,6 @@ async def open_android_netsim_controller_transport(
|
|
|
217
232
|
)
|
|
218
233
|
)
|
|
219
234
|
|
|
220
|
-
def terminate(self):
|
|
221
|
-
self.task.cancel()
|
|
222
|
-
|
|
223
|
-
async def wait_for_termination(self):
|
|
224
|
-
await self.done
|
|
225
|
-
|
|
226
235
|
server_address = f'{server_host}:{server_port}'
|
|
227
236
|
|
|
228
237
|
class Server(PacketStreamerServicer, ParserSource):
|
|
@@ -258,27 +267,27 @@ async def open_android_netsim_controller_transport(
|
|
|
258
267
|
|
|
259
268
|
return await self.device.send_packet(packet)
|
|
260
269
|
|
|
261
|
-
|
|
262
|
-
logger.debug('StreamPackets request')
|
|
263
|
-
|
|
264
|
-
# Check that we don't already have a device
|
|
270
|
+
def lease_sink(self, device):
|
|
265
271
|
if self.device:
|
|
266
|
-
|
|
267
|
-
|
|
272
|
+
return None
|
|
273
|
+
self.device = device
|
|
274
|
+
return self.parser.feed_data
|
|
275
|
+
|
|
276
|
+
def release_sink(self):
|
|
277
|
+
self.device = None
|
|
278
|
+
|
|
279
|
+
async def StreamPackets(self, request_iterator, context):
|
|
280
|
+
logger.debug('StreamPackets request')
|
|
268
281
|
|
|
269
282
|
# Instantiate a new device
|
|
270
|
-
|
|
283
|
+
device = HciDevice(context, self)
|
|
271
284
|
|
|
272
|
-
#
|
|
273
|
-
logger.debug('
|
|
285
|
+
# Pump packets to/from the device
|
|
286
|
+
logger.debug('Pumping device packets')
|
|
274
287
|
try:
|
|
275
|
-
await
|
|
276
|
-
|
|
277
|
-
logger.debug('
|
|
278
|
-
self.device.terminate()
|
|
279
|
-
|
|
280
|
-
logger.debug('Device terminated')
|
|
281
|
-
self.device = None
|
|
288
|
+
await device.pump()
|
|
289
|
+
finally:
|
|
290
|
+
logger.debug('Pump terminated')
|
|
282
291
|
|
|
283
292
|
server = Server()
|
|
284
293
|
await server.start()
|
|
@@ -291,9 +300,9 @@ async def open_android_netsim_controller_transport(
|
|
|
291
300
|
|
|
292
301
|
# -----------------------------------------------------------------------------
|
|
293
302
|
async def open_android_netsim_host_transport_with_address(
|
|
294
|
-
server_host:
|
|
303
|
+
server_host: str | None,
|
|
295
304
|
server_port: int,
|
|
296
|
-
options:
|
|
305
|
+
options: dict[str, str] | None = None,
|
|
297
306
|
):
|
|
298
307
|
if server_host == '_' or not server_host:
|
|
299
308
|
server_host = 'localhost'
|
|
@@ -318,7 +327,7 @@ async def open_android_netsim_host_transport_with_address(
|
|
|
318
327
|
|
|
319
328
|
# -----------------------------------------------------------------------------
|
|
320
329
|
async def open_android_netsim_host_transport_with_channel(
|
|
321
|
-
channel, options:
|
|
330
|
+
channel, options: dict[str, str] | None = None
|
|
322
331
|
):
|
|
323
332
|
# Wrapper for I/O operations
|
|
324
333
|
class HciDevice:
|
|
@@ -398,7 +407,7 @@ async def open_android_netsim_host_transport_with_channel(
|
|
|
398
407
|
|
|
399
408
|
|
|
400
409
|
# -----------------------------------------------------------------------------
|
|
401
|
-
async def open_android_netsim_transport(spec:
|
|
410
|
+
async def open_android_netsim_transport(spec: str | None) -> Transport:
|
|
402
411
|
'''
|
|
403
412
|
Open a transport connection as a client or server, implementing Android's `netsim`
|
|
404
413
|
simulator protocol over gRPC.
|
bumble/transport/common.py
CHANGED
|
@@ -23,7 +23,7 @@ import io
|
|
|
23
23
|
import logging
|
|
24
24
|
import struct
|
|
25
25
|
from collections.abc import Awaitable, Callable
|
|
26
|
-
from typing import Any,
|
|
26
|
+
from typing import Any, Protocol
|
|
27
27
|
|
|
28
28
|
from bumble import core, hci
|
|
29
29
|
from bumble.colors import color
|
|
@@ -107,11 +107,11 @@ class PacketParser:
|
|
|
107
107
|
NEED_LENGTH = 1
|
|
108
108
|
NEED_BODY = 2
|
|
109
109
|
|
|
110
|
-
sink:
|
|
110
|
+
sink: TransportSink | None
|
|
111
111
|
extended_packet_info: dict[int, tuple[int, int, str]]
|
|
112
|
-
packet_info:
|
|
112
|
+
packet_info: tuple[int, int, str] | None = None
|
|
113
113
|
|
|
114
|
-
def __init__(self, sink:
|
|
114
|
+
def __init__(self, sink: TransportSink | None = None) -> None:
|
|
115
115
|
self.sink = sink
|
|
116
116
|
self.extended_packet_info = {}
|
|
117
117
|
self.reset()
|
|
@@ -176,7 +176,7 @@ class PacketReader:
|
|
|
176
176
|
self.source = source
|
|
177
177
|
self.at_end = False
|
|
178
178
|
|
|
179
|
-
def next_packet(self) ->
|
|
179
|
+
def next_packet(self) -> bytes | None:
|
|
180
180
|
# Get the packet type
|
|
181
181
|
packet_type = self.source.read(1)
|
|
182
182
|
if len(packet_type) != 1:
|
|
@@ -253,7 +253,7 @@ class BaseSource:
|
|
|
253
253
|
"""
|
|
254
254
|
|
|
255
255
|
terminated: asyncio.Future[None]
|
|
256
|
-
sink:
|
|
256
|
+
sink: TransportSink | None
|
|
257
257
|
|
|
258
258
|
def __init__(self) -> None:
|
|
259
259
|
self.terminated = asyncio.get_running_loop().create_future()
|
|
@@ -357,7 +357,7 @@ class Transport:
|
|
|
357
357
|
|
|
358
358
|
# -----------------------------------------------------------------------------
|
|
359
359
|
class PumpedPacketSource(ParserSource):
|
|
360
|
-
pump_task:
|
|
360
|
+
pump_task: asyncio.Task[None] | None
|
|
361
361
|
|
|
362
362
|
def __init__(self, receive) -> None:
|
|
363
363
|
super().__init__()
|
|
@@ -390,7 +390,7 @@ class PumpedPacketSource(ParserSource):
|
|
|
390
390
|
|
|
391
391
|
# -----------------------------------------------------------------------------
|
|
392
392
|
class PumpedPacketSink:
|
|
393
|
-
pump_task:
|
|
393
|
+
pump_task: asyncio.Task[None] | None
|
|
394
394
|
|
|
395
395
|
def __init__(self, send: Callable[[bytes], Awaitable[Any]]):
|
|
396
396
|
self.send_function = send
|
|
@@ -443,7 +443,7 @@ class SnoopingTransport(Transport):
|
|
|
443
443
|
|
|
444
444
|
@staticmethod
|
|
445
445
|
def create_with(
|
|
446
|
-
transport: Transport, snooper:
|
|
446
|
+
transport: Transport, snooper: contextlib.AbstractContextManager[Snooper]
|
|
447
447
|
) -> SnoopingTransport:
|
|
448
448
|
"""
|
|
449
449
|
Create an instance given a snooper that works as as context manager.
|
bumble/transport/file.py
CHANGED
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
# Imports
|
|
17
17
|
# -----------------------------------------------------------------------------
|
|
18
18
|
import asyncio
|
|
19
|
-
import io
|
|
20
19
|
import logging
|
|
21
20
|
|
|
22
21
|
from bumble.transport.common import StreamPacketSink, StreamPacketSource, Transport
|
|
@@ -36,7 +35,7 @@ async def open_file_transport(spec: str) -> Transport:
|
|
|
36
35
|
'''
|
|
37
36
|
|
|
38
37
|
# Open the file
|
|
39
|
-
file =
|
|
38
|
+
file = open(spec, 'r+b', buffering=0)
|
|
40
39
|
|
|
41
40
|
# Setup reading
|
|
42
41
|
read_transport, packet_source = await asyncio.get_running_loop().connect_read_pipe(
|
bumble/transport/hci_socket.py
CHANGED
|
@@ -22,7 +22,6 @@ import logging
|
|
|
22
22
|
import os
|
|
23
23
|
import socket
|
|
24
24
|
import struct
|
|
25
|
-
from typing import Optional
|
|
26
25
|
|
|
27
26
|
from bumble.transport.common import ParserSource, Transport
|
|
28
27
|
|
|
@@ -33,7 +32,7 @@ logger = logging.getLogger(__name__)
|
|
|
33
32
|
|
|
34
33
|
|
|
35
34
|
# -----------------------------------------------------------------------------
|
|
36
|
-
async def open_hci_socket_transport(spec:
|
|
35
|
+
async def open_hci_socket_transport(spec: str | None) -> Transport:
|
|
37
36
|
'''
|
|
38
37
|
Open an HCI Socket (only available on some platforms).
|
|
39
38
|
The parameter string is either empty (to use the first/default Bluetooth adapter)
|
|
@@ -87,7 +86,7 @@ async def open_hci_socket_transport(spec: Optional[str]) -> Transport:
|
|
|
87
86
|
)
|
|
88
87
|
!= 0
|
|
89
88
|
):
|
|
90
|
-
raise
|
|
89
|
+
raise OSError(ctypes.get_errno(), os.strerror(ctypes.get_errno()))
|
|
91
90
|
|
|
92
91
|
class HciSocketSource(ParserSource):
|
|
93
92
|
def __init__(self, hci_socket):
|
bumble/transport/pty.py
CHANGED
|
@@ -17,12 +17,10 @@
|
|
|
17
17
|
# -----------------------------------------------------------------------------
|
|
18
18
|
import asyncio
|
|
19
19
|
import atexit
|
|
20
|
-
import io
|
|
21
20
|
import logging
|
|
22
21
|
import os
|
|
23
22
|
import pty
|
|
24
23
|
import tty
|
|
25
|
-
from typing import Optional
|
|
26
24
|
|
|
27
25
|
from bumble.transport.common import StreamPacketSink, StreamPacketSource, Transport
|
|
28
26
|
|
|
@@ -33,7 +31,7 @@ logger = logging.getLogger(__name__)
|
|
|
33
31
|
|
|
34
32
|
|
|
35
33
|
# -----------------------------------------------------------------------------
|
|
36
|
-
async def open_pty_transport(spec:
|
|
34
|
+
async def open_pty_transport(spec: str | None) -> Transport:
|
|
37
35
|
'''
|
|
38
36
|
Open a PTY transport.
|
|
39
37
|
The parameter string may be empty, or a path name where a symbolic link
|
|
@@ -48,11 +46,11 @@ async def open_pty_transport(spec: Optional[str]) -> Transport:
|
|
|
48
46
|
tty.setraw(replica)
|
|
49
47
|
|
|
50
48
|
read_transport, packet_source = await asyncio.get_running_loop().connect_read_pipe(
|
|
51
|
-
StreamPacketSource,
|
|
49
|
+
StreamPacketSource, open(primary, 'rb', closefd=False)
|
|
52
50
|
)
|
|
53
51
|
|
|
54
52
|
write_transport, _ = await asyncio.get_running_loop().connect_write_pipe(
|
|
55
|
-
asyncio.BaseProtocol,
|
|
53
|
+
asyncio.BaseProtocol, open(primary, 'wb', closefd=False)
|
|
56
54
|
)
|
|
57
55
|
packet_sink = StreamPacketSink(write_transport)
|
|
58
56
|
|
bumble/transport/pyusb.py
CHANGED
|
@@ -19,7 +19,6 @@ import asyncio
|
|
|
19
19
|
import logging
|
|
20
20
|
import threading
|
|
21
21
|
import time
|
|
22
|
-
from typing import Optional
|
|
23
22
|
|
|
24
23
|
import usb.core
|
|
25
24
|
import usb.util
|
|
@@ -284,7 +283,9 @@ async def open_pyusb_transport(spec: str) -> Transport:
|
|
|
284
283
|
device = await _power_cycle(device) # type: ignore
|
|
285
284
|
except Exception as e:
|
|
286
285
|
logging.debug(e, stack_info=True)
|
|
287
|
-
logging.info(
|
|
286
|
+
logging.info(
|
|
287
|
+
f"Unable to power cycle {hex(device.idVendor)} {hex(device.idProduct)}"
|
|
288
|
+
) # type: ignore
|
|
288
289
|
|
|
289
290
|
# Collect the metadata
|
|
290
291
|
device_metadata = {'vendor_id': device.idVendor, 'product_id': device.idProduct}
|
|
@@ -370,7 +371,9 @@ async def _power_cycle(device: UsbDevice) -> UsbDevice:
|
|
|
370
371
|
# Device needs to be find again otherwise it will appear as disconnected
|
|
371
372
|
return usb.core.find(idVendor=device.idVendor, idProduct=device.idProduct) # type: ignore
|
|
372
373
|
except USBError:
|
|
373
|
-
logger.exception(
|
|
374
|
+
logger.exception(
|
|
375
|
+
f"Adjustment needed: Please revise the udev rule for device {hex(device.idVendor)}:{hex(device.idProduct)} for proper recognition."
|
|
376
|
+
) # type: ignore
|
|
374
377
|
|
|
375
378
|
return device
|
|
376
379
|
|
|
@@ -385,7 +388,7 @@ def _set_port_status(device: UsbDevice, port: int, on: bool):
|
|
|
385
388
|
)
|
|
386
389
|
|
|
387
390
|
|
|
388
|
-
def _find_device_by_path(sys_path: str) ->
|
|
391
|
+
def _find_device_by_path(sys_path: str) -> UsbDevice | None:
|
|
389
392
|
"""Finds a USB device based on its system path."""
|
|
390
393
|
bus_num, *port_parts = sys_path.split('-')
|
|
391
394
|
ports = [int(port) for port in port_parts[0].split('.')]
|
|
@@ -398,7 +401,7 @@ def _find_device_by_path(sys_path: str) -> Optional[UsbDevice]:
|
|
|
398
401
|
return None
|
|
399
402
|
|
|
400
403
|
|
|
401
|
-
def _find_hub_by_device_path(sys_path: str) ->
|
|
404
|
+
def _find_hub_by_device_path(sys_path: str) -> UsbDevice | None:
|
|
402
405
|
"""Finds the USB hub associated with a specific device path."""
|
|
403
406
|
hub_sys_path = sys_path.rsplit('.', 1)[0]
|
|
404
407
|
hub_device = _find_device_by_path(hub_sys_path)
|