bumble 0.0.213__py3-none-any.whl → 0.0.215__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 (123) hide show
  1. bumble/_version.py +16 -3
  2. bumble/a2dp.py +15 -16
  3. bumble/apps/auracast.py +14 -38
  4. bumble/apps/bench.py +10 -15
  5. bumble/apps/ble_rpa_tool.py +1 -0
  6. bumble/apps/console.py +22 -25
  7. bumble/apps/controller_info.py +20 -25
  8. bumble/apps/controller_loopback.py +6 -10
  9. bumble/apps/controllers.py +2 -3
  10. bumble/apps/device_info.py +4 -5
  11. bumble/apps/gatt_dump.py +3 -3
  12. bumble/apps/gg_bridge.py +7 -8
  13. bumble/apps/hci_bridge.py +4 -3
  14. bumble/apps/l2cap_bridge.py +5 -5
  15. bumble/apps/lea_unicast/app.py +16 -26
  16. bumble/apps/pair.py +30 -43
  17. bumble/apps/pandora_server.py +5 -4
  18. bumble/apps/player/player.py +20 -24
  19. bumble/apps/rfcomm_bridge.py +4 -10
  20. bumble/apps/scan.py +17 -8
  21. bumble/apps/show.py +4 -5
  22. bumble/apps/speaker/speaker.py +23 -27
  23. bumble/apps/unbond.py +3 -3
  24. bumble/apps/usb_probe.py +2 -4
  25. bumble/att.py +241 -246
  26. bumble/audio/io.py +5 -9
  27. bumble/avc.py +2 -2
  28. bumble/avctp.py +6 -7
  29. bumble/avdtp.py +19 -22
  30. bumble/avrcp.py +1097 -589
  31. bumble/codecs.py +2 -0
  32. bumble/controller.py +142 -35
  33. bumble/core.py +567 -248
  34. bumble/crypto/__init__.py +2 -2
  35. bumble/crypto/builtin.py +1 -1
  36. bumble/crypto/cryptography.py +2 -4
  37. bumble/data_types.py +1025 -0
  38. bumble/device.py +319 -267
  39. bumble/drivers/__init__.py +3 -2
  40. bumble/drivers/intel.py +3 -4
  41. bumble/drivers/rtk.py +26 -9
  42. bumble/gap.py +4 -4
  43. bumble/gatt.py +3 -2
  44. bumble/gatt_adapters.py +3 -11
  45. bumble/gatt_client.py +69 -81
  46. bumble/gatt_server.py +124 -124
  47. bumble/hci.py +114 -18
  48. bumble/helpers.py +19 -26
  49. bumble/hfp.py +10 -21
  50. bumble/hid.py +22 -16
  51. bumble/host.py +191 -103
  52. bumble/keys.py +5 -3
  53. bumble/l2cap.py +138 -104
  54. bumble/link.py +18 -19
  55. bumble/logging.py +65 -0
  56. bumble/pairing.py +7 -6
  57. bumble/pandora/__init__.py +9 -8
  58. bumble/pandora/config.py +3 -1
  59. bumble/pandora/device.py +3 -2
  60. bumble/pandora/host.py +38 -36
  61. bumble/pandora/l2cap.py +22 -21
  62. bumble/pandora/security.py +15 -15
  63. bumble/pandora/utils.py +5 -3
  64. bumble/profiles/aics.py +11 -11
  65. bumble/profiles/ams.py +403 -0
  66. bumble/profiles/ancs.py +6 -7
  67. bumble/profiles/ascs.py +14 -9
  68. bumble/profiles/asha.py +8 -12
  69. bumble/profiles/bap.py +11 -23
  70. bumble/profiles/bass.py +2 -7
  71. bumble/profiles/battery_service.py +3 -4
  72. bumble/profiles/cap.py +1 -2
  73. bumble/profiles/csip.py +2 -6
  74. bumble/profiles/device_information_service.py +2 -2
  75. bumble/profiles/gap.py +4 -4
  76. bumble/profiles/gatt_service.py +1 -4
  77. bumble/profiles/gmap.py +5 -5
  78. bumble/profiles/hap.py +62 -59
  79. bumble/profiles/heart_rate_service.py +5 -4
  80. bumble/profiles/le_audio.py +3 -1
  81. bumble/profiles/mcp.py +3 -7
  82. bumble/profiles/pacs.py +3 -6
  83. bumble/profiles/pbp.py +2 -0
  84. bumble/profiles/tmap.py +2 -3
  85. bumble/profiles/vcs.py +2 -8
  86. bumble/profiles/vocs.py +8 -8
  87. bumble/rfcomm.py +11 -14
  88. bumble/rtp.py +1 -0
  89. bumble/sdp.py +10 -8
  90. bumble/smp.py +151 -159
  91. bumble/snoop.py +5 -5
  92. bumble/tools/generate_company_id_list.py +1 -0
  93. bumble/tools/intel_fw_download.py +3 -3
  94. bumble/tools/intel_util.py +5 -4
  95. bumble/tools/rtk_fw_download.py +6 -3
  96. bumble/tools/rtk_util.py +26 -8
  97. bumble/transport/__init__.py +19 -15
  98. bumble/transport/android_emulator.py +8 -13
  99. bumble/transport/android_netsim.py +19 -18
  100. bumble/transport/common.py +12 -15
  101. bumble/transport/file.py +1 -1
  102. bumble/transport/hci_socket.py +4 -6
  103. bumble/transport/pty.py +5 -6
  104. bumble/transport/pyusb.py +7 -10
  105. bumble/transport/serial.py +2 -1
  106. bumble/transport/tcp_client.py +2 -2
  107. bumble/transport/tcp_server.py +11 -14
  108. bumble/transport/udp.py +3 -3
  109. bumble/transport/unix.py +67 -1
  110. bumble/transport/usb.py +6 -6
  111. bumble/transport/vhci.py +0 -1
  112. bumble/transport/ws_client.py +2 -1
  113. bumble/transport/ws_server.py +3 -2
  114. bumble/utils.py +20 -5
  115. bumble/vendor/android/hci.py +1 -2
  116. bumble/vendor/zephyr/hci.py +0 -1
  117. {bumble-0.0.213.dist-info → bumble-0.0.215.dist-info}/METADATA +4 -2
  118. bumble-0.0.215.dist-info/RECORD +183 -0
  119. bumble-0.0.213.dist-info/RECORD +0 -180
  120. {bumble-0.0.213.dist-info → bumble-0.0.215.dist-info}/WHEEL +0 -0
  121. {bumble-0.0.213.dist-info → bumble-0.0.215.dist-info}/entry_points.txt +0 -0
  122. {bumble-0.0.213.dist-info → bumble-0.0.215.dist-info}/licenses/LICENSE +0 -0
  123. {bumble-0.0.213.dist-info → bumble-0.0.215.dist-info}/top_level.txt +0 -0
