bumble 0.0.203__py3-none-any.whl → 0.0.207__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 +626 -87
- bumble/apps/bench.py +227 -148
- bumble/apps/controller_info.py +23 -7
- bumble/apps/device_info.py +50 -4
- bumble/apps/lea_unicast/app.py +61 -201
- bumble/apps/pair.py +13 -8
- bumble/apps/show.py +6 -6
- bumble/att.py +10 -11
- bumble/audio/__init__.py +17 -0
- bumble/audio/io.py +553 -0
- bumble/controller.py +24 -9
- bumble/core.py +4 -1
- bumble/device.py +993 -48
- bumble/drivers/common.py +2 -0
- bumble/drivers/intel.py +593 -24
- bumble/gatt.py +67 -12
- bumble/gatt_client.py +14 -2
- bumble/gatt_server.py +12 -1
- bumble/hci.py +854 -33
- bumble/host.py +363 -64
- bumble/l2cap.py +3 -16
- bumble/pairing.py +3 -0
- bumble/profiles/aics.py +45 -80
- bumble/profiles/ascs.py +6 -18
- bumble/profiles/asha.py +5 -5
- bumble/profiles/bass.py +9 -21
- bumble/profiles/device_information_service.py +4 -1
- bumble/profiles/gatt_service.py +166 -0
- bumble/profiles/gmap.py +193 -0
- bumble/profiles/heart_rate_service.py +5 -6
- bumble/profiles/le_audio.py +87 -4
- bumble/profiles/pacs.py +48 -16
- bumble/profiles/tmap.py +3 -9
- bumble/profiles/{vcp.py → vcs.py} +33 -28
- bumble/profiles/vocs.py +299 -0
- bumble/sdp.py +223 -93
- bumble/smp.py +8 -3
- bumble/tools/intel_fw_download.py +130 -0
- bumble/tools/intel_util.py +154 -0
- bumble/transport/usb.py +8 -2
- bumble/utils.py +22 -7
- bumble/vendor/android/hci.py +29 -4
- {bumble-0.0.203.dist-info → bumble-0.0.207.dist-info}/METADATA +12 -10
- {bumble-0.0.203.dist-info → bumble-0.0.207.dist-info}/RECORD +49 -43
- {bumble-0.0.203.dist-info → bumble-0.0.207.dist-info}/WHEEL +1 -1
- {bumble-0.0.203.dist-info → bumble-0.0.207.dist-info}/entry_points.txt +3 -0
- bumble/apps/lea_unicast/liblc3.wasm +0 -0
- {bumble-0.0.203.dist-info → bumble-0.0.207.dist-info}/LICENSE +0 -0
- {bumble-0.0.203.dist-info → bumble-0.0.207.dist-info}/top_level.txt +0 -0
bumble/apps/device_info.py
CHANGED
|
@@ -29,7 +29,9 @@ from bumble.gatt import Service
|
|
|
29
29
|
from bumble.profiles.device_information_service import DeviceInformationServiceProxy
|
|
30
30
|
from bumble.profiles.battery_service import BatteryServiceProxy
|
|
31
31
|
from bumble.profiles.gap import GenericAccessServiceProxy
|
|
32
|
+
from bumble.profiles.pacs import PublishedAudioCapabilitiesServiceProxy
|
|
32
33
|
from bumble.profiles.tmap import TelephonyAndMediaAudioServiceProxy
|
|
34
|
+
from bumble.profiles.vcs import VolumeControlServiceProxy
|
|
33
35
|
from bumble.transport import open_transport_or_link
|
|
34
36
|
|
|
35
37
|
|
|
@@ -126,14 +128,52 @@ async def show_tmas(
|
|
|
126
128
|
print(color('### Telephony And Media Audio Service', 'yellow'))
|
|
127
129
|
|
|
128
130
|
if tmas.role:
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
131
|
+
role = await tmas.role.read_value()
|
|
132
|
+
print(color(' Role:', 'green'), role)
|
|
133
|
+
|
|
134
|
+
print()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# -----------------------------------------------------------------------------
|
|
138
|
+
async def show_pacs(pacs: PublishedAudioCapabilitiesServiceProxy) -> None:
|
|
139
|
+
print(color('### Published Audio Capabilities Service', 'yellow'))
|
|
140
|
+
|
|
141
|
+
contexts = await pacs.available_audio_contexts.read_value()
|
|
142
|
+
print(color(' Available Audio Contexts:', 'green'), contexts)
|
|
143
|
+
|
|
144
|
+
contexts = await pacs.supported_audio_contexts.read_value()
|
|
145
|
+
print(color(' Supported Audio Contexts:', 'green'), contexts)
|
|
146
|
+
|
|
147
|
+
if pacs.sink_pac:
|
|
148
|
+
pac = await pacs.sink_pac.read_value()
|
|
149
|
+
print(color(' Sink PAC: ', 'green'), pac)
|
|
150
|
+
|
|
151
|
+
if pacs.sink_audio_locations:
|
|
152
|
+
audio_locations = await pacs.sink_audio_locations.read_value()
|
|
153
|
+
print(color(' Sink Audio Locations: ', 'green'), audio_locations)
|
|
154
|
+
|
|
155
|
+
if pacs.source_pac:
|
|
156
|
+
pac = await pacs.source_pac.read_value()
|
|
157
|
+
print(color(' Source PAC: ', 'green'), pac)
|
|
158
|
+
|
|
159
|
+
if pacs.source_audio_locations:
|
|
160
|
+
audio_locations = await pacs.source_audio_locations.read_value()
|
|
161
|
+
print(color(' Source Audio Locations: ', 'green'), audio_locations)
|
|
133
162
|
|
|
134
163
|
print()
|
|
135
164
|
|
|
136
165
|
|
|
166
|
+
# -----------------------------------------------------------------------------
|
|
167
|
+
async def show_vcs(vcs: VolumeControlServiceProxy) -> None:
|
|
168
|
+
print(color('### Volume Control Service', 'yellow'))
|
|
169
|
+
|
|
170
|
+
volume_state = await vcs.volume_state.read_value()
|
|
171
|
+
print(color(' Volume State:', 'green'), volume_state)
|
|
172
|
+
|
|
173
|
+
volume_flags = await vcs.volume_flags.read_value()
|
|
174
|
+
print(color(' Volume Flags:', 'green'), volume_flags)
|
|
175
|
+
|
|
176
|
+
|
|
137
177
|
# -----------------------------------------------------------------------------
|
|
138
178
|
async def show_device_info(peer, done: Optional[asyncio.Future]) -> None:
|
|
139
179
|
try:
|
|
@@ -161,6 +201,12 @@ async def show_device_info(peer, done: Optional[asyncio.Future]) -> None:
|
|
|
161
201
|
if tmas := peer.create_service_proxy(TelephonyAndMediaAudioServiceProxy):
|
|
162
202
|
await try_show(show_tmas, tmas)
|
|
163
203
|
|
|
204
|
+
if pacs := peer.create_service_proxy(PublishedAudioCapabilitiesServiceProxy):
|
|
205
|
+
await try_show(show_pacs, pacs)
|
|
206
|
+
|
|
207
|
+
if vcs := peer.create_service_proxy(VolumeControlServiceProxy):
|
|
208
|
+
await try_show(show_vcs, vcs)
|
|
209
|
+
|
|
164
210
|
if done is not None:
|
|
165
211
|
done.set_result(None)
|
|
166
212
|
except asyncio.CancelledError:
|
bumble/apps/lea_unicast/app.py
CHANGED
|
@@ -16,23 +16,22 @@
|
|
|
16
16
|
# Imports
|
|
17
17
|
# -----------------------------------------------------------------------------
|
|
18
18
|
from __future__ import annotations
|
|
19
|
+
|
|
19
20
|
import asyncio
|
|
20
21
|
import datetime
|
|
21
|
-
import enum
|
|
22
22
|
import functools
|
|
23
23
|
from importlib import resources
|
|
24
24
|
import json
|
|
25
25
|
import os
|
|
26
26
|
import logging
|
|
27
27
|
import pathlib
|
|
28
|
-
from typing import Optional, List, cast
|
|
29
28
|
import weakref
|
|
30
|
-
import
|
|
29
|
+
import wave
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
import
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
try:
|
|
32
|
+
import lc3 # type: ignore # pylint: disable=E0401
|
|
33
|
+
except ImportError as e:
|
|
34
|
+
raise ImportError("Try `python -m pip install \".[lc3]\"`.") from e
|
|
36
35
|
|
|
37
36
|
import click
|
|
38
37
|
import aiohttp.web
|
|
@@ -40,11 +39,12 @@ import aiohttp.web
|
|
|
40
39
|
import bumble
|
|
41
40
|
from bumble.core import AdvertisingData
|
|
42
41
|
from bumble.colors import color
|
|
43
|
-
from bumble.device import Device, DeviceConfiguration, AdvertisingParameters
|
|
42
|
+
from bumble.device import Device, DeviceConfiguration, AdvertisingParameters, CisLink
|
|
44
43
|
from bumble.transport import open_transport
|
|
45
44
|
from bumble.profiles import ascs, bap, pacs
|
|
46
45
|
from bumble.hci import Address, CodecID, CodingFormat, HCI_IsoDataPacket
|
|
47
46
|
|
|
47
|
+
|
|
48
48
|
# -----------------------------------------------------------------------------
|
|
49
49
|
# Logging
|
|
50
50
|
# -----------------------------------------------------------------------------
|
|
@@ -54,6 +54,7 @@ logger = logging.getLogger(__name__)
|
|
|
54
54
|
# Constants
|
|
55
55
|
# -----------------------------------------------------------------------------
|
|
56
56
|
DEFAULT_UI_PORT = 7654
|
|
57
|
+
DEFAULT_PCM_BYTES_PER_SAMPLE = 2
|
|
57
58
|
|
|
58
59
|
|
|
59
60
|
def _sink_pac_record() -> pacs.PacRecord:
|
|
@@ -100,153 +101,8 @@ def _source_pac_record() -> pacs.PacRecord:
|
|
|
100
101
|
)
|
|
101
102
|
|
|
102
103
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
# -----------------------------------------------------------------------------
|
|
106
|
-
store = wasmtime.loader.store
|
|
107
|
-
_memory = cast(wasmtime.Memory, liblc3.memory)
|
|
108
|
-
STACK_POINTER = _memory.data_len(store)
|
|
109
|
-
_memory.grow(store, 1)
|
|
110
|
-
# Mapping wasmtime memory to linear address
|
|
111
|
-
memory = (ctypes.c_ubyte * _memory.data_len(store)).from_address(
|
|
112
|
-
ctypes.addressof(_memory.data_ptr(store).contents) # type: ignore
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
class Liblc3PcmFormat(enum.IntEnum):
|
|
117
|
-
S16 = 0
|
|
118
|
-
S24 = 1
|
|
119
|
-
S24_3LE = 2
|
|
120
|
-
FLOAT = 3
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
MAX_DECODER_SIZE = liblc3.lc3_decoder_size(10000, 48000)
|
|
124
|
-
MAX_ENCODER_SIZE = liblc3.lc3_encoder_size(10000, 48000)
|
|
125
|
-
|
|
126
|
-
DECODER_STACK_POINTER = STACK_POINTER
|
|
127
|
-
ENCODER_STACK_POINTER = DECODER_STACK_POINTER + MAX_DECODER_SIZE * 2
|
|
128
|
-
DECODE_BUFFER_STACK_POINTER = ENCODER_STACK_POINTER + MAX_ENCODER_SIZE * 2
|
|
129
|
-
ENCODE_BUFFER_STACK_POINTER = DECODE_BUFFER_STACK_POINTER + 8192
|
|
130
|
-
DEFAULT_PCM_SAMPLE_RATE = 48000
|
|
131
|
-
DEFAULT_PCM_FORMAT = Liblc3PcmFormat.S16
|
|
132
|
-
DEFAULT_PCM_BYTES_PER_SAMPLE = 2
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
encoders: List[int] = []
|
|
136
|
-
decoders: List[int] = []
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
def setup_encoders(
|
|
140
|
-
sample_rate_hz: int, frame_duration_us: int, num_channels: int
|
|
141
|
-
) -> None:
|
|
142
|
-
logger.info(
|
|
143
|
-
f"setup_encoders {sample_rate_hz}Hz {frame_duration_us}us {num_channels}channels"
|
|
144
|
-
)
|
|
145
|
-
encoders[:num_channels] = [
|
|
146
|
-
liblc3.lc3_setup_encoder(
|
|
147
|
-
frame_duration_us,
|
|
148
|
-
sample_rate_hz,
|
|
149
|
-
DEFAULT_PCM_SAMPLE_RATE, # Input sample rate
|
|
150
|
-
ENCODER_STACK_POINTER + MAX_ENCODER_SIZE * i,
|
|
151
|
-
)
|
|
152
|
-
for i in range(num_channels)
|
|
153
|
-
]
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
def setup_decoders(
|
|
157
|
-
sample_rate_hz: int, frame_duration_us: int, num_channels: int
|
|
158
|
-
) -> None:
|
|
159
|
-
logger.info(
|
|
160
|
-
f"setup_decoders {sample_rate_hz}Hz {frame_duration_us}us {num_channels}channels"
|
|
161
|
-
)
|
|
162
|
-
decoders[:num_channels] = [
|
|
163
|
-
liblc3.lc3_setup_decoder(
|
|
164
|
-
frame_duration_us,
|
|
165
|
-
sample_rate_hz,
|
|
166
|
-
DEFAULT_PCM_SAMPLE_RATE, # Output sample rate
|
|
167
|
-
DECODER_STACK_POINTER + MAX_DECODER_SIZE * i,
|
|
168
|
-
)
|
|
169
|
-
for i in range(num_channels)
|
|
170
|
-
]
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
def decode(
|
|
174
|
-
frame_duration_us: int,
|
|
175
|
-
num_channels: int,
|
|
176
|
-
input_bytes: bytes,
|
|
177
|
-
) -> bytes:
|
|
178
|
-
if not input_bytes:
|
|
179
|
-
return b''
|
|
180
|
-
|
|
181
|
-
input_buffer_offset = DECODE_BUFFER_STACK_POINTER
|
|
182
|
-
input_buffer_size = len(input_bytes)
|
|
183
|
-
input_bytes_per_frame = input_buffer_size // num_channels
|
|
184
|
-
|
|
185
|
-
# Copy into wasm
|
|
186
|
-
memory[input_buffer_offset : input_buffer_offset + input_buffer_size] = input_bytes # type: ignore
|
|
187
|
-
|
|
188
|
-
output_buffer_offset = input_buffer_offset + input_buffer_size
|
|
189
|
-
output_buffer_size = (
|
|
190
|
-
liblc3.lc3_frame_samples(frame_duration_us, DEFAULT_PCM_SAMPLE_RATE)
|
|
191
|
-
* DEFAULT_PCM_BYTES_PER_SAMPLE
|
|
192
|
-
* num_channels
|
|
193
|
-
)
|
|
194
|
-
|
|
195
|
-
for i in range(num_channels):
|
|
196
|
-
res = liblc3.lc3_decode(
|
|
197
|
-
decoders[i],
|
|
198
|
-
input_buffer_offset + input_bytes_per_frame * i,
|
|
199
|
-
input_bytes_per_frame,
|
|
200
|
-
DEFAULT_PCM_FORMAT,
|
|
201
|
-
output_buffer_offset + i * DEFAULT_PCM_BYTES_PER_SAMPLE,
|
|
202
|
-
num_channels, # Stride
|
|
203
|
-
)
|
|
204
|
-
|
|
205
|
-
if res != 0:
|
|
206
|
-
logging.error(f"Parsing failed, res={res}")
|
|
207
|
-
|
|
208
|
-
# Extract decoded data from the output buffer
|
|
209
|
-
return bytes(
|
|
210
|
-
memory[output_buffer_offset : output_buffer_offset + output_buffer_size]
|
|
211
|
-
)
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
def encode(
|
|
215
|
-
sdu_length: int,
|
|
216
|
-
num_channels: int,
|
|
217
|
-
stride: int,
|
|
218
|
-
input_bytes: bytes,
|
|
219
|
-
) -> bytes:
|
|
220
|
-
if not input_bytes:
|
|
221
|
-
return b''
|
|
222
|
-
|
|
223
|
-
input_buffer_offset = ENCODE_BUFFER_STACK_POINTER
|
|
224
|
-
input_buffer_size = len(input_bytes)
|
|
225
|
-
|
|
226
|
-
# Copy into wasm
|
|
227
|
-
memory[input_buffer_offset : input_buffer_offset + input_buffer_size] = input_bytes # type: ignore
|
|
228
|
-
|
|
229
|
-
output_buffer_offset = input_buffer_offset + input_buffer_size
|
|
230
|
-
output_buffer_size = sdu_length
|
|
231
|
-
output_frame_size = output_buffer_size // num_channels
|
|
232
|
-
|
|
233
|
-
for i in range(num_channels):
|
|
234
|
-
res = liblc3.lc3_encode(
|
|
235
|
-
encoders[i],
|
|
236
|
-
DEFAULT_PCM_FORMAT,
|
|
237
|
-
input_buffer_offset + DEFAULT_PCM_BYTES_PER_SAMPLE * i,
|
|
238
|
-
stride,
|
|
239
|
-
output_frame_size,
|
|
240
|
-
output_buffer_offset + output_frame_size * i,
|
|
241
|
-
)
|
|
242
|
-
|
|
243
|
-
if res != 0:
|
|
244
|
-
logging.error(f"Parsing failed, res={res}")
|
|
245
|
-
|
|
246
|
-
# Extract decoded data from the output buffer
|
|
247
|
-
return bytes(
|
|
248
|
-
memory[output_buffer_offset : output_buffer_offset + output_buffer_size]
|
|
249
|
-
)
|
|
104
|
+
decoder: lc3.Decoder | None = None
|
|
105
|
+
encoding_config: bap.CodecSpecificConfiguration | None = None
|
|
250
106
|
|
|
251
107
|
|
|
252
108
|
async def lc3_source_task(
|
|
@@ -254,44 +110,49 @@ async def lc3_source_task(
|
|
|
254
110
|
sdu_length: int,
|
|
255
111
|
frame_duration_us: int,
|
|
256
112
|
device: Device,
|
|
257
|
-
|
|
113
|
+
cis_link: CisLink,
|
|
258
114
|
) -> None:
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
assert bits_per_sample == DEFAULT_PCM_BYTES_PER_SAMPLE * 8
|
|
115
|
+
logger.info(
|
|
116
|
+
"lc3_source_task filename=%s, sdu_length=%d, frame_duration=%.1f",
|
|
117
|
+
filename,
|
|
118
|
+
sdu_length,
|
|
119
|
+
frame_duration_us / 1000,
|
|
120
|
+
)
|
|
121
|
+
with wave.open(filename, 'rb') as wav:
|
|
122
|
+
bits_per_sample = wav.getsampwidth() * 8
|
|
268
123
|
|
|
269
|
-
|
|
270
|
-
liblc3.lc3_frame_samples(frame_duration_us, DEFAULT_PCM_SAMPLE_RATE)
|
|
271
|
-
* DEFAULT_PCM_BYTES_PER_SAMPLE
|
|
272
|
-
)
|
|
273
|
-
packet_sequence_number = 0
|
|
124
|
+
encoder: lc3.Encoder | None = None
|
|
274
125
|
|
|
275
126
|
while True:
|
|
276
127
|
next_round = datetime.datetime.now() + datetime.timedelta(
|
|
277
128
|
microseconds=frame_duration_us
|
|
278
129
|
)
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
130
|
+
if not encoder:
|
|
131
|
+
if (
|
|
132
|
+
encoding_config
|
|
133
|
+
and (frame_duration := encoding_config.frame_duration)
|
|
134
|
+
and (sampling_frequency := encoding_config.sampling_frequency)
|
|
135
|
+
and (
|
|
136
|
+
audio_channel_allocation := encoding_config.audio_channel_allocation
|
|
137
|
+
)
|
|
138
|
+
):
|
|
139
|
+
logger.info("Use %s", encoding_config)
|
|
140
|
+
encoder = lc3.Encoder(
|
|
141
|
+
frame_duration_us=frame_duration.us,
|
|
142
|
+
sample_rate_hz=sampling_frequency.hz,
|
|
143
|
+
num_channels=audio_channel_allocation.channel_count,
|
|
144
|
+
input_sample_rate_hz=wav.getframerate(),
|
|
145
|
+
)
|
|
146
|
+
else:
|
|
147
|
+
sdu = encoder.encode(
|
|
148
|
+
pcm=wav.readframes(encoder.get_frame_samples()),
|
|
149
|
+
num_bytes=sdu_length,
|
|
150
|
+
bit_depth=bits_per_sample,
|
|
151
|
+
)
|
|
152
|
+
cis_link.write(sdu)
|
|
153
|
+
|
|
293
154
|
sleep_time = next_round - datetime.datetime.now()
|
|
294
|
-
await asyncio.sleep(sleep_time.total_seconds())
|
|
155
|
+
await asyncio.sleep(sleep_time.total_seconds() * 0.9)
|
|
295
156
|
|
|
296
157
|
|
|
297
158
|
# -----------------------------------------------------------------------------
|
|
@@ -410,7 +271,7 @@ class Speaker:
|
|
|
410
271
|
|
|
411
272
|
def __init__(
|
|
412
273
|
self,
|
|
413
|
-
device_config_path:
|
|
274
|
+
device_config_path: str | None,
|
|
414
275
|
ui_port: int,
|
|
415
276
|
transport: str,
|
|
416
277
|
lc3_input_file_path: str,
|
|
@@ -437,6 +298,7 @@ class Speaker:
|
|
|
437
298
|
advertising_interval_min=25,
|
|
438
299
|
advertising_interval_max=25,
|
|
439
300
|
address=Address('F1:F2:F3:F4:F5:F6'),
|
|
301
|
+
identity_address_type=Address.RANDOM_DEVICE_ADDRESS,
|
|
440
302
|
)
|
|
441
303
|
|
|
442
304
|
device_config.le_enabled = True
|
|
@@ -490,12 +352,12 @@ class Speaker:
|
|
|
490
352
|
not isinstance(codec_config, bap.CodecSpecificConfiguration)
|
|
491
353
|
or codec_config.frame_duration is None
|
|
492
354
|
or codec_config.audio_channel_allocation is None
|
|
355
|
+
or decoder is None
|
|
356
|
+
or not pdu.iso_sdu_fragment
|
|
493
357
|
):
|
|
494
358
|
return
|
|
495
|
-
pcm = decode(
|
|
496
|
-
|
|
497
|
-
codec_config.audio_channel_allocation.channel_count,
|
|
498
|
-
pdu.iso_sdu_fragment,
|
|
359
|
+
pcm = decoder.decode(
|
|
360
|
+
pdu.iso_sdu_fragment, bit_depth=DEFAULT_PCM_BYTES_PER_SAMPLE * 8
|
|
499
361
|
)
|
|
500
362
|
self.device.abort_on('disconnection', self.ui_server.send_audio(pcm))
|
|
501
363
|
|
|
@@ -521,7 +383,7 @@ class Speaker:
|
|
|
521
383
|
),
|
|
522
384
|
frame_duration_us=codec_config.frame_duration.us,
|
|
523
385
|
device=self.device,
|
|
524
|
-
|
|
386
|
+
cis_link=ase.cis_link,
|
|
525
387
|
),
|
|
526
388
|
)
|
|
527
389
|
else:
|
|
@@ -537,16 +399,14 @@ class Speaker:
|
|
|
537
399
|
):
|
|
538
400
|
return
|
|
539
401
|
if ase.role == ascs.AudioRole.SOURCE:
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
codec_config.frame_duration.us,
|
|
543
|
-
codec_config.audio_channel_allocation.channel_count,
|
|
544
|
-
)
|
|
402
|
+
global encoding_config
|
|
403
|
+
encoding_config = codec_config
|
|
545
404
|
else:
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
codec_config.frame_duration.us,
|
|
549
|
-
codec_config.
|
|
405
|
+
global decoder
|
|
406
|
+
decoder = lc3.Decoder(
|
|
407
|
+
frame_duration_us=codec_config.frame_duration.us,
|
|
408
|
+
sample_rate_hz=codec_config.sampling_frequency.hz,
|
|
409
|
+
num_channels=codec_config.audio_channel_allocation.channel_count,
|
|
550
410
|
)
|
|
551
411
|
|
|
552
412
|
for ase in ascs_service.ase_state_machines.values():
|
|
@@ -585,7 +445,7 @@ def speaker(ui_port: int, device_config: str, transport: str, lc3_file: str) ->
|
|
|
585
445
|
|
|
586
446
|
# -----------------------------------------------------------------------------
|
|
587
447
|
def main():
|
|
588
|
-
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', '
|
|
448
|
+
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
|
589
449
|
speaker()
|
|
590
450
|
|
|
591
451
|
|
bumble/apps/pair.py
CHANGED
|
@@ -373,7 +373,9 @@ async def pair(
|
|
|
373
373
|
shared_data = (
|
|
374
374
|
None
|
|
375
375
|
if oob == '-'
|
|
376
|
-
else OobData.from_ad(
|
|
376
|
+
else OobData.from_ad(
|
|
377
|
+
AdvertisingData.from_bytes(bytes.fromhex(oob))
|
|
378
|
+
).shared_data
|
|
377
379
|
)
|
|
378
380
|
legacy_context = OobLegacyContext()
|
|
379
381
|
oob_contexts = PairingConfig.OobConfig(
|
|
@@ -381,16 +383,19 @@ async def pair(
|
|
|
381
383
|
peer_data=shared_data,
|
|
382
384
|
legacy_context=legacy_context,
|
|
383
385
|
)
|
|
384
|
-
oob_data = OobData(
|
|
385
|
-
address=device.random_address,
|
|
386
|
-
shared_data=shared_data,
|
|
387
|
-
legacy_context=legacy_context,
|
|
388
|
-
)
|
|
389
386
|
print(color('@@@-----------------------------------', 'yellow'))
|
|
390
387
|
print(color('@@@ OOB Data:', 'yellow'))
|
|
391
|
-
|
|
388
|
+
if shared_data is None:
|
|
389
|
+
oob_data = OobData(
|
|
390
|
+
address=device.random_address, shared_data=our_oob_context.share()
|
|
391
|
+
)
|
|
392
|
+
print(
|
|
393
|
+
color(
|
|
394
|
+
f'@@@ SHARE: {bytes(oob_data.to_ad()).hex()}',
|
|
395
|
+
'yellow',
|
|
396
|
+
)
|
|
397
|
+
)
|
|
392
398
|
print(color(f'@@@ TK={legacy_context.tk.hex()}', 'yellow'))
|
|
393
|
-
print(color(f'@@@ HEX: ({bytes(oob_data.to_ad()).hex()})', 'yellow'))
|
|
394
399
|
print(color('@@@-----------------------------------', 'yellow'))
|
|
395
400
|
else:
|
|
396
401
|
oob_contexts = None
|
bumble/apps/show.py
CHANGED
|
@@ -144,18 +144,18 @@ class Printer:
|
|
|
144
144
|
help='Format of the input file',
|
|
145
145
|
)
|
|
146
146
|
@click.option(
|
|
147
|
-
'--
|
|
147
|
+
'--vendor',
|
|
148
148
|
type=click.Choice(['android', 'zephyr']),
|
|
149
149
|
multiple=True,
|
|
150
150
|
help='Support vendor-specific commands (list one or more)',
|
|
151
151
|
)
|
|
152
152
|
@click.argument('filename')
|
|
153
153
|
# pylint: disable=redefined-builtin
|
|
154
|
-
def main(format,
|
|
155
|
-
for
|
|
156
|
-
if
|
|
154
|
+
def main(format, vendor, filename):
|
|
155
|
+
for vendor_name in vendor:
|
|
156
|
+
if vendor_name == 'android':
|
|
157
157
|
import bumble.vendor.android.hci
|
|
158
|
-
elif
|
|
158
|
+
elif vendor_name == 'zephyr':
|
|
159
159
|
import bumble.vendor.zephyr.hci
|
|
160
160
|
|
|
161
161
|
input = open(filename, 'rb')
|
|
@@ -180,7 +180,7 @@ def main(format, vendors, filename):
|
|
|
180
180
|
else:
|
|
181
181
|
printer.print(color("[TRUNCATED]", "red"))
|
|
182
182
|
except Exception as error:
|
|
183
|
-
logger.exception()
|
|
183
|
+
logger.exception('')
|
|
184
184
|
print(color(f'!!! {error}', 'red'))
|
|
185
185
|
|
|
186
186
|
|
bumble/att.py
CHANGED
|
@@ -57,6 +57,7 @@ if TYPE_CHECKING:
|
|
|
57
57
|
# pylint: disable=line-too-long
|
|
58
58
|
|
|
59
59
|
ATT_CID = 0x04
|
|
60
|
+
ATT_PSM = 0x001F
|
|
60
61
|
|
|
61
62
|
ATT_ERROR_RESPONSE = 0x01
|
|
62
63
|
ATT_EXCHANGE_MTU_REQUEST = 0x02
|
|
@@ -756,13 +757,13 @@ class AttributeValue:
|
|
|
756
757
|
def __init__(
|
|
757
758
|
self,
|
|
758
759
|
read: Union[
|
|
759
|
-
Callable[[Optional[Connection]],
|
|
760
|
-
Callable[[Optional[Connection]], Awaitable[
|
|
760
|
+
Callable[[Optional[Connection]], Any],
|
|
761
|
+
Callable[[Optional[Connection]], Awaitable[Any]],
|
|
761
762
|
None,
|
|
762
763
|
] = None,
|
|
763
764
|
write: Union[
|
|
764
|
-
Callable[[Optional[Connection],
|
|
765
|
-
Callable[[Optional[Connection],
|
|
765
|
+
Callable[[Optional[Connection], Any], None],
|
|
766
|
+
Callable[[Optional[Connection], Any], Awaitable[None]],
|
|
766
767
|
None,
|
|
767
768
|
] = None,
|
|
768
769
|
):
|
|
@@ -821,13 +822,13 @@ class Attribute(EventEmitter):
|
|
|
821
822
|
READ_REQUIRES_AUTHORIZATION = Permissions.READ_REQUIRES_AUTHORIZATION
|
|
822
823
|
WRITE_REQUIRES_AUTHORIZATION = Permissions.WRITE_REQUIRES_AUTHORIZATION
|
|
823
824
|
|
|
824
|
-
value:
|
|
825
|
+
value: Any
|
|
825
826
|
|
|
826
827
|
def __init__(
|
|
827
828
|
self,
|
|
828
829
|
attribute_type: Union[str, bytes, UUID],
|
|
829
830
|
permissions: Union[str, Attribute.Permissions],
|
|
830
|
-
value:
|
|
831
|
+
value: Any = b'',
|
|
831
832
|
) -> None:
|
|
832
833
|
EventEmitter.__init__(self)
|
|
833
834
|
self.handle = 0
|
|
@@ -845,11 +846,7 @@ class Attribute(EventEmitter):
|
|
|
845
846
|
else:
|
|
846
847
|
self.type = attribute_type
|
|
847
848
|
|
|
848
|
-
|
|
849
|
-
if isinstance(value, str):
|
|
850
|
-
self.value = bytes(value, 'utf-8')
|
|
851
|
-
else:
|
|
852
|
-
self.value = value
|
|
849
|
+
self.value = value
|
|
853
850
|
|
|
854
851
|
def encode_value(self, value: Any) -> bytes:
|
|
855
852
|
return value
|
|
@@ -892,6 +889,8 @@ class Attribute(EventEmitter):
|
|
|
892
889
|
else:
|
|
893
890
|
value = self.value
|
|
894
891
|
|
|
892
|
+
self.emit('read', connection, value)
|
|
893
|
+
|
|
895
894
|
return self.encode_value(value)
|
|
896
895
|
|
|
897
896
|
async def write_value(self, connection: Connection, value_bytes: bytes) -> None:
|
bumble/audio/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Copyright 2025 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
|
+
# -----------------------------------------------------------------------------
|