bumble 0.0.213__py3-none-any.whl → 0.0.214__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 +3 -2
- bumble/apps/bench.py +2 -6
- bumble/apps/controller_info.py +2 -7
- bumble/apps/controller_loopback.py +5 -9
- bumble/apps/controllers.py +2 -3
- bumble/apps/device_info.py +2 -3
- bumble/apps/gatt_dump.py +3 -3
- bumble/apps/gg_bridge.py +3 -3
- bumble/apps/hci_bridge.py +3 -2
- bumble/apps/l2cap_bridge.py +3 -3
- bumble/apps/lea_unicast/app.py +2 -2
- bumble/apps/player/player.py +2 -3
- bumble/apps/rfcomm_bridge.py +2 -3
- bumble/apps/scan.py +2 -3
- bumble/apps/show.py +2 -2
- bumble/apps/speaker/speaker.py +2 -6
- bumble/apps/unbond.py +2 -3
- bumble/apps/usb_probe.py +2 -3
- bumble/avrcp.py +1 -1
- bumble/controller.py +90 -22
- bumble/device.py +55 -5
- bumble/hci.py +47 -0
- bumble/host.py +10 -0
- bumble/l2cap.py +17 -30
- bumble/link.py +10 -10
- bumble/logging.py +65 -0
- bumble/pandora/__init__.py +1 -1
- bumble/profiles/ams.py +404 -0
- bumble/profiles/ascs.py +10 -0
- bumble/smp.py +9 -6
- bumble/tools/intel_util.py +3 -2
- bumble/tools/rtk_util.py +4 -3
- {bumble-0.0.213.dist-info → bumble-0.0.214.dist-info}/METADATA +3 -2
- {bumble-0.0.213.dist-info → bumble-0.0.214.dist-info}/RECORD +39 -37
- {bumble-0.0.213.dist-info → bumble-0.0.214.dist-info}/WHEEL +0 -0
- {bumble-0.0.213.dist-info → bumble-0.0.214.dist-info}/entry_points.txt +0 -0
- {bumble-0.0.213.dist-info → bumble-0.0.214.dist-info}/licenses/LICENSE +0 -0
- {bumble-0.0.213.dist-info → bumble-0.0.214.dist-info}/top_level.txt +0 -0
bumble/profiles/ams.py
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
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
|
+
Apple Media Service (AMS).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
# -----------------------------------------------------------------------------
|
|
20
|
+
# Imports
|
|
21
|
+
# -----------------------------------------------------------------------------
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
import asyncio
|
|
24
|
+
import dataclasses
|
|
25
|
+
import enum
|
|
26
|
+
import logging
|
|
27
|
+
from typing import Optional, Iterable, Union
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
from bumble.device import Peer
|
|
31
|
+
from bumble.gatt import (
|
|
32
|
+
Characteristic,
|
|
33
|
+
GATT_AMS_SERVICE,
|
|
34
|
+
GATT_AMS_REMOTE_COMMAND_CHARACTERISTIC,
|
|
35
|
+
GATT_AMS_ENTITY_UPDATE_CHARACTERISTIC,
|
|
36
|
+
GATT_AMS_ENTITY_ATTRIBUTE_CHARACTERISTIC,
|
|
37
|
+
TemplateService,
|
|
38
|
+
)
|
|
39
|
+
from bumble.gatt_client import CharacteristicProxy, ProfileServiceProxy, ServiceProxy
|
|
40
|
+
from bumble import utils
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# -----------------------------------------------------------------------------
|
|
44
|
+
# Logging
|
|
45
|
+
# -----------------------------------------------------------------------------
|
|
46
|
+
logger = logging.getLogger(__name__)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# -----------------------------------------------------------------------------
|
|
50
|
+
# Protocol
|
|
51
|
+
# -----------------------------------------------------------------------------
|
|
52
|
+
class RemoteCommandId(utils.OpenIntEnum):
|
|
53
|
+
PLAY = 0
|
|
54
|
+
PAUSE = 1
|
|
55
|
+
TOGGLE_PLAY_PAUSE = 2
|
|
56
|
+
NEXT_TRACK = 3
|
|
57
|
+
PREVIOUS_TRACK = 4
|
|
58
|
+
VOLUME_UP = 5
|
|
59
|
+
VOLUME_DOWN = 6
|
|
60
|
+
ADVANCE_REPEAT_MODE = 7
|
|
61
|
+
ADVANCE_SHUFFLE_MODE = 8
|
|
62
|
+
SKIP_FORWARD = 9
|
|
63
|
+
SKIP_BACKWARD = 10
|
|
64
|
+
LIKE_TRACK = 11
|
|
65
|
+
DISLIKE_TRACK = 12
|
|
66
|
+
BOOKMARK_TRACK = 13
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class EntityId(utils.OpenIntEnum):
|
|
70
|
+
PLAYER = 0
|
|
71
|
+
QUEUE = 1
|
|
72
|
+
TRACK = 2
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ActionId(utils.OpenIntEnum):
|
|
76
|
+
POSITIVE = 0
|
|
77
|
+
NEGATIVE = 1
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class EntityUpdateFlags(enum.IntFlag):
|
|
81
|
+
TRUNCATED = 1
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class PlayerAttributeId(utils.OpenIntEnum):
|
|
85
|
+
NAME = 0
|
|
86
|
+
PLAYBACK_INFO = 1
|
|
87
|
+
VOLUME = 2
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class QueueAttributeId(utils.OpenIntEnum):
|
|
91
|
+
INDEX = 0
|
|
92
|
+
COUNT = 1
|
|
93
|
+
SHUFFLE_MODE = 2
|
|
94
|
+
REPEAT_MODE = 3
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class ShuffleMode(utils.OpenIntEnum):
|
|
98
|
+
OFF = 0
|
|
99
|
+
ONE = 1
|
|
100
|
+
ALL = 2
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class RepeatMode(utils.OpenIntEnum):
|
|
104
|
+
OFF = 0
|
|
105
|
+
ONE = 1
|
|
106
|
+
ALL = 2
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class TrackAttributeId(utils.OpenIntEnum):
|
|
110
|
+
ARTIST = 0
|
|
111
|
+
ALBUM = 1
|
|
112
|
+
TITLE = 2
|
|
113
|
+
DURATION = 3
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class PlaybackState(utils.OpenIntEnum):
|
|
117
|
+
PAUSED = 0
|
|
118
|
+
PLAYING = 1
|
|
119
|
+
REWINDING = 2
|
|
120
|
+
FAST_FORWARDING = 3
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclasses.dataclass
|
|
124
|
+
class PlaybackInfo:
|
|
125
|
+
playback_state: PlaybackState = PlaybackState.PAUSED
|
|
126
|
+
playback_rate: float = 1.0
|
|
127
|
+
elapsed_time: float = 0.0
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# -----------------------------------------------------------------------------
|
|
131
|
+
# GATT Server-side
|
|
132
|
+
# -----------------------------------------------------------------------------
|
|
133
|
+
class Ams(TemplateService):
|
|
134
|
+
UUID = GATT_AMS_SERVICE
|
|
135
|
+
|
|
136
|
+
remote_command_characteristic: Characteristic
|
|
137
|
+
entity_update_characteristic: Characteristic
|
|
138
|
+
entity_attribute_characteristic: Characteristic
|
|
139
|
+
|
|
140
|
+
def __init__(self) -> None:
|
|
141
|
+
# TODO not the final implementation
|
|
142
|
+
self.remote_command_characteristic = Characteristic(
|
|
143
|
+
GATT_AMS_REMOTE_COMMAND_CHARACTERISTIC,
|
|
144
|
+
Characteristic.Properties.NOTIFY
|
|
145
|
+
| Characteristic.Properties.WRITE_WITHOUT_RESPONSE,
|
|
146
|
+
Characteristic.Permissions.WRITEABLE,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# TODO not the final implementation
|
|
150
|
+
self.entity_update_characteristic = Characteristic(
|
|
151
|
+
GATT_AMS_ENTITY_UPDATE_CHARACTERISTIC,
|
|
152
|
+
Characteristic.Properties.NOTIFY | Characteristic.Properties.WRITE,
|
|
153
|
+
Characteristic.Permissions.WRITEABLE,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# TODO not the final implementation
|
|
157
|
+
self.entity_attribute_characteristic = Characteristic(
|
|
158
|
+
GATT_AMS_ENTITY_ATTRIBUTE_CHARACTERISTIC,
|
|
159
|
+
Characteristic.Properties.READ
|
|
160
|
+
| Characteristic.Properties.WRITE_WITHOUT_RESPONSE,
|
|
161
|
+
Characteristic.Permissions.WRITEABLE | Characteristic.Permissions.READABLE,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
super().__init__(
|
|
165
|
+
[
|
|
166
|
+
self.remote_command_characteristic,
|
|
167
|
+
self.entity_update_characteristic,
|
|
168
|
+
self.entity_attribute_characteristic,
|
|
169
|
+
]
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# -----------------------------------------------------------------------------
|
|
174
|
+
# GATT Client-side
|
|
175
|
+
# -----------------------------------------------------------------------------
|
|
176
|
+
class AmsProxy(ProfileServiceProxy):
|
|
177
|
+
SERVICE_CLASS = Ams
|
|
178
|
+
|
|
179
|
+
# NOTE: these don't use adapters, because the format for write and notifications
|
|
180
|
+
# are different.
|
|
181
|
+
remote_command: CharacteristicProxy[bytes]
|
|
182
|
+
entity_update: CharacteristicProxy[bytes]
|
|
183
|
+
entity_attribute: CharacteristicProxy[bytes]
|
|
184
|
+
|
|
185
|
+
def __init__(self, service_proxy: ServiceProxy):
|
|
186
|
+
self.remote_command = service_proxy.get_required_characteristic_by_uuid(
|
|
187
|
+
GATT_AMS_REMOTE_COMMAND_CHARACTERISTIC
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
self.entity_update = service_proxy.get_required_characteristic_by_uuid(
|
|
191
|
+
GATT_AMS_ENTITY_UPDATE_CHARACTERISTIC
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
self.entity_attribute = service_proxy.get_required_characteristic_by_uuid(
|
|
195
|
+
GATT_AMS_ENTITY_ATTRIBUTE_CHARACTERISTIC
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class AmsClient(utils.EventEmitter):
|
|
200
|
+
EVENT_SUPPORTED_COMMANDS = "supported_commands"
|
|
201
|
+
EVENT_PLAYER_NAME = "player_name"
|
|
202
|
+
EVENT_PLAYER_PLAYBACK_INFO = "player_playback_info"
|
|
203
|
+
EVENT_PLAYER_VOLUME = "player_volume"
|
|
204
|
+
EVENT_QUEUE_COUNT = "queue_count"
|
|
205
|
+
EVENT_QUEUE_INDEX = "queue_index"
|
|
206
|
+
EVENT_QUEUE_SHUFFLE_MODE = "queue_shuffle_mode"
|
|
207
|
+
EVENT_QUEUE_REPEAT_MODE = "queue_repeat_mode"
|
|
208
|
+
EVENT_TRACK_ARTIST = "track_artist"
|
|
209
|
+
EVENT_TRACK_ALBUM = "track_album"
|
|
210
|
+
EVENT_TRACK_TITLE = "track_title"
|
|
211
|
+
EVENT_TRACK_DURATION = "track_duration"
|
|
212
|
+
|
|
213
|
+
supported_commands: set[RemoteCommandId]
|
|
214
|
+
player_name: str = ""
|
|
215
|
+
player_playback_info: PlaybackInfo = PlaybackInfo(PlaybackState.PAUSED, 0.0, 0.0)
|
|
216
|
+
player_volume: float = 1.0
|
|
217
|
+
queue_count: int = 0
|
|
218
|
+
queue_index: int = 0
|
|
219
|
+
queue_shuffle_mode: ShuffleMode = ShuffleMode.OFF
|
|
220
|
+
queue_repeat_mode: RepeatMode = RepeatMode.OFF
|
|
221
|
+
track_artist: str = ""
|
|
222
|
+
track_album: str = ""
|
|
223
|
+
track_title: str = ""
|
|
224
|
+
track_duration: float = 0.0
|
|
225
|
+
|
|
226
|
+
def __init__(self, ams_proxy: AmsProxy) -> None:
|
|
227
|
+
super().__init__()
|
|
228
|
+
self._ams_proxy = ams_proxy
|
|
229
|
+
self._started = False
|
|
230
|
+
self._read_attribute_semaphore = asyncio.Semaphore()
|
|
231
|
+
self.supported_commands = set()
|
|
232
|
+
|
|
233
|
+
@classmethod
|
|
234
|
+
async def for_peer(cls, peer: Peer) -> Optional[AmsClient]:
|
|
235
|
+
ams_proxy = await peer.discover_service_and_create_proxy(AmsProxy)
|
|
236
|
+
if ams_proxy is None:
|
|
237
|
+
return None
|
|
238
|
+
return cls(ams_proxy)
|
|
239
|
+
|
|
240
|
+
async def start(self) -> None:
|
|
241
|
+
logger.debug("subscribing to remote command characteristic")
|
|
242
|
+
await self._ams_proxy.remote_command.subscribe(
|
|
243
|
+
self._on_remote_command_notification
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
logger.debug("subscribing to entity update characteristic")
|
|
247
|
+
await self._ams_proxy.entity_update.subscribe(
|
|
248
|
+
lambda data: utils.AsyncRunner.spawn(
|
|
249
|
+
self._on_entity_update_notification(data)
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
self._started = True
|
|
254
|
+
|
|
255
|
+
async def stop(self) -> None:
|
|
256
|
+
await self._ams_proxy.remote_command.unsubscribe(
|
|
257
|
+
self._on_remote_command_notification
|
|
258
|
+
)
|
|
259
|
+
await self._ams_proxy.entity_update.unsubscribe(
|
|
260
|
+
self._on_entity_update_notification
|
|
261
|
+
)
|
|
262
|
+
self._started = False
|
|
263
|
+
|
|
264
|
+
async def observe(
|
|
265
|
+
self,
|
|
266
|
+
entity: EntityId,
|
|
267
|
+
attributes: Iterable[
|
|
268
|
+
Union[PlayerAttributeId, QueueAttributeId, TrackAttributeId]
|
|
269
|
+
],
|
|
270
|
+
) -> None:
|
|
271
|
+
await self._ams_proxy.entity_update.write_value(
|
|
272
|
+
bytes([entity] + list(attributes)), with_response=True
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
async def command(self, command: RemoteCommandId) -> None:
|
|
276
|
+
await self._ams_proxy.remote_command.write_value(
|
|
277
|
+
bytes([command]), with_response=True
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
async def play(self) -> None:
|
|
281
|
+
await self.command(RemoteCommandId.PLAY)
|
|
282
|
+
|
|
283
|
+
async def pause(self) -> None:
|
|
284
|
+
await self.command(RemoteCommandId.PAUSE)
|
|
285
|
+
|
|
286
|
+
async def toggle_play_pause(self) -> None:
|
|
287
|
+
await self.command(RemoteCommandId.TOGGLE_PLAY_PAUSE)
|
|
288
|
+
|
|
289
|
+
async def next_track(self) -> None:
|
|
290
|
+
await self.command(RemoteCommandId.NEXT_TRACK)
|
|
291
|
+
|
|
292
|
+
async def previous_track(self) -> None:
|
|
293
|
+
await self.command(RemoteCommandId.PREVIOUS_TRACK)
|
|
294
|
+
|
|
295
|
+
async def volume_up(self) -> None:
|
|
296
|
+
await self.command(RemoteCommandId.VOLUME_UP)
|
|
297
|
+
|
|
298
|
+
async def volume_down(self) -> None:
|
|
299
|
+
await self.command(RemoteCommandId.VOLUME_DOWN)
|
|
300
|
+
|
|
301
|
+
async def advance_repeat_mode(self) -> None:
|
|
302
|
+
await self.command(RemoteCommandId.ADVANCE_REPEAT_MODE)
|
|
303
|
+
|
|
304
|
+
async def advance_shuffle_mode(self) -> None:
|
|
305
|
+
await self.command(RemoteCommandId.ADVANCE_SHUFFLE_MODE)
|
|
306
|
+
|
|
307
|
+
async def skip_forward(self) -> None:
|
|
308
|
+
await self.command(RemoteCommandId.SKIP_FORWARD)
|
|
309
|
+
|
|
310
|
+
async def skip_backward(self) -> None:
|
|
311
|
+
await self.command(RemoteCommandId.SKIP_BACKWARD)
|
|
312
|
+
|
|
313
|
+
async def like_track(self) -> None:
|
|
314
|
+
await self.command(RemoteCommandId.LIKE_TRACK)
|
|
315
|
+
|
|
316
|
+
async def dislike_track(self) -> None:
|
|
317
|
+
await self.command(RemoteCommandId.DISLIKE_TRACK)
|
|
318
|
+
|
|
319
|
+
async def bookmark_track(self) -> None:
|
|
320
|
+
await self.command(RemoteCommandId.BOOKMARK_TRACK)
|
|
321
|
+
|
|
322
|
+
def _on_remote_command_notification(self, data: bytes) -> None:
|
|
323
|
+
supported_commands = [RemoteCommandId(command) for command in data]
|
|
324
|
+
logger.debug(
|
|
325
|
+
f"supported commands: {[command.name for command in supported_commands]}"
|
|
326
|
+
)
|
|
327
|
+
for command in supported_commands:
|
|
328
|
+
self.supported_commands.add(command)
|
|
329
|
+
|
|
330
|
+
self.emit(self.EVENT_SUPPORTED_COMMANDS)
|
|
331
|
+
|
|
332
|
+
async def _on_entity_update_notification(self, data: bytes) -> None:
|
|
333
|
+
entity = EntityId(data[0])
|
|
334
|
+
flags = EntityUpdateFlags(data[2])
|
|
335
|
+
value = data[3:]
|
|
336
|
+
|
|
337
|
+
if flags & EntityUpdateFlags.TRUNCATED:
|
|
338
|
+
logger.debug("truncated attribute, fetching full value")
|
|
339
|
+
|
|
340
|
+
# Write the entity and attribute we're interested in
|
|
341
|
+
# (protected by a semaphore, so that we only read one attribute at a time)
|
|
342
|
+
async with self._read_attribute_semaphore:
|
|
343
|
+
await self._ams_proxy.entity_attribute.write_value(
|
|
344
|
+
data[:2], with_response=True
|
|
345
|
+
)
|
|
346
|
+
value = await self._ams_proxy.entity_attribute.read_value()
|
|
347
|
+
|
|
348
|
+
if entity == EntityId.PLAYER:
|
|
349
|
+
player_attribute = PlayerAttributeId(data[1])
|
|
350
|
+
if player_attribute == PlayerAttributeId.NAME:
|
|
351
|
+
self.player_name = value.decode()
|
|
352
|
+
self.emit(self.EVENT_PLAYER_NAME)
|
|
353
|
+
elif player_attribute == PlayerAttributeId.PLAYBACK_INFO:
|
|
354
|
+
playback_state_str, playback_rate_str, elapsed_time_str = (
|
|
355
|
+
value.decode().split(",")
|
|
356
|
+
)
|
|
357
|
+
self.player_playback_info = PlaybackInfo(
|
|
358
|
+
PlaybackState(int(playback_state_str)),
|
|
359
|
+
float(playback_rate_str),
|
|
360
|
+
float(elapsed_time_str),
|
|
361
|
+
)
|
|
362
|
+
self.emit(self.EVENT_PLAYER_PLAYBACK_INFO)
|
|
363
|
+
elif player_attribute == PlayerAttributeId.VOLUME:
|
|
364
|
+
self.player_volume = float(value.decode())
|
|
365
|
+
self.emit(self.EVENT_PLAYER_VOLUME)
|
|
366
|
+
else:
|
|
367
|
+
logger.warning(f"received unknown player attribute {player_attribute}")
|
|
368
|
+
|
|
369
|
+
elif entity == EntityId.QUEUE:
|
|
370
|
+
queue_attribute = QueueAttributeId(data[1])
|
|
371
|
+
if queue_attribute == QueueAttributeId.COUNT:
|
|
372
|
+
self.queue_count = int(value)
|
|
373
|
+
self.emit(self.EVENT_QUEUE_COUNT)
|
|
374
|
+
elif queue_attribute == QueueAttributeId.INDEX:
|
|
375
|
+
self.queue_index = int(value)
|
|
376
|
+
self.emit(self.EVENT_QUEUE_INDEX)
|
|
377
|
+
elif queue_attribute == QueueAttributeId.REPEAT_MODE:
|
|
378
|
+
self.queue_repeat_mode = RepeatMode(int(value))
|
|
379
|
+
self.emit(self.EVENT_QUEUE_REPEAT_MODE)
|
|
380
|
+
elif queue_attribute == QueueAttributeId.SHUFFLE_MODE:
|
|
381
|
+
self.queue_shuffle_mode = ShuffleMode(int(value))
|
|
382
|
+
self.emit(self.EVENT_QUEUE_SHUFFLE_MODE)
|
|
383
|
+
else:
|
|
384
|
+
logger.warning(f"received unknown queue attribute {queue_attribute}")
|
|
385
|
+
|
|
386
|
+
elif entity == EntityId.TRACK:
|
|
387
|
+
track_attribute = TrackAttributeId(data[1])
|
|
388
|
+
if track_attribute == TrackAttributeId.ARTIST:
|
|
389
|
+
self.track_artist = value.decode()
|
|
390
|
+
self.emit(self.EVENT_TRACK_ARTIST)
|
|
391
|
+
elif track_attribute == TrackAttributeId.ALBUM:
|
|
392
|
+
self.track_album = value.decode()
|
|
393
|
+
self.emit(self.EVENT_TRACK_ALBUM)
|
|
394
|
+
elif track_attribute == TrackAttributeId.TITLE:
|
|
395
|
+
self.track_title = value.decode()
|
|
396
|
+
self.emit(self.EVENT_TRACK_TITLE)
|
|
397
|
+
elif track_attribute == TrackAttributeId.DURATION:
|
|
398
|
+
self.track_duration = float(value.decode())
|
|
399
|
+
self.emit(self.EVENT_TRACK_DURATION)
|
|
400
|
+
else:
|
|
401
|
+
logger.warning(f"received unknown track attribute {track_attribute}")
|
|
402
|
+
|
|
403
|
+
else:
|
|
404
|
+
logger.warning(f"received unknown attribute ID {data[1]}")
|
bumble/profiles/ascs.py
CHANGED
|
@@ -452,6 +452,16 @@ class AseStateMachine(gatt.Characteristic):
|
|
|
452
452
|
|
|
453
453
|
self.metadata = le_audio.Metadata.from_bytes(metadata)
|
|
454
454
|
self.state = self.State.ENABLING
|
|
455
|
+
# CIS could be established before enable.
|
|
456
|
+
if cis_link := next(
|
|
457
|
+
(
|
|
458
|
+
cis_link
|
|
459
|
+
for cis_link in self.service.device.cis_links.values()
|
|
460
|
+
if cis_link.cig_id == self.cig_id and cis_link.cis_id == self.cis_id
|
|
461
|
+
),
|
|
462
|
+
None,
|
|
463
|
+
):
|
|
464
|
+
self.on_cis_establishment(cis_link)
|
|
455
465
|
|
|
456
466
|
return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
|
|
457
467
|
|
bumble/smp.py
CHANGED
|
@@ -946,7 +946,9 @@ class Session:
|
|
|
946
946
|
self.tk = self.passkey.to_bytes(16, byteorder='little')
|
|
947
947
|
logger.debug(f'TK from passkey = {self.tk.hex()}')
|
|
948
948
|
|
|
949
|
-
|
|
949
|
+
self.connection.cancel_on_disconnection(
|
|
950
|
+
self.pairing_config.delegate.display_number(self.passkey, digits=6)
|
|
951
|
+
)
|
|
950
952
|
|
|
951
953
|
def input_passkey(self, next_steps: Optional[Callable[[], None]] = None) -> None:
|
|
952
954
|
# Prompt the user for the passkey displayed on the peer
|
|
@@ -1569,11 +1571,12 @@ class Session:
|
|
|
1569
1571
|
if self.pairing_method == PairingMethod.CTKD_OVER_CLASSIC:
|
|
1570
1572
|
# Authentication is already done in SMP, so remote shall start keys distribution immediately
|
|
1571
1573
|
return
|
|
1572
|
-
elif self.sc:
|
|
1573
|
-
if self.pairing_method == PairingMethod.PASSKEY:
|
|
1574
|
-
self.display_or_input_passkey()
|
|
1575
1574
|
|
|
1575
|
+
if self.sc:
|
|
1576
1576
|
self.send_public_key_command()
|
|
1577
|
+
|
|
1578
|
+
if self.pairing_method == PairingMethod.PASSKEY:
|
|
1579
|
+
self.display_or_input_passkey()
|
|
1577
1580
|
else:
|
|
1578
1581
|
if self.pairing_method == PairingMethod.PASSKEY:
|
|
1579
1582
|
self.display_or_input_passkey(self.send_pairing_confirm_command)
|
|
@@ -1846,10 +1849,10 @@ class Session:
|
|
|
1846
1849
|
elif self.pairing_method == PairingMethod.PASSKEY:
|
|
1847
1850
|
self.send_pairing_confirm_command()
|
|
1848
1851
|
else:
|
|
1852
|
+
# Send our public key back to the initiator
|
|
1853
|
+
self.send_public_key_command()
|
|
1849
1854
|
|
|
1850
1855
|
def next_steps() -> None:
|
|
1851
|
-
# Send our public key back to the initiator
|
|
1852
|
-
self.send_public_key_command()
|
|
1853
1856
|
|
|
1854
1857
|
if self.pairing_method in (
|
|
1855
1858
|
PairingMethod.JUST_WORKS,
|
bumble/tools/intel_util.py
CHANGED
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
# -----------------------------------------------------------------------------
|
|
18
18
|
import logging
|
|
19
19
|
import asyncio
|
|
20
|
-
import os
|
|
21
20
|
from typing import Any, Optional
|
|
22
21
|
|
|
23
22
|
import click
|
|
@@ -26,6 +25,8 @@ from bumble.colors import color
|
|
|
26
25
|
from bumble import transport
|
|
27
26
|
from bumble.drivers import intel
|
|
28
27
|
from bumble.host import Host
|
|
28
|
+
import bumble.logging
|
|
29
|
+
|
|
29
30
|
|
|
30
31
|
# -----------------------------------------------------------------------------
|
|
31
32
|
# Logging
|
|
@@ -107,7 +108,7 @@ async def do_bootloader(usb_transport: str, force: bool) -> None:
|
|
|
107
108
|
# -----------------------------------------------------------------------------
|
|
108
109
|
@click.group()
|
|
109
110
|
def main():
|
|
110
|
-
logging.
|
|
111
|
+
bumble.logging.setup_basic_logging()
|
|
111
112
|
|
|
112
113
|
|
|
113
114
|
@main.command
|
bumble/tools/rtk_util.py
CHANGED
|
@@ -15,15 +15,16 @@
|
|
|
15
15
|
# -----------------------------------------------------------------------------
|
|
16
16
|
# Imports
|
|
17
17
|
# -----------------------------------------------------------------------------
|
|
18
|
-
import logging
|
|
19
18
|
import asyncio
|
|
20
|
-
import
|
|
19
|
+
import logging
|
|
21
20
|
|
|
22
21
|
import click
|
|
23
22
|
|
|
24
23
|
from bumble import transport
|
|
25
24
|
from bumble.host import Host
|
|
26
25
|
from bumble.drivers import rtk
|
|
26
|
+
import bumble.logging
|
|
27
|
+
|
|
27
28
|
|
|
28
29
|
# -----------------------------------------------------------------------------
|
|
29
30
|
# Logging
|
|
@@ -112,7 +113,7 @@ async def do_info(usb_transport, force):
|
|
|
112
113
|
# -----------------------------------------------------------------------------
|
|
113
114
|
@click.group()
|
|
114
115
|
def main():
|
|
115
|
-
logging.
|
|
116
|
+
bumble.logging.setup_basic_logging()
|
|
116
117
|
|
|
117
118
|
|
|
118
119
|
@main.command
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bumble
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.214
|
|
4
4
|
Summary: Bluetooth Stack for Apps, Emulation, Test and Experimentation
|
|
5
5
|
Author-email: Google <bumble-dev@google.com>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
6
7
|
Project-URL: Homepage, https://github.com/google/bumble
|
|
7
8
|
Requires-Python: >=3.9
|
|
8
9
|
Description-Content-Type: text/markdown
|
|
@@ -33,7 +34,7 @@ Requires-Dist: pytest-asyncio>=0.23.5; extra == "test"
|
|
|
33
34
|
Requires-Dist: pytest-html>=3.2.0; extra == "test"
|
|
34
35
|
Requires-Dist: coverage>=6.4; extra == "test"
|
|
35
36
|
Provides-Extra: development
|
|
36
|
-
Requires-Dist: black
|
|
37
|
+
Requires-Dist: black~=25.1; extra == "development"
|
|
37
38
|
Requires-Dist: bt-test-interfaces>=0.0.6; extra == "development"
|
|
38
39
|
Requires-Dist: grpcio-tools>=1.62.1; extra == "development"
|
|
39
40
|
Requires-Dist: invoke>=1.7.3; extra == "development"
|