bumble/tools/rtk_util.py CHANGED
@@ -15,15 +15,15 @@
15
15
  # -----------------------------------------------------------------------------
16
16
  # Imports
17
17
  # -----------------------------------------------------------------------------
18
- import logging
19
18
  import asyncio
20
- import os
19
+ import logging
21
20
 
22
21
  import click
23
22
 
24
- from bumble import transport
25
- from bumble.host import Host
23
+ import bumble.logging
24
+ from bumble import company_ids, hci, transport
26
25
  from bumble.drivers import rtk
26
+ from bumble.host import Host
27
27
 
28
28
  # -----------------------------------------------------------------------------
29
29
  # Logging
@@ -61,10 +61,22 @@ async def do_load(usb_transport, force):
61
61
  # Get the driver.
62
62
  driver = await rtk.Driver.for_host(host, force)
63
63
  if driver is None:
64
- print("Firmware already loaded or no supported driver for this device.")
64
+ # Try to see if there's already a FW image loaded
65
+ firmware_version = await rtk.Driver.get_loaded_firmware_version(host)
66
+ if firmware_version is None:
67
+ print("Device not supported")
68
+ return
69
+
70
+ print(f"Firmware already loaded: 0x{firmware_version:08X}")
65
71
  return
66
72
 
67
- await driver.download_firmware()
73
+ firmware_version = await driver.download_firmware()
74
+
75
+ if firmware_version is None:
76
+ print("Failed to load firmware")
77
+ return
78
+
79
+ print(f"Loaded firmware version 0x{firmware_version:08X}")
68
80
 
69
81
 
70
82
  # -----------------------------------------------------------------------------
@@ -106,13 +118,19 @@ async def do_info(usb_transport, force):
106
118
  f" Config: {driver_info.config_name}\n"
107
119
  )
108
120
  else:
109
- print("Firmware already loaded or no supported driver for this device.")
121
+ # Try to see if there's already a FW image loaded
122
+ firmware_version = await rtk.Driver.get_loaded_firmware_version(host)
123
+ if firmware_version is None:
124
+ print("Device not supported")
125
+ return
126
+
127
+ print(f"Firmware loaded: 0x{firmware_version:08X}")
110
128
 
111
129
 
112
130
  # -----------------------------------------------------------------------------
113
131
  @click.group()
114
132
  def main():
115
- logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
133
+ bumble.logging.setup_basic_logging()
116
134
 
117
135
 
118
136
  @main.command
@@ -15,18 +15,14 @@
15
15
  # -----------------------------------------------------------------------------
16
16
  # Imports
17
17
  # -----------------------------------------------------------------------------
18
- from contextlib import asynccontextmanager
19
18
  import logging
20
19
  import os
20
+ import re
21
21
  from typing import Optional
22
22
 
23
23
  from bumble import utils
24
- from bumble.transport.common import (
25
- Transport,
26
- SnoopingTransport,
27
- TransportSpecError,
28
- )
29
24
  from bumble.snoop import create_snooper
25
+ from bumble.transport.common import SnoopingTransport, Transport, TransportSpecError
30
26
 
31
27
  # -----------------------------------------------------------------------------
32
28
  # Logging
@@ -48,8 +44,8 @@ def _wrap_transport(transport: Transport) -> Transport:
48
44
  return SnoopingTransport.create_with(
49
45
  transport, create_snooper(snooper_spec)
50
46
  )
51
- except Exception as exc:
52
- logger.warning(f'Exception while creating snooper: {exc}')
47
+ except Exception:
48
+ logger.exception('Exception while creating snooper')
53
49
 
54
50
  return transport
55
51
 
@@ -88,12 +84,14 @@ async def open_transport(name: str) -> Transport:
88
84
  scheme, *tail = name.split(':', 1)
89
85
  spec = tail[0] if tail else None
90
86
  metadata = None
91
- if spec:
92
- # Metadata may precede the spec
93
- if spec.startswith('['):
94
- metadata_str, *tail = spec[1:].split(']')
95
- spec = tail[0] if tail else None
96
- metadata = dict([entry.split('=') for entry in metadata_str.split(',')])
87
+ if spec and (m := re.search(r'\[(\w+=\w+(?:,\w+=\w+)*,?)\]', spec)):
88
+ metadata_str = m.group(1)
89
+ if m.start() == 0:
90
+ # <metadata><spec>
91
+ spec = spec[m.end() :]
92
+ else:
93
+ spec = spec[: m.start()]
94
+ metadata = dict([entry.split('=') for entry in metadata_str.split(',')])
97
95
 
98
96
  transport = await _open_transport(scheme, spec)
99
97
  if metadata:
@@ -185,12 +183,18 @@ async def _open_transport(scheme: str, spec: Optional[str]) -> Transport:
185
183
 
186
184
  return await open_android_netsim_transport(spec)
187
185
 
188
- if scheme == 'unix':
186
+ if scheme in ('unix', 'unix-client'):
189
187
  from bumble.transport.unix import open_unix_client_transport
190
188
 
191
189
  assert spec
192
190
  return await open_unix_client_transport(spec)
193
191
 
192
+ if scheme == 'unix-server':
193
+ from bumble.transport.unix import open_unix_server_transport
194
+
195
+ assert spec
196
+ return await open_unix_server_transport(spec)
197
+
194
198
  raise TransportSpecError('unknown transport scheme')
195
199
 
196
200
 
@@ -16,28 +16,27 @@
16
16
  # Imports
17
17
  # -----------------------------------------------------------------------------
18
18
  import logging
19
- import grpc.aio
20
-
21
19
  from typing import Optional, Union
22
20
 
21
+ import grpc.aio
22
+
23
23
  from bumble.transport.common import (
24
- PumpedTransport,
25
- PumpedPacketSource,
26
24
  PumpedPacketSink,
25
+ PumpedPacketSource,
26
+ PumpedTransport,
27
27
  Transport,
28
28
  TransportSpecError,
29
29
  )
30
30
 
31
31
  # pylint: disable=no-name-in-module
32
+ from bumble.transport.grpc_protobuf.emulated_bluetooth_packets_pb2 import HCIPacket
32
33
  from bumble.transport.grpc_protobuf.emulated_bluetooth_pb2_grpc import (
33
34
  EmulatedBluetoothServiceStub,
34
35
  )
35
- from bumble.transport.grpc_protobuf.emulated_bluetooth_packets_pb2 import HCIPacket
36
36
  from bumble.transport.grpc_protobuf.emulated_bluetooth_vhci_pb2_grpc import (
37
37
  VhciForwardingServiceStub,
38
38
  )
39
39
 
40
-
41
40
  # -----------------------------------------------------------------------------
42
41
  # Logging
43
42
  # -----------------------------------------------------------------------------
@@ -77,21 +76,17 @@ async def open_android_emulator_transport(spec: Optional[str]) -> Transport:
77
76
 
78
77
  # Parse the parameters
79
78
  mode = 'host'
80
- server_host = 'localhost'
81
- server_port = '8554'
79
+ server_address = 'localhost:8554'
82
80
  if spec:
83
81
  params = spec.split(',')
84
82
  for param in params:
85
83
  if param.startswith('mode='):
86
84
  mode = param.split('=')[1]
87
- elif ':' in param:
88
- server_host, server_port = param.split(':')
89
85
  else:
90
- raise TransportSpecError('invalid parameter')
86
+ server_address = param
91
87
 
92
88
  # Connect to the gRPC server
93
- server_address = f'{server_host}:{server_port}'
94
- logger.debug(f'connecting to gRPC server at {server_address}')
89
+ logger.debug('connecting to gRPC server at %s', server_address)
95
90
  channel = grpc.aio.insecure_channel(server_address)
96
91
 
97
92
  service: Union[EmulatedBluetoothServiceStub, VhciForwardingServiceStub]
@@ -29,28 +29,27 @@ import grpc.aio
29
29
  import bumble
30
30
  from bumble.transport.common import (
31
31
  ParserSource,
32
- PumpedTransport,
33
- PumpedPacketSource,
34
32
  PumpedPacketSink,
33
+ PumpedPacketSource,
34
+ PumpedTransport,
35
35
  Transport,
36
- TransportSpecError,
37
36
  TransportInitError,
37
+ TransportSpecError,
38
38
  )
39
39
 
40
40
  # pylint: disable=no-name-in-module
41
- from bumble.transport.grpc_protobuf.netsim.packet_streamer_pb2_grpc import (
42
- PacketStreamerStub,
43
- PacketStreamerServicer,
44
- add_PacketStreamerServicer_to_server,
45
- )
41
+ from bumble.transport.grpc_protobuf.netsim.common_pb2 import ChipKind
42
+ from bumble.transport.grpc_protobuf.netsim.hci_packet_pb2 import HCIPacket
46
43
  from bumble.transport.grpc_protobuf.netsim.packet_streamer_pb2 import (
47
44
  PacketRequest,
48
45
  PacketResponse,
49
46
  )
50
- from bumble.transport.grpc_protobuf.netsim.hci_packet_pb2 import HCIPacket
47
+ from bumble.transport.grpc_protobuf.netsim.packet_streamer_pb2_grpc import (
48
+ PacketStreamerServicer,
49
+ PacketStreamerStub,
50
+ add_PacketStreamerServicer_to_server,
51
+ )
51
52
  from bumble.transport.grpc_protobuf.netsim.startup_pb2 import Chip, ChipInfo, DeviceInfo
52
- from bumble.transport.grpc_protobuf.netsim.common_pb2 import ChipKind
53
-
54
53
 
55
54
  # -----------------------------------------------------------------------------
56
55
  # Logging
@@ -145,8 +144,6 @@ def publish_grpc_port(grpc_port: int, instance_number: int) -> bool:
145
144
  async def open_android_netsim_controller_transport(
146
145
  server_host: Optional[str], server_port: int, options: dict[str, str]
147
146
  ) -> Transport:
148
- if not server_port:
149
- raise TransportSpecError('invalid port')
150
147
  if server_host == '_' or not server_host:
151
148
  server_host = 'localhost'
152
149
 
@@ -168,14 +165,16 @@ async def open_android_netsim_controller_transport(
168
165
  await self.pump_loop()
169
166
  except asyncio.CancelledError:
170
167
  logger.debug('Pump task canceled')
171
- self.done.set_result(None)
168
+ if not self.done.done():
169
+ self.done.set_result(None)
172
170
 
173
171
  async def pump_loop(self):
174
172
  while True:
175
173
  request = await self.context.read()
176
174
  if request == grpc.aio.EOF:
177
175
  logger.debug('End of request stream')
178
- self.done.set_result(None)
176
+ if not self.done.done():
177
+ self.done.set_result(None)
179
178
  return
180
179
 
181
180
  # If we're not initialized yet, wait for a init packet.
@@ -220,6 +219,8 @@ async def open_android_netsim_controller_transport(
220
219
  async def wait_for_termination(self):
221
220
  await self.done
222
221
 
222
+ server_address = f'{server_host}:{server_port}'
223
+
223
224
  class Server(PacketStreamerServicer, ParserSource):
224
225
  def __init__(self):
225
226
  PacketStreamerServicer.__init__(self)
@@ -230,8 +231,8 @@ async def open_android_netsim_controller_transport(
230
231
  # a server listening on that port, we get an exception.
231
232
  self.grpc_server = grpc.aio.server(options=(('grpc.so_reuseport', 0),))
232
233
  add_PacketStreamerServicer_to_server(self, self.grpc_server)
233
- self.grpc_server.add_insecure_port(f'{server_host}:{server_port}')
234
- logger.debug(f'gRPC server listening on {server_host}:{server_port}')
234
+ self.port = self.grpc_server.add_insecure_port(server_address)
235
+ logger.debug('gRPC server listening on %s', server_address)
235
236
 
236
237
  async def start(self):
237
238
  logger.debug('Starting gRPC server')
@@ -443,7 +444,7 @@ async def open_android_netsim_transport(spec: Optional[str]) -> Transport:
443
444
  params = spec.split(',') if spec else []
444
445
  if params and ':' in params[0]:
445
446
  # Explicit <host>:<port>
446
- host, port_str = params[0].split(':')
447
+ host, port_str = params[0].rsplit(':', maxsplit=1)
447
448
  port = int(port_str)
448
449
  params_offset = 1
449
450
  else:
@@ -16,19 +16,18 @@
16
16
  # Imports
17
17
  # -----------------------------------------------------------------------------
18
18
  from __future__ import annotations
19
- import contextlib
20
- import struct
19
+
21
20
  import asyncio
22
- import logging
21
+ import contextlib
23
22
  import io
23
+ import logging
24
+ import struct
24
25
  from typing import Any, ContextManager, Optional, Protocol
25
26
 
26
- from bumble import core
27
- from bumble import hci
27
+ from bumble import core, hci
28
28
  from bumble.colors import color
29
29
  from bumble.snoop import Snooper
30
30
 
31
-
32
31
  # -----------------------------------------------------------------------------
33
32
  # Logging
34
33
  # -----------------------------------------------------------------------------
@@ -90,8 +89,8 @@ class PacketPump:
90
89
  try:
91
90
  # Deliver the packet to the sink
92
91
  self.sink.on_packet(await self.reader.next_packet())
93
- except Exception as error:
94
- logger.warning(f'!!! {error}')
92
+ except Exception:
93
+ logger.exception('!!!')
95
94
 
96
95
 
97
96
  # -----------------------------------------------------------------------------
@@ -158,10 +157,8 @@ class PacketParser:
158
157
  if self.sink:
159
158
  try:
160
159
  self.sink.on_packet(bytes(self.packet))
161
- except Exception as error:
162
- logger.exception(
163
- color(f'!!! Exception in on_packet: {error}', 'red')
164
- )
160
+ except Exception:
161
+ logger.exception(color('!!! Exception in on_packet', 'red'))
165
162
  self.reset()
166
163
 
167
164
  def set_packet_sink(self, sink: TransportSink) -> None:
@@ -378,7 +375,7 @@ class PumpedPacketSource(ParserSource):
378
375
  self.terminated.set_result(None)
379
376
  break
380
377
  except Exception as error:
381
- logger.warning(f'exception while waiting for packet: {error}')
378
+ logger.exception('exception while waiting for packet')
382
379
  if not self.terminated.done():
383
380
  self.terminated.set_exception(error)
384
381
  break
@@ -409,8 +406,8 @@ class PumpedPacketSink:
409
406
  except asyncio.CancelledError:
410
407
  logger.debug('sink pump task done')
411
408
  break
412
- except Exception as error:
413
- logger.warning(f'exception while sending packet: {error}')
409
+ except Exception:
410
+ logger.exception('exception while sending packet')
414
411
  break
415
412
 
416
413
  self.pump_task = asyncio.create_task(pump_packets())
bumble/transport/file.py CHANGED
@@ -19,7 +19,7 @@ import asyncio
19
19
  import io
20
20
  import logging
21
21
 
22
- from bumble.transport.common import Transport, StreamPacketSource, StreamPacketSink
22
+ from bumble.transport.common import StreamPacketSink, StreamPacketSource, Transport
23
23
 
24
24
  # -----------------------------------------------------------------------------
25
25
  # Logging
@@ -16,17 +16,15 @@
16
16
  # Imports
17
17
  # -----------------------------------------------------------------------------
18
18
  import asyncio
19
+ import collections
20
+ import ctypes
19
21
  import logging
20
- import struct
21
22
  import os
22
23
  import socket
23
- import ctypes
24
- import collections
25
-
24
+ import struct
26
25
  from typing import Optional
27
26
 
28
- from bumble.transport.common import Transport, ParserSource
29
-
27
+ from bumble.transport.common import ParserSource, Transport
30
28
 
31
29
  # -----------------------------------------------------------------------------
32
30
  # Logging
bumble/transport/pty.py CHANGED
@@ -16,16 +16,15 @@
16
16
  # Imports
17
17
  # -----------------------------------------------------------------------------
18
18
  import asyncio
19
- import pty
20
- import tty
21
- import io
22
19
  import atexit
23
- import os
20
+ import io
24
21
  import logging
25
-
22
+ import os
23
+ import pty
24
+ import tty
26
25
  from typing import Optional
27
26
 
28
- from bumble.transport.common import Transport, StreamPacketSource, StreamPacketSink
27
+ from bumble.transport.common import StreamPacketSink, StreamPacketSource, Transport
29
28
 
30
29
  # -----------------------------------------------------------------------------
31
30
  # Logging
bumble/transport/pyusb.py CHANGED
@@ -19,20 +19,18 @@ import asyncio
19
19
  import logging
20
20
  import threading
21
21
  import time
22
+ from typing import Optional
22
23
 
23
24
  import usb.core
24
25
  import usb.util
25
-
26
- from typing import Optional
27
26
  from usb.core import Device as UsbDevice
28
27
  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
28
+ from usb.legacy import CLASS_HUB, REQ_CLEAR_FEATURE, REQ_SET_FEATURE
29
+ from usb.util import CTRL_RECIPIENT_OTHER, CTRL_TYPE_CLASS
31
30
 
32
- from bumble.transport.common import Transport, ParserSource, TransportInitError
33
31
  from bumble import hci
34
32
  from bumble.colors import color
35
-
33
+ from bumble.transport.common import ParserSource, Transport, TransportInitError
36
34
 
37
35
  # -----------------------------------------------------------------------------
38
36
  # Constant
@@ -285,7 +283,7 @@ async def open_pyusb_transport(spec: str) -> Transport:
285
283
  try:
286
284
  device = await _power_cycle(device) # type: ignore
287
285
  except Exception as e:
288
- logging.debug(e)
286
+ logging.debug(e, stack_info=True)
289
287
  logging.info(f"Unable to power cycle {hex(device.idVendor)} {hex(device.idProduct)}") # type: ignore
290
288
 
291
289
  # Collect the metadata
@@ -371,9 +369,8 @@ async def _power_cycle(device: UsbDevice) -> UsbDevice:
371
369
 
372
370
  # Device needs to be find again otherwise it will appear as disconnected
373
371
  return usb.core.find(idVendor=device.idVendor, idProduct=device.idProduct) # type: ignore
374
- except USBError as e:
375
- logger.error(f"Adjustment needed: Please revise the udev rule for device {hex(device.idVendor)}:{hex(device.idProduct)} for proper recognition.") # type: ignore
376
- logger.error(e)
372
+ except USBError:
373
+ logger.exception(f"Adjustment needed: Please revise the udev rule for device {hex(device.idVendor)}:{hex(device.idProduct)} for proper recognition.") # type: ignore
377
374
 
378
375
  return device
379
376
 
@@ -17,9 +17,10 @@
17
17
  # -----------------------------------------------------------------------------
18
18
  import asyncio
19
19
  import logging
20
+
20
21
  import serial_asyncio
21
22
 
22
- from bumble.transport.common import Transport, StreamPacketSource, StreamPacketSink
23
+ from bumble.transport.common import StreamPacketSink, StreamPacketSource, Transport
23
24
 
24
25
  # -----------------------------------------------------------------------------
25
26
  # Logging
@@ -18,7 +18,7 @@
18
18
  import asyncio
19
19
  import logging
20
20
 
21
- from bumble.transport.common import Transport, StreamPacketSource, StreamPacketSink
21
+ from bumble.transport.common import StreamPacketSink, StreamPacketSource, Transport
22
22
 
23
23
  # -----------------------------------------------------------------------------
24
24
  # Logging
@@ -41,7 +41,7 @@ async def open_tcp_client_transport(spec: str) -> Transport:
41
41
  logger.debug(f'connection lost: {exc}')
42
42
  self.on_transport_lost()
43
43
 
44
- remote_host, remote_port = spec.split(':')
44
+ remote_host, remote_port = spec.rsplit(':', maxsplit=1)
45
45
  tcp_transport, packet_source = await asyncio.get_running_loop().create_connection(
46
46
  TcpPacketSource,
47
47
  host=remote_host,
@@ -16,11 +16,12 @@
16
16
  # Imports
17
17
  # -----------------------------------------------------------------------------
18
18
  from __future__ import annotations
19
+
19
20
  import asyncio
20
21
  import logging
21
22
  import socket
22
23
 
23
- from bumble.transport.common import Transport, StreamPacketSource
24
+ from bumble.transport.common import StreamPacketSource, Transport
24
25
 
25
26
  # -----------------------------------------------------------------------------
26
27
  # Logging
@@ -29,13 +30,6 @@ logger = logging.getLogger(__name__)
29
30
 
30
31
 
31
32
  # -----------------------------------------------------------------------------
32
-
33
-
34
- # A pass-through function to ease mock testing.
35
- async def _create_server(*args, **kw_args):
36
- await asyncio.get_running_loop().create_server(*args, **kw_args)
37
-
38
-
39
33
  async def open_tcp_server_transport(spec: str) -> Transport:
40
34
  '''
41
35
  Open a TCP server transport.
@@ -46,13 +40,15 @@ async def open_tcp_server_transport(spec: str) -> Transport:
46
40
 
47
41
  Example: _:9001
48
42
  '''
49
- local_host, local_port = spec.split(':')
43
+ local_host, local_port = spec.rsplit(':', maxsplit=1)
50
44
  return await _open_tcp_server_transport_impl(
51
45
  host=local_host if local_host != '_' else None, port=int(local_port)
52
46
  )
53
47
 
54
48
 
55
- async def open_tcp_server_transport_with_socket(sock: socket.socket) -> Transport:
49
+ async def open_tcp_server_transport_with_socket(
50
+ sock: socket.socket,
51
+ ) -> Transport:
56
52
  '''
57
53
  Open a TCP server transport with an existing socket.
58
54
 
@@ -63,8 +59,9 @@ async def open_tcp_server_transport_with_socket(sock: socket.socket) -> Transpor
63
59
 
64
60
  async def _open_tcp_server_transport_impl(**kwargs) -> Transport:
65
61
  class TcpServerTransport(Transport):
66
- async def close(self):
67
- await super().close()
62
+ def __init__(self, source, sink, server):
63
+ self.server = server
64
+ super().__init__(source, sink)
68
65
 
69
66
  class TcpServerProtocol(asyncio.BaseProtocol):
70
67
  def __init__(self, packet_source, packet_sink):
@@ -102,8 +99,8 @@ async def _open_tcp_server_transport_impl(**kwargs) -> Transport:
102
99
 
103
100
  packet_source = StreamPacketSource()
104
101
  packet_sink = TcpServerPacketSink()
105
- await _create_server(
102
+ server = await asyncio.get_running_loop().create_server(
106
103
  lambda: TcpServerProtocol(packet_source, packet_sink), **kwargs
107
104
  )
108
105
 
109
- return TcpServerTransport(packet_source, packet_sink)
106
+ return TcpServerTransport(packet_source, packet_sink, server)
bumble/transport/udp.py CHANGED
@@ -18,7 +18,7 @@
18
18
  import asyncio
19
19
  import logging
20
20
 
21
- from bumble.transport.common import Transport, ParserSource
21
+ from bumble.transport.common import ParserSource, Transport
22
22
 
23
23
  # -----------------------------------------------------------------------------
24
24
  # Logging
@@ -51,8 +51,8 @@ async def open_udp_transport(spec: str) -> Transport:
51
51
  self.transport.close()
52
52
 
53
53
  local, remote = spec.split(',')
54
- local_host, local_port = local.split(':')
55
- remote_host, remote_port = remote.split(':')
54
+ local_host, local_port = local.rsplit(':', maxsplit=1)
55
+ remote_host, remote_port = remote.rsplit(':', maxsplit=1)
56
56
  (
57
57
  udp_transport,
58
58
  packet_source,
bumble/transport/unix.py CHANGED
@@ -18,7 +18,7 @@
18
18
  import asyncio
19
19
  import logging
20
20
 
21
- from bumble.transport.common import Transport, StreamPacketSource, StreamPacketSink
21
+ from bumble.transport.common import StreamPacketSink, StreamPacketSource, Transport
22
22
 
23
23
  # -----------------------------------------------------------------------------
24
24
  # Logging
@@ -54,3 +54,69 @@ async def open_unix_client_transport(spec: str) -> Transport:
54
54
  packet_sink = StreamPacketSink(unix_transport)
55
55
 
56
56
  return Transport(packet_source, packet_sink)
57
+
58
+
59
+ # -----------------------------------------------------------------------------
60
+ async def open_unix_server_transport(spec: str) -> Transport:
61
+ '''Open a UNIX socket server transport.
62
+
63
+ The parameter is the path of unix socket. For abstract socket, the first character
64
+ needs to be '@'.
65
+
66
+ Example:
67
+ * /tmp/hci.socket
68
+ * @hci_socket
69
+ '''
70
+ # For abstract socket, the first character should be null character.
71
+ if spec.startswith('@'):
72
+ spec = '\0' + spec[1:]
73
+
74
+ class UnixServerTransport(Transport):
75
+ def __init__(self, source, sink, server):
76
+ self.server = server
77
+ super().__init__(source, sink)
78
+
79
+ async def close(self):
80
+ await super().close()
81
+
82
+ class UnixServerProtocol(asyncio.BaseProtocol):
83
+ def __init__(self, packet_source, packet_sink):
84
+ self.packet_source = packet_source
85
+ self.packet_sink = packet_sink
86
+
87
+ # Called when a new connection is established
88
+ def connection_made(self, transport):
89
+ peer_name = transport.get_extra_info('peer_name')
90
+ logger.debug('connection from %s', peer_name)
91
+ self.packet_sink.transport = transport
92
+
93
+ # Called when the client is disconnected
94
+ def connection_lost(self, error):
95
+ logger.debug('connection lost: %s', error)
96
+ self.packet_sink.transport = None
97
+
98
+ def eof_received(self):
99
+ logger.debug('connection end')
100
+ self.packet_sink.transport = None
101
+
102
+ # Called when data is received on the socket
103
+ def data_received(self, data):
104
+ self.packet_source.data_received(data)
105
+
106
+ class UnixServerPacketSink:
107
+ def __init__(self):
108
+ self.transport = None
109
+
110
+ def on_packet(self, packet):
111
+ if self.transport:
112
+ self.transport.write(packet)
113
+ else:
114
+ logger.debug('no client, dropping packet')
115
+
116
+ packet_source = StreamPacketSource()
117
+ packet_sink = UnixServerPacketSink()
118
+ server = await asyncio.get_running_loop().create_unix_server(
119
+ lambda: UnixServerProtocol(packet_source, packet_sink), spec
120
+ )
121
+
122
+ return UnixServerTransport(packet_source, packet_sink, server)