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.
Files changed (61) hide show
  1. bumble/_version.py +2 -2
  2. bumble/apps/auracast.py +351 -66
  3. bumble/apps/console.py +5 -20
  4. bumble/apps/device_info.py +230 -0
  5. bumble/apps/gatt_dump.py +4 -0
  6. bumble/apps/lea_unicast/app.py +16 -17
  7. bumble/apps/pair.py +32 -5
  8. bumble/at.py +12 -6
  9. bumble/att.py +56 -40
  10. bumble/avc.py +8 -5
  11. bumble/avctp.py +3 -2
  12. bumble/avdtp.py +7 -3
  13. bumble/avrcp.py +2 -1
  14. bumble/codecs.py +17 -13
  15. bumble/colors.py +6 -2
  16. bumble/core.py +37 -7
  17. bumble/decoder.py +14 -10
  18. bumble/device.py +382 -111
  19. bumble/drivers/rtk.py +32 -13
  20. bumble/gatt.py +30 -20
  21. bumble/gatt_client.py +15 -29
  22. bumble/gatt_server.py +14 -6
  23. bumble/hci.py +322 -32
  24. bumble/hid.py +24 -28
  25. bumble/host.py +20 -6
  26. bumble/l2cap.py +24 -17
  27. bumble/link.py +8 -3
  28. bumble/pandora/__init__.py +3 -0
  29. bumble/pandora/l2cap.py +310 -0
  30. bumble/profiles/aics.py +520 -0
  31. bumble/profiles/ascs.py +739 -0
  32. bumble/profiles/asha.py +295 -0
  33. bumble/profiles/bap.py +1 -874
  34. bumble/profiles/bass.py +440 -0
  35. bumble/profiles/csip.py +4 -4
  36. bumble/profiles/gap.py +110 -0
  37. bumble/profiles/hap.py +665 -0
  38. bumble/profiles/heart_rate_service.py +4 -3
  39. bumble/profiles/le_audio.py +43 -9
  40. bumble/profiles/mcp.py +448 -0
  41. bumble/profiles/pacs.py +210 -0
  42. bumble/profiles/tmap.py +89 -0
  43. bumble/profiles/vcp.py +5 -3
  44. bumble/rfcomm.py +4 -2
  45. bumble/sdp.py +13 -11
  46. bumble/smp.py +43 -12
  47. bumble/snoop.py +5 -4
  48. bumble/transport/__init__.py +8 -2
  49. bumble/transport/android_emulator.py +9 -3
  50. bumble/transport/android_netsim.py +9 -7
  51. bumble/transport/common.py +46 -18
  52. bumble/transport/pyusb.py +21 -4
  53. bumble/transport/unix.py +56 -0
  54. bumble/transport/usb.py +57 -46
  55. {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/METADATA +41 -41
  56. {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/RECORD +60 -49
  57. {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/WHEEL +1 -1
  58. bumble/profiles/asha_service.py +0 -193
  59. {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/LICENSE +0 -0
  60. {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/entry_points.txt +0 -0
  61. {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 ValueError('invalid port')
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 RuntimeError('gRPC server port not found')
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 RuntimeError(response.error)
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 ValueError('unsupported response type')
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 ValueError('invalid parameter, expected <name>=<value>')
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 ValueError('<host>:<port> missing')
445
+ raise TransportSpecError('<host>:<port> missing')
444
446
  return await open_android_netsim_controller_transport(host, port, options)
445
447
 
446
- raise ValueError('invalid mode option')
448
+ raise TransportSpecError('invalid mode option')
@@ -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(Exception):
53
- """
54
- The Transport has been lost/disconnected.
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 ValueError(f'invalid packet type {packet_type}')
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 ValueError(f'invalid packet type {packet_type[0]} found')
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 ValueError('packet too short')
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 ValueError('packet too short')
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 ValueError(f'invalid packet type {packet_type[0]} found')
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 ParserSource:
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
- parser: PacketParser
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.parser.set_packet_sink(sink)
264
+ self.sink = sink
256
265
 
257
266
  def on_transport_lost(self) -> None:
258
- self.terminated.set_result(None)
259
- if self.parser.sink:
260
- if hasattr(self.parser.sink, 'on_transport_lost'):
261
- self.parser.sink.on_transport_lost()
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 RuntimeError('unexpected code path') # Satisfy the type checker
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 = usb_find(idVendor=int(vendor_id, 16), idProduct=int(product_id, 16))
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 ValueError('device not found')
279
+ raise TransportInitError('device not found')
263
280
  logger.debug(f'USB Device: {device}')
264
281
 
265
282
  # Power Cycle the device
@@ -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, ParserSource
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.packets = collections.deque() # Queue of packets waiting to be sent
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
- pass
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.append(packet)
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.TRANSFER_COMPLETED:
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
- else:
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 on_packet_sent(self):
155
- if self.packets:
156
- self.packets.popleft()
157
- self.process_queue()
158
-
159
- def process_queue(self):
160
- if len(self.packets) == 0:
161
- return # Nothing to do
162
-
163
- packet = self.packets[0]
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(color(f'unsupported packet type {packet_type}', 'red'))
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.clear()
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, ParserSource):
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.parser.feed_data(packet)
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 ValueError('device not found')
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 ValueError('no compatible interface found for device')
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.195
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 >=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"
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 ==0.0.9 ; extra == 'avatar'
31
- Requires-Dist: rootcanal ==1.10.0 ; (python_version >= "3.10") and extra == 'avatar'
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 >=0.7 ; extra == 'build'
33
+ Requires-Dist: build>=0.7; extra == "build"
34
34
  Provides-Extra: development
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'
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 >=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'
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 >=0.0.6 ; extra == 'pandora'
51
+ Requires-Dist: bt-test-interfaces>=0.0.6; extra == "pandora"
52
52
  Provides-Extra: test
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'
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
  _ _ _