bumble 0.0.195__py3-none-any.whl → 0.0.199__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 +351 -66
- bumble/apps/console.py +5 -20
- bumble/apps/device_info.py +230 -0
- bumble/apps/gatt_dump.py +4 -0
- bumble/apps/lea_unicast/app.py +16 -17
- bumble/apps/pair.py +32 -5
- bumble/at.py +12 -6
- bumble/att.py +56 -40
- bumble/avc.py +8 -5
- bumble/avctp.py +3 -2
- bumble/avdtp.py +7 -3
- bumble/avrcp.py +2 -1
- bumble/codecs.py +17 -13
- bumble/colors.py +6 -2
- bumble/core.py +37 -7
- bumble/decoder.py +14 -10
- bumble/device.py +382 -111
- bumble/drivers/rtk.py +32 -13
- bumble/gatt.py +30 -20
- bumble/gatt_client.py +15 -29
- bumble/gatt_server.py +14 -6
- bumble/hci.py +322 -32
- bumble/hid.py +24 -28
- bumble/host.py +20 -6
- bumble/l2cap.py +24 -17
- bumble/link.py +8 -3
- bumble/pandora/__init__.py +3 -0
- bumble/pandora/l2cap.py +310 -0
- bumble/profiles/aics.py +520 -0
- bumble/profiles/ascs.py +739 -0
- bumble/profiles/asha.py +295 -0
- bumble/profiles/bap.py +1 -874
- bumble/profiles/bass.py +440 -0
- bumble/profiles/csip.py +4 -4
- bumble/profiles/gap.py +110 -0
- bumble/profiles/hap.py +665 -0
- bumble/profiles/heart_rate_service.py +4 -3
- bumble/profiles/le_audio.py +43 -9
- bumble/profiles/mcp.py +448 -0
- bumble/profiles/pacs.py +210 -0
- bumble/profiles/tmap.py +89 -0
- bumble/profiles/vcp.py +5 -3
- bumble/rfcomm.py +4 -2
- bumble/sdp.py +13 -11
- bumble/smp.py +43 -12
- bumble/snoop.py +5 -4
- bumble/transport/__init__.py +8 -2
- bumble/transport/android_emulator.py +9 -3
- bumble/transport/android_netsim.py +9 -7
- bumble/transport/common.py +46 -18
- bumble/transport/pyusb.py +21 -4
- bumble/transport/unix.py +56 -0
- bumble/transport/usb.py +57 -46
- {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/METADATA +41 -41
- {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/RECORD +60 -49
- {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/WHEEL +1 -1
- bumble/profiles/asha_service.py +0 -193
- {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/LICENSE +0 -0
- {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/entry_points.txt +0 -0
- {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/top_level.txt +0 -0
|
@@ -31,6 +31,8 @@ from .common import (
|
|
|
31
31
|
PumpedPacketSource,
|
|
32
32
|
PumpedPacketSink,
|
|
33
33
|
Transport,
|
|
34
|
+
TransportSpecError,
|
|
35
|
+
TransportInitError,
|
|
34
36
|
)
|
|
35
37
|
|
|
36
38
|
# pylint: disable=no-name-in-module
|
|
@@ -135,7 +137,7 @@ async def open_android_netsim_controller_transport(
|
|
|
135
137
|
server_host: Optional[str], server_port: int, options: Dict[str, str]
|
|
136
138
|
) -> Transport:
|
|
137
139
|
if not server_port:
|
|
138
|
-
raise
|
|
140
|
+
raise TransportSpecError('invalid port')
|
|
139
141
|
if server_host == '_' or not server_host:
|
|
140
142
|
server_host = 'localhost'
|
|
141
143
|
|
|
@@ -288,7 +290,7 @@ async def open_android_netsim_host_transport_with_address(
|
|
|
288
290
|
instance_number = 0 if options is None else int(options.get('instance', '0'))
|
|
289
291
|
server_port = find_grpc_port(instance_number)
|
|
290
292
|
if not server_port:
|
|
291
|
-
raise
|
|
293
|
+
raise TransportInitError('gRPC server port not found')
|
|
292
294
|
|
|
293
295
|
# Connect to the gRPC server
|
|
294
296
|
server_address = f'{server_host}:{server_port}'
|
|
@@ -326,7 +328,7 @@ async def open_android_netsim_host_transport_with_channel(
|
|
|
326
328
|
|
|
327
329
|
if response_type == 'error':
|
|
328
330
|
logger.warning(f'received error: {response.error}')
|
|
329
|
-
raise
|
|
331
|
+
raise TransportInitError(response.error)
|
|
330
332
|
|
|
331
333
|
if response_type == 'hci_packet':
|
|
332
334
|
return (
|
|
@@ -334,7 +336,7 @@ async def open_android_netsim_host_transport_with_channel(
|
|
|
334
336
|
+ response.hci_packet.packet
|
|
335
337
|
)
|
|
336
338
|
|
|
337
|
-
raise
|
|
339
|
+
raise TransportSpecError('unsupported response type')
|
|
338
340
|
|
|
339
341
|
async def write(self, packet):
|
|
340
342
|
await self.hci_device.write(
|
|
@@ -429,7 +431,7 @@ async def open_android_netsim_transport(spec: Optional[str]) -> Transport:
|
|
|
429
431
|
options: Dict[str, str] = {}
|
|
430
432
|
for param in params[params_offset:]:
|
|
431
433
|
if '=' not in param:
|
|
432
|
-
raise
|
|
434
|
+
raise TransportSpecError('invalid parameter, expected <name>=<value>')
|
|
433
435
|
option_name, option_value = param.split('=')
|
|
434
436
|
options[option_name] = option_value
|
|
435
437
|
|
|
@@ -440,7 +442,7 @@ async def open_android_netsim_transport(spec: Optional[str]) -> Transport:
|
|
|
440
442
|
)
|
|
441
443
|
if mode == 'controller':
|
|
442
444
|
if host is None:
|
|
443
|
-
raise
|
|
445
|
+
raise TransportSpecError('<host>:<port> missing')
|
|
444
446
|
return await open_android_netsim_controller_transport(host, port, options)
|
|
445
447
|
|
|
446
|
-
raise
|
|
448
|
+
raise TransportSpecError('invalid mode option')
|
bumble/transport/common.py
CHANGED
|
@@ -23,6 +23,7 @@ import logging
|
|
|
23
23
|
import io
|
|
24
24
|
from typing import Any, ContextManager, Tuple, Optional, Protocol, Dict
|
|
25
25
|
|
|
26
|
+
from bumble import core
|
|
26
27
|
from bumble import hci
|
|
27
28
|
from bumble.colors import color
|
|
28
29
|
from bumble.snoop import Snooper
|
|
@@ -49,10 +50,16 @@ HCI_PACKET_INFO: Dict[int, Tuple[int, int, str]] = {
|
|
|
49
50
|
# -----------------------------------------------------------------------------
|
|
50
51
|
# Errors
|
|
51
52
|
# -----------------------------------------------------------------------------
|
|
52
|
-
class TransportLostError(
|
|
53
|
-
"""
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
class TransportLostError(core.BaseBumbleError, RuntimeError):
|
|
54
|
+
"""The Transport has been lost/disconnected."""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TransportInitError(core.BaseBumbleError, RuntimeError):
|
|
58
|
+
"""Error raised when the transport cannot be initialized."""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class TransportSpecError(core.BaseBumbleError, ValueError):
|
|
62
|
+
"""Error raised when the transport spec is invalid."""
|
|
56
63
|
|
|
57
64
|
|
|
58
65
|
# -----------------------------------------------------------------------------
|
|
@@ -132,7 +139,9 @@ class PacketParser:
|
|
|
132
139
|
packet_type
|
|
133
140
|
) or self.extended_packet_info.get(packet_type)
|
|
134
141
|
if self.packet_info is None:
|
|
135
|
-
raise
|
|
142
|
+
raise core.InvalidPacketError(
|
|
143
|
+
f'invalid packet type {packet_type}'
|
|
144
|
+
)
|
|
136
145
|
self.state = PacketParser.NEED_LENGTH
|
|
137
146
|
self.bytes_needed = self.packet_info[0] + self.packet_info[1]
|
|
138
147
|
elif self.state == PacketParser.NEED_LENGTH:
|
|
@@ -178,19 +187,19 @@ class PacketReader:
|
|
|
178
187
|
# Get the packet info based on its type
|
|
179
188
|
packet_info = HCI_PACKET_INFO.get(packet_type[0])
|
|
180
189
|
if packet_info is None:
|
|
181
|
-
raise
|
|
190
|
+
raise core.InvalidPacketError(f'invalid packet type {packet_type[0]} found')
|
|
182
191
|
|
|
183
192
|
# Read the header (that includes the length)
|
|
184
193
|
header_size = packet_info[0] + packet_info[1]
|
|
185
194
|
header = self.source.read(header_size)
|
|
186
195
|
if len(header) != header_size:
|
|
187
|
-
raise
|
|
196
|
+
raise core.InvalidPacketError('packet too short')
|
|
188
197
|
|
|
189
198
|
# Read the body
|
|
190
199
|
body_length = struct.unpack_from(packet_info[2], header, packet_info[1])[0]
|
|
191
200
|
body = self.source.read(body_length)
|
|
192
201
|
if len(body) != body_length:
|
|
193
|
-
raise
|
|
202
|
+
raise core.InvalidPacketError('packet too short')
|
|
194
203
|
|
|
195
204
|
return packet_type + header + body
|
|
196
205
|
|
|
@@ -211,7 +220,7 @@ class AsyncPacketReader:
|
|
|
211
220
|
# Get the packet info based on its type
|
|
212
221
|
packet_info = HCI_PACKET_INFO.get(packet_type[0])
|
|
213
222
|
if packet_info is None:
|
|
214
|
-
raise
|
|
223
|
+
raise core.InvalidPacketError(f'invalid packet type {packet_type[0]} found')
|
|
215
224
|
|
|
216
225
|
# Read the header (that includes the length)
|
|
217
226
|
header_size = packet_info[0] + packet_info[1]
|
|
@@ -239,26 +248,28 @@ class AsyncPipeSink:
|
|
|
239
248
|
|
|
240
249
|
|
|
241
250
|
# -----------------------------------------------------------------------------
|
|
242
|
-
class
|
|
251
|
+
class BaseSource:
|
|
243
252
|
"""
|
|
244
253
|
Base class designed to be subclassed by transport-specific source classes
|
|
245
254
|
"""
|
|
246
255
|
|
|
247
256
|
terminated: asyncio.Future[None]
|
|
248
|
-
|
|
257
|
+
sink: Optional[TransportSink]
|
|
249
258
|
|
|
250
259
|
def __init__(self) -> None:
|
|
251
|
-
self.parser = PacketParser()
|
|
252
260
|
self.terminated = asyncio.get_running_loop().create_future()
|
|
261
|
+
self.sink = None
|
|
253
262
|
|
|
254
263
|
def set_packet_sink(self, sink: TransportSink) -> None:
|
|
255
|
-
self.
|
|
264
|
+
self.sink = sink
|
|
256
265
|
|
|
257
266
|
def on_transport_lost(self) -> None:
|
|
258
|
-
self.terminated.
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
267
|
+
if not self.terminated.done():
|
|
268
|
+
self.terminated.set_result(None)
|
|
269
|
+
|
|
270
|
+
if self.sink:
|
|
271
|
+
if hasattr(self.sink, 'on_transport_lost'):
|
|
272
|
+
self.sink.on_transport_lost()
|
|
262
273
|
|
|
263
274
|
async def wait_for_termination(self) -> None:
|
|
264
275
|
"""
|
|
@@ -271,6 +282,23 @@ class ParserSource:
|
|
|
271
282
|
pass
|
|
272
283
|
|
|
273
284
|
|
|
285
|
+
# -----------------------------------------------------------------------------
|
|
286
|
+
class ParserSource(BaseSource):
|
|
287
|
+
"""
|
|
288
|
+
Base class for sources that use an HCI parser.
|
|
289
|
+
"""
|
|
290
|
+
|
|
291
|
+
parser: PacketParser
|
|
292
|
+
|
|
293
|
+
def __init__(self) -> None:
|
|
294
|
+
super().__init__()
|
|
295
|
+
self.parser = PacketParser()
|
|
296
|
+
|
|
297
|
+
def set_packet_sink(self, sink: TransportSink) -> None:
|
|
298
|
+
super().set_packet_sink(sink)
|
|
299
|
+
self.parser.set_packet_sink(sink)
|
|
300
|
+
|
|
301
|
+
|
|
274
302
|
# -----------------------------------------------------------------------------
|
|
275
303
|
class StreamPacketSource(asyncio.Protocol, ParserSource):
|
|
276
304
|
def data_received(self, data: bytes) -> None:
|
|
@@ -420,7 +448,7 @@ class SnoopingTransport(Transport):
|
|
|
420
448
|
return SnoopingTransport(
|
|
421
449
|
transport, exit_stack.enter_context(snooper), exit_stack.pop_all().close
|
|
422
450
|
)
|
|
423
|
-
raise
|
|
451
|
+
raise core.UnreachableError() # Satisfy the type checker
|
|
424
452
|
|
|
425
453
|
class Source:
|
|
426
454
|
sink: TransportSink
|
bumble/transport/pyusb.py
CHANGED
|
@@ -23,13 +23,13 @@ import time
|
|
|
23
23
|
import usb.core
|
|
24
24
|
import usb.util
|
|
25
25
|
|
|
26
|
-
from typing import Optional
|
|
26
|
+
from typing import Optional, Set
|
|
27
27
|
from usb.core import Device as UsbDevice
|
|
28
28
|
from usb.core import USBError
|
|
29
29
|
from usb.util import CTRL_TYPE_CLASS, CTRL_RECIPIENT_OTHER
|
|
30
30
|
from usb.legacy import REQ_SET_FEATURE, REQ_CLEAR_FEATURE, CLASS_HUB
|
|
31
31
|
|
|
32
|
-
from .common import Transport, ParserSource
|
|
32
|
+
from .common import Transport, ParserSource, TransportInitError
|
|
33
33
|
from .. import hci
|
|
34
34
|
from ..colors import color
|
|
35
35
|
|
|
@@ -46,6 +46,11 @@ RESET_DELAY = 3
|
|
|
46
46
|
# -----------------------------------------------------------------------------
|
|
47
47
|
logger = logging.getLogger(__name__)
|
|
48
48
|
|
|
49
|
+
# -----------------------------------------------------------------------------
|
|
50
|
+
# Global
|
|
51
|
+
# -----------------------------------------------------------------------------
|
|
52
|
+
devices_in_use: Set[int] = set()
|
|
53
|
+
|
|
49
54
|
|
|
50
55
|
# -----------------------------------------------------------------------------
|
|
51
56
|
async def open_pyusb_transport(spec: str) -> Transport:
|
|
@@ -216,6 +221,7 @@ async def open_pyusb_transport(spec: str) -> Transport:
|
|
|
216
221
|
async def close(self):
|
|
217
222
|
await self.source.stop()
|
|
218
223
|
await self.sink.stop()
|
|
224
|
+
devices_in_use.remove(device.address)
|
|
219
225
|
usb.util.release_interface(self.device, 0)
|
|
220
226
|
|
|
221
227
|
usb_find = usb.core.find
|
|
@@ -233,7 +239,18 @@ async def open_pyusb_transport(spec: str) -> Transport:
|
|
|
233
239
|
spec = spec[1:]
|
|
234
240
|
if ':' in spec:
|
|
235
241
|
vendor_id, product_id = spec.split(':')
|
|
236
|
-
device =
|
|
242
|
+
device = None
|
|
243
|
+
devices = usb_find(
|
|
244
|
+
find_all=True, idVendor=int(vendor_id, 16), idProduct=int(product_id, 16)
|
|
245
|
+
)
|
|
246
|
+
for d in devices:
|
|
247
|
+
if d.address in devices_in_use:
|
|
248
|
+
continue
|
|
249
|
+
device = d
|
|
250
|
+
devices_in_use.add(d.address)
|
|
251
|
+
break
|
|
252
|
+
if device is None:
|
|
253
|
+
raise ValueError('device already in use')
|
|
237
254
|
elif '-' in spec:
|
|
238
255
|
|
|
239
256
|
def device_path(device):
|
|
@@ -259,7 +276,7 @@ async def open_pyusb_transport(spec: str) -> Transport:
|
|
|
259
276
|
device = None
|
|
260
277
|
|
|
261
278
|
if device is None:
|
|
262
|
-
raise
|
|
279
|
+
raise TransportInitError('device not found')
|
|
263
280
|
logger.debug(f'USB Device: {device}')
|
|
264
281
|
|
|
265
282
|
# Power Cycle the device
|
bumble/transport/unix.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Copyright 2021-2024 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
# -----------------------------------------------------------------------------
|
|
16
|
+
# Imports
|
|
17
|
+
# -----------------------------------------------------------------------------
|
|
18
|
+
import asyncio
|
|
19
|
+
import logging
|
|
20
|
+
|
|
21
|
+
from .common import Transport, StreamPacketSource, StreamPacketSink
|
|
22
|
+
|
|
23
|
+
# -----------------------------------------------------------------------------
|
|
24
|
+
# Logging
|
|
25
|
+
# -----------------------------------------------------------------------------
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# -----------------------------------------------------------------------------
|
|
30
|
+
async def open_unix_client_transport(spec: str) -> Transport:
|
|
31
|
+
'''Open a UNIX socket client transport.
|
|
32
|
+
|
|
33
|
+
The parameter is the path of unix socket. For abstract socket, the first character
|
|
34
|
+
needs to be '@'.
|
|
35
|
+
|
|
36
|
+
Example:
|
|
37
|
+
* /tmp/hci.socket
|
|
38
|
+
* @hci_socket
|
|
39
|
+
'''
|
|
40
|
+
|
|
41
|
+
class UnixPacketSource(StreamPacketSource):
|
|
42
|
+
def connection_lost(self, exc):
|
|
43
|
+
logger.debug(f'connection lost: {exc}')
|
|
44
|
+
self.on_transport_lost()
|
|
45
|
+
|
|
46
|
+
# For abstract socket, the first character should be null character.
|
|
47
|
+
if spec.startswith('@'):
|
|
48
|
+
spec = '\0' + spec[1:]
|
|
49
|
+
|
|
50
|
+
(
|
|
51
|
+
unix_transport,
|
|
52
|
+
packet_source,
|
|
53
|
+
) = await asyncio.get_running_loop().create_unix_connection(UnixPacketSource, spec)
|
|
54
|
+
packet_sink = StreamPacketSink(unix_transport)
|
|
55
|
+
|
|
56
|
+
return Transport(packet_source, packet_sink)
|
bumble/transport/usb.py
CHANGED
|
@@ -15,19 +15,18 @@
|
|
|
15
15
|
# -----------------------------------------------------------------------------
|
|
16
16
|
# Imports
|
|
17
17
|
# -----------------------------------------------------------------------------
|
|
18
|
+
from __future__ import annotations
|
|
18
19
|
import asyncio
|
|
19
20
|
import logging
|
|
20
21
|
import threading
|
|
21
|
-
import collections
|
|
22
22
|
import ctypes
|
|
23
23
|
import platform
|
|
24
24
|
|
|
25
25
|
import usb1
|
|
26
26
|
|
|
27
|
-
from bumble.transport.common import Transport,
|
|
27
|
+
from bumble.transport.common import Transport, BaseSource, TransportInitError
|
|
28
28
|
from bumble import hci
|
|
29
29
|
from bumble.colors import color
|
|
30
|
-
from bumble.utils import AsyncRunner
|
|
31
30
|
|
|
32
31
|
|
|
33
32
|
# -----------------------------------------------------------------------------
|
|
@@ -115,13 +114,17 @@ async def open_usb_transport(spec: str) -> Transport:
|
|
|
115
114
|
self.device = device
|
|
116
115
|
self.acl_out = acl_out
|
|
117
116
|
self.acl_out_transfer = device.getTransfer()
|
|
118
|
-
self.
|
|
117
|
+
self.acl_out_transfer_ready = asyncio.Semaphore(1)
|
|
118
|
+
self.packets: asyncio.Queue[bytes] = (
|
|
119
|
+
asyncio.Queue()
|
|
120
|
+
) # Queue of packets waiting to be sent
|
|
119
121
|
self.loop = asyncio.get_running_loop()
|
|
122
|
+
self.queue_task = None
|
|
120
123
|
self.cancel_done = self.loop.create_future()
|
|
121
124
|
self.closed = False
|
|
122
125
|
|
|
123
126
|
def start(self):
|
|
124
|
-
|
|
127
|
+
self.queue_task = asyncio.create_task(self.process_queue())
|
|
125
128
|
|
|
126
129
|
def on_packet(self, packet):
|
|
127
130
|
# Ignore packets if we're closed
|
|
@@ -133,62 +136,64 @@ async def open_usb_transport(spec: str) -> Transport:
|
|
|
133
136
|
return
|
|
134
137
|
|
|
135
138
|
# Queue the packet
|
|
136
|
-
self.packets.
|
|
137
|
-
if len(self.packets) == 1:
|
|
138
|
-
# The queue was previously empty, re-prime the pump
|
|
139
|
-
self.process_queue()
|
|
139
|
+
self.packets.put_nowait(packet)
|
|
140
140
|
|
|
141
141
|
def transfer_callback(self, transfer):
|
|
142
|
+
self.loop.call_soon_threadsafe(self.acl_out_transfer_ready.release)
|
|
142
143
|
status = transfer.getStatus()
|
|
143
144
|
|
|
144
145
|
# pylint: disable=no-member
|
|
145
|
-
if status == usb1.
|
|
146
|
-
self.loop.call_soon_threadsafe(self.on_packet_sent)
|
|
147
|
-
elif status == usb1.TRANSFER_CANCELLED:
|
|
146
|
+
if status == usb1.TRANSFER_CANCELLED:
|
|
148
147
|
self.loop.call_soon_threadsafe(self.cancel_done.set_result, None)
|
|
149
|
-
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
if status != usb1.TRANSFER_COMPLETED:
|
|
150
151
|
logger.warning(
|
|
151
152
|
color(f'!!! OUT transfer not completed: status={status}', 'red')
|
|
152
153
|
)
|
|
153
154
|
|
|
154
|
-
def
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
self.
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
155
|
+
async def process_queue(self):
|
|
156
|
+
while True:
|
|
157
|
+
# Wait for a packet to transfer.
|
|
158
|
+
packet = await self.packets.get()
|
|
159
|
+
|
|
160
|
+
# Wait until we can start a transfer.
|
|
161
|
+
await self.acl_out_transfer_ready.acquire()
|
|
162
|
+
|
|
163
|
+
# Transfer the packet.
|
|
164
|
+
packet_type = packet[0]
|
|
165
|
+
if packet_type == hci.HCI_ACL_DATA_PACKET:
|
|
166
|
+
self.acl_out_transfer.setBulk(
|
|
167
|
+
self.acl_out, packet[1:], callback=self.transfer_callback
|
|
168
|
+
)
|
|
169
|
+
self.acl_out_transfer.submit()
|
|
170
|
+
elif packet_type == hci.HCI_COMMAND_PACKET:
|
|
171
|
+
self.acl_out_transfer.setControl(
|
|
172
|
+
USB_RECIPIENT_DEVICE | USB_REQUEST_TYPE_CLASS,
|
|
173
|
+
0,
|
|
174
|
+
0,
|
|
175
|
+
0,
|
|
176
|
+
packet[1:],
|
|
177
|
+
callback=self.transfer_callback,
|
|
178
|
+
)
|
|
179
|
+
self.acl_out_transfer.submit()
|
|
180
|
+
else:
|
|
181
|
+
logger.warning(
|
|
182
|
+
color(f'unsupported packet type {packet_type}', 'red')
|
|
183
|
+
)
|
|
182
184
|
|
|
183
185
|
def close(self):
|
|
184
186
|
self.closed = True
|
|
187
|
+
if self.queue_task:
|
|
188
|
+
self.queue_task.cancel()
|
|
185
189
|
|
|
186
190
|
async def terminate(self):
|
|
187
191
|
if not self.closed:
|
|
188
192
|
self.close()
|
|
189
193
|
|
|
190
194
|
# Empty the packet queue so that we don't send any more data
|
|
191
|
-
self.packets.
|
|
195
|
+
while not self.packets.empty():
|
|
196
|
+
self.packets.get_nowait()
|
|
192
197
|
|
|
193
198
|
# If we have a transfer in flight, cancel it
|
|
194
199
|
if self.acl_out_transfer.isSubmitted():
|
|
@@ -203,7 +208,7 @@ async def open_usb_transport(spec: str) -> Transport:
|
|
|
203
208
|
except usb1.USBError:
|
|
204
209
|
logger.debug('OUT transfer likely already completed')
|
|
205
210
|
|
|
206
|
-
class UsbPacketSource(asyncio.Protocol,
|
|
211
|
+
class UsbPacketSource(asyncio.Protocol, BaseSource):
|
|
207
212
|
def __init__(self, device, metadata, acl_in, events_in):
|
|
208
213
|
super().__init__()
|
|
209
214
|
self.device = device
|
|
@@ -280,7 +285,13 @@ async def open_usb_transport(spec: str) -> Transport:
|
|
|
280
285
|
packet = await self.queue.get()
|
|
281
286
|
except asyncio.CancelledError:
|
|
282
287
|
return
|
|
283
|
-
self.
|
|
288
|
+
if self.sink:
|
|
289
|
+
try:
|
|
290
|
+
self.sink.on_packet(packet)
|
|
291
|
+
except Exception as error:
|
|
292
|
+
logger.exception(
|
|
293
|
+
color(f'!!! Exception in sink.on_packet: {error}', 'red')
|
|
294
|
+
)
|
|
284
295
|
|
|
285
296
|
def close(self):
|
|
286
297
|
self.closed = True
|
|
@@ -442,7 +453,7 @@ async def open_usb_transport(spec: str) -> Transport:
|
|
|
442
453
|
|
|
443
454
|
if found is None:
|
|
444
455
|
context.close()
|
|
445
|
-
raise
|
|
456
|
+
raise TransportInitError('device not found')
|
|
446
457
|
|
|
447
458
|
logger.debug(f'USB Device: {found}')
|
|
448
459
|
|
|
@@ -507,7 +518,7 @@ async def open_usb_transport(spec: str) -> Transport:
|
|
|
507
518
|
|
|
508
519
|
endpoints = find_endpoints(found)
|
|
509
520
|
if endpoints is None:
|
|
510
|
-
raise
|
|
521
|
+
raise TransportInitError('no compatible interface found for device')
|
|
511
522
|
(configuration, interface, setting, acl_in, acl_out, events_in) = endpoints
|
|
512
523
|
logger.debug(
|
|
513
524
|
f'selected endpoints: configuration={configuration}, '
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: bumble
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.199
|
|
4
4
|
Summary: Bluetooth Stack for Apps, Emulation, Test and Experimentation
|
|
5
5
|
Home-page: https://github.com/google/bumble
|
|
6
6
|
Author: Google
|
|
@@ -8,52 +8,52 @@ Author-email: tbd@tbd.com
|
|
|
8
8
|
Requires-Python: >=3.8
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
10
|
License-File: LICENSE
|
|
11
|
-
Requires-Dist: pyee
|
|
12
|
-
Requires-Dist: aiohttp
|
|
13
|
-
Requires-Dist: appdirs
|
|
14
|
-
Requires-Dist: click
|
|
15
|
-
Requires-Dist: cryptography
|
|
16
|
-
Requires-Dist: grpcio
|
|
17
|
-
Requires-Dist: humanize
|
|
18
|
-
Requires-Dist: libusb1
|
|
19
|
-
Requires-Dist: libusb-package
|
|
20
|
-
Requires-Dist: platformdirs
|
|
21
|
-
Requires-Dist: prompt-toolkit
|
|
22
|
-
Requires-Dist: prettytable
|
|
23
|
-
Requires-Dist: protobuf
|
|
24
|
-
Requires-Dist: pyserial-asyncio
|
|
25
|
-
Requires-Dist: pyserial
|
|
26
|
-
Requires-Dist: pyusb
|
|
27
|
-
Requires-Dist: websockets
|
|
28
|
-
Requires-Dist: cryptography
|
|
11
|
+
Requires-Dist: pyee>=8.2.2
|
|
12
|
+
Requires-Dist: aiohttp~=3.8; platform_system != "Emscripten"
|
|
13
|
+
Requires-Dist: appdirs>=1.4; platform_system != "Emscripten"
|
|
14
|
+
Requires-Dist: click>=8.1.3; platform_system != "Emscripten"
|
|
15
|
+
Requires-Dist: cryptography==39; platform_system != "Emscripten"
|
|
16
|
+
Requires-Dist: grpcio>=1.62.1; platform_system != "Emscripten"
|
|
17
|
+
Requires-Dist: humanize>=4.6.0; platform_system != "Emscripten"
|
|
18
|
+
Requires-Dist: libusb1>=2.0.1; platform_system != "Emscripten"
|
|
19
|
+
Requires-Dist: libusb-package==1.0.26.1; platform_system != "Emscripten"
|
|
20
|
+
Requires-Dist: platformdirs>=3.10.0; platform_system != "Emscripten"
|
|
21
|
+
Requires-Dist: prompt-toolkit>=3.0.16; platform_system != "Emscripten"
|
|
22
|
+
Requires-Dist: prettytable>=3.6.0; platform_system != "Emscripten"
|
|
23
|
+
Requires-Dist: protobuf>=3.12.4; platform_system != "Emscripten"
|
|
24
|
+
Requires-Dist: pyserial-asyncio>=0.5; platform_system != "Emscripten"
|
|
25
|
+
Requires-Dist: pyserial>=3.5; platform_system != "Emscripten"
|
|
26
|
+
Requires-Dist: pyusb>=1.2; platform_system != "Emscripten"
|
|
27
|
+
Requires-Dist: websockets>=12.0; platform_system != "Emscripten"
|
|
28
|
+
Requires-Dist: cryptography>=39.0; platform_system == "Emscripten"
|
|
29
29
|
Provides-Extra: avatar
|
|
30
|
-
Requires-Dist: pandora-avatar
|
|
31
|
-
Requires-Dist: rootcanal
|
|
30
|
+
Requires-Dist: pandora-avatar==0.0.9; extra == "avatar"
|
|
31
|
+
Requires-Dist: rootcanal==1.10.0; python_version >= "3.10" and extra == "avatar"
|
|
32
32
|
Provides-Extra: build
|
|
33
|
-
Requires-Dist: build
|
|
33
|
+
Requires-Dist: build>=0.7; extra == "build"
|
|
34
34
|
Provides-Extra: development
|
|
35
|
-
Requires-Dist: black
|
|
36
|
-
Requires-Dist: grpcio-tools
|
|
37
|
-
Requires-Dist: invoke
|
|
38
|
-
Requires-Dist: mypy
|
|
39
|
-
Requires-Dist: nox
|
|
40
|
-
Requires-Dist: pylint
|
|
41
|
-
Requires-Dist: pyyaml
|
|
42
|
-
Requires-Dist: types-appdirs
|
|
43
|
-
Requires-Dist: types-invoke
|
|
44
|
-
Requires-Dist: types-protobuf
|
|
45
|
-
Requires-Dist: wasmtime
|
|
35
|
+
Requires-Dist: black==24.3; extra == "development"
|
|
36
|
+
Requires-Dist: grpcio-tools>=1.62.1; extra == "development"
|
|
37
|
+
Requires-Dist: invoke>=1.7.3; extra == "development"
|
|
38
|
+
Requires-Dist: mypy==1.10.0; extra == "development"
|
|
39
|
+
Requires-Dist: nox>=2022; extra == "development"
|
|
40
|
+
Requires-Dist: pylint==3.1.0; extra == "development"
|
|
41
|
+
Requires-Dist: pyyaml>=6.0; extra == "development"
|
|
42
|
+
Requires-Dist: types-appdirs>=1.4.3; extra == "development"
|
|
43
|
+
Requires-Dist: types-invoke>=1.7.3; extra == "development"
|
|
44
|
+
Requires-Dist: types-protobuf>=4.21.0; extra == "development"
|
|
45
|
+
Requires-Dist: wasmtime==20.0.0; extra == "development"
|
|
46
46
|
Provides-Extra: documentation
|
|
47
|
-
Requires-Dist: mkdocs
|
|
48
|
-
Requires-Dist: mkdocs-material
|
|
49
|
-
Requires-Dist: mkdocstrings[python]
|
|
47
|
+
Requires-Dist: mkdocs>=1.4.0; extra == "documentation"
|
|
48
|
+
Requires-Dist: mkdocs-material>=8.5.6; extra == "documentation"
|
|
49
|
+
Requires-Dist: mkdocstrings[python]>=0.19.0; extra == "documentation"
|
|
50
50
|
Provides-Extra: pandora
|
|
51
|
-
Requires-Dist: bt-test-interfaces
|
|
51
|
+
Requires-Dist: bt-test-interfaces>=0.0.6; extra == "pandora"
|
|
52
52
|
Provides-Extra: test
|
|
53
|
-
Requires-Dist: pytest
|
|
54
|
-
Requires-Dist: pytest-asyncio
|
|
55
|
-
Requires-Dist: pytest-html
|
|
56
|
-
Requires-Dist: coverage
|
|
53
|
+
Requires-Dist: pytest>=8.2; extra == "test"
|
|
54
|
+
Requires-Dist: pytest-asyncio>=0.23.5; extra == "test"
|
|
55
|
+
Requires-Dist: pytest-html>=3.2.0; extra == "test"
|
|
56
|
+
Requires-Dist: coverage>=6.4; extra == "test"
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
_ _ _
|