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.
- bumble/_version.py +16 -3
- bumble/a2dp.py +15 -16
- bumble/apps/auracast.py +14 -38
- bumble/apps/bench.py +10 -15
- bumble/apps/ble_rpa_tool.py +1 -0
- bumble/apps/console.py +22 -25
- bumble/apps/controller_info.py +20 -25
- bumble/apps/controller_loopback.py +6 -10
- bumble/apps/controllers.py +2 -3
- bumble/apps/device_info.py +4 -5
- bumble/apps/gatt_dump.py +3 -3
- bumble/apps/gg_bridge.py +7 -8
- bumble/apps/hci_bridge.py +4 -3
- bumble/apps/l2cap_bridge.py +5 -5
- bumble/apps/lea_unicast/app.py +16 -26
- bumble/apps/pair.py +30 -43
- bumble/apps/pandora_server.py +5 -4
- bumble/apps/player/player.py +20 -24
- bumble/apps/rfcomm_bridge.py +4 -10
- bumble/apps/scan.py +17 -8
- bumble/apps/show.py +4 -5
- bumble/apps/speaker/speaker.py +23 -27
- bumble/apps/unbond.py +3 -3
- bumble/apps/usb_probe.py +2 -4
- bumble/att.py +241 -246
- bumble/audio/io.py +5 -9
- bumble/avc.py +2 -2
- bumble/avctp.py +6 -7
- bumble/avdtp.py +19 -22
- bumble/avrcp.py +1097 -589
- bumble/codecs.py +2 -0
- bumble/controller.py +142 -35
- bumble/core.py +567 -248
- bumble/crypto/__init__.py +2 -2
- bumble/crypto/builtin.py +1 -1
- bumble/crypto/cryptography.py +2 -4
- bumble/data_types.py +1025 -0
- bumble/device.py +319 -267
- bumble/drivers/__init__.py +3 -2
- bumble/drivers/intel.py +3 -4
- bumble/drivers/rtk.py +26 -9
- bumble/gap.py +4 -4
- bumble/gatt.py +3 -2
- bumble/gatt_adapters.py +3 -11
- bumble/gatt_client.py +69 -81
- bumble/gatt_server.py +124 -124
- bumble/hci.py +114 -18
- bumble/helpers.py +19 -26
- bumble/hfp.py +10 -21
- bumble/hid.py +22 -16
- bumble/host.py +191 -103
- bumble/keys.py +5 -3
- bumble/l2cap.py +138 -104
- bumble/link.py +18 -19
- bumble/logging.py +65 -0
- bumble/pairing.py +7 -6
- bumble/pandora/__init__.py +9 -8
- bumble/pandora/config.py +3 -1
- bumble/pandora/device.py +3 -2
- bumble/pandora/host.py +38 -36
- bumble/pandora/l2cap.py +22 -21
- bumble/pandora/security.py +15 -15
- bumble/pandora/utils.py +5 -3
- bumble/profiles/aics.py +11 -11
- bumble/profiles/ams.py +403 -0
- bumble/profiles/ancs.py +6 -7
- bumble/profiles/ascs.py +14 -9
- bumble/profiles/asha.py +8 -12
- bumble/profiles/bap.py +11 -23
- bumble/profiles/bass.py +2 -7
- bumble/profiles/battery_service.py +3 -4
- bumble/profiles/cap.py +1 -2
- bumble/profiles/csip.py +2 -6
- bumble/profiles/device_information_service.py +2 -2
- bumble/profiles/gap.py +4 -4
- bumble/profiles/gatt_service.py +1 -4
- bumble/profiles/gmap.py +5 -5
- bumble/profiles/hap.py +62 -59
- bumble/profiles/heart_rate_service.py +5 -4
- bumble/profiles/le_audio.py +3 -1
- bumble/profiles/mcp.py +3 -7
- bumble/profiles/pacs.py +3 -6
- bumble/profiles/pbp.py +2 -0
- bumble/profiles/tmap.py +2 -3
- bumble/profiles/vcs.py +2 -8
- bumble/profiles/vocs.py +8 -8
- bumble/rfcomm.py +11 -14
- bumble/rtp.py +1 -0
- bumble/sdp.py +10 -8
- bumble/smp.py +151 -159
- bumble/snoop.py +5 -5
- bumble/tools/generate_company_id_list.py +1 -0
- bumble/tools/intel_fw_download.py +3 -3
- bumble/tools/intel_util.py +5 -4
- bumble/tools/rtk_fw_download.py +6 -3
- bumble/tools/rtk_util.py +26 -8
- bumble/transport/__init__.py +19 -15
- bumble/transport/android_emulator.py +8 -13
- bumble/transport/android_netsim.py +19 -18
- bumble/transport/common.py +12 -15
- bumble/transport/file.py +1 -1
- bumble/transport/hci_socket.py +4 -6
- bumble/transport/pty.py +5 -6
- bumble/transport/pyusb.py +7 -10
- bumble/transport/serial.py +2 -1
- bumble/transport/tcp_client.py +2 -2
- bumble/transport/tcp_server.py +11 -14
- bumble/transport/udp.py +3 -3
- bumble/transport/unix.py +67 -1
- bumble/transport/usb.py +6 -6
- bumble/transport/vhci.py +0 -1
- bumble/transport/ws_client.py +2 -1
- bumble/transport/ws_server.py +3 -2
- bumble/utils.py +20 -5
- bumble/vendor/android/hci.py +1 -2
- bumble/vendor/zephyr/hci.py +0 -1
- {bumble-0.0.213.dist-info → bumble-0.0.215.dist-info}/METADATA +4 -2
- bumble-0.0.215.dist-info/RECORD +183 -0
- bumble-0.0.213.dist-info/RECORD +0 -180
- {bumble-0.0.213.dist-info → bumble-0.0.215.dist-info}/WHEEL +0 -0
- {bumble-0.0.213.dist-info → bumble-0.0.215.dist-info}/entry_points.txt +0 -0
- {bumble-0.0.213.dist-info → bumble-0.0.215.dist-info}/licenses/LICENSE +0 -0
- {bumble-0.0.213.dist-info → bumble-0.0.215.dist-info}/top_level.txt +0 -0
bumble/pandora/host.py
CHANGED
|
@@ -13,51 +13,23 @@
|
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
16
|
+
|
|
16
17
|
import asyncio
|
|
17
|
-
import bumble.device
|
|
18
|
-
import grpc
|
|
19
|
-
import grpc.aio
|
|
20
18
|
import logging
|
|
21
19
|
import struct
|
|
20
|
+
from typing import AsyncGenerator, Optional, cast
|
|
22
21
|
|
|
23
|
-
import
|
|
24
|
-
|
|
25
|
-
from bumble.pandora.config import Config
|
|
26
|
-
from bumble.core import (
|
|
27
|
-
PhysicalTransport,
|
|
28
|
-
UUID,
|
|
29
|
-
AdvertisingData,
|
|
30
|
-
Appearance,
|
|
31
|
-
ConnectionError,
|
|
32
|
-
)
|
|
33
|
-
from bumble.device import (
|
|
34
|
-
DEVICE_DEFAULT_SCAN_INTERVAL,
|
|
35
|
-
DEVICE_DEFAULT_SCAN_WINDOW,
|
|
36
|
-
Advertisement,
|
|
37
|
-
AdvertisingParameters,
|
|
38
|
-
AdvertisingEventProperties,
|
|
39
|
-
AdvertisingType,
|
|
40
|
-
Device,
|
|
41
|
-
)
|
|
42
|
-
from bumble.gatt import Service
|
|
43
|
-
from bumble.hci import (
|
|
44
|
-
HCI_CONNECTION_ALREADY_EXISTS_ERROR,
|
|
45
|
-
HCI_PAGE_TIMEOUT_ERROR,
|
|
46
|
-
HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR,
|
|
47
|
-
Address,
|
|
48
|
-
Phy,
|
|
49
|
-
Role,
|
|
50
|
-
OwnAddressType,
|
|
51
|
-
)
|
|
22
|
+
import grpc
|
|
23
|
+
import grpc.aio
|
|
52
24
|
from google.protobuf import any_pb2 # pytype: disable=pyi-error
|
|
53
25
|
from google.protobuf import empty_pb2 # pytype: disable=pyi-error
|
|
54
|
-
from pandora.host_grpc_aio import HostServicer
|
|
55
26
|
from pandora import host_pb2
|
|
27
|
+
from pandora.host_grpc_aio import HostServicer
|
|
56
28
|
from pandora.host_pb2 import (
|
|
29
|
+
DISCOVERABLE_GENERAL,
|
|
30
|
+
DISCOVERABLE_LIMITED,
|
|
57
31
|
NOT_CONNECTABLE,
|
|
58
32
|
NOT_DISCOVERABLE,
|
|
59
|
-
DISCOVERABLE_LIMITED,
|
|
60
|
-
DISCOVERABLE_GENERAL,
|
|
61
33
|
PRIMARY_1M,
|
|
62
34
|
PRIMARY_CODED,
|
|
63
35
|
SECONDARY_1M,
|
|
@@ -85,7 +57,37 @@ from pandora.host_pb2 import (
|
|
|
85
57
|
WaitConnectionResponse,
|
|
86
58
|
WaitDisconnectionRequest,
|
|
87
59
|
)
|
|
88
|
-
|
|
60
|
+
|
|
61
|
+
import bumble.device
|
|
62
|
+
import bumble.utils
|
|
63
|
+
from bumble.core import (
|
|
64
|
+
UUID,
|
|
65
|
+
AdvertisingData,
|
|
66
|
+
Appearance,
|
|
67
|
+
ConnectionError,
|
|
68
|
+
PhysicalTransport,
|
|
69
|
+
)
|
|
70
|
+
from bumble.device import (
|
|
71
|
+
DEVICE_DEFAULT_SCAN_INTERVAL,
|
|
72
|
+
DEVICE_DEFAULT_SCAN_WINDOW,
|
|
73
|
+
Advertisement,
|
|
74
|
+
AdvertisingEventProperties,
|
|
75
|
+
AdvertisingParameters,
|
|
76
|
+
AdvertisingType,
|
|
77
|
+
Device,
|
|
78
|
+
)
|
|
79
|
+
from bumble.gatt import Service
|
|
80
|
+
from bumble.hci import (
|
|
81
|
+
HCI_CONNECTION_ALREADY_EXISTS_ERROR,
|
|
82
|
+
HCI_PAGE_TIMEOUT_ERROR,
|
|
83
|
+
HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR,
|
|
84
|
+
Address,
|
|
85
|
+
OwnAddressType,
|
|
86
|
+
Phy,
|
|
87
|
+
Role,
|
|
88
|
+
)
|
|
89
|
+
from bumble.pandora import utils
|
|
90
|
+
from bumble.pandora.config import Config
|
|
89
91
|
|
|
90
92
|
PRIMARY_PHY_MAP: dict[int, PrimaryPhy] = {
|
|
91
93
|
# Default value reported by Bumble for legacy Advertising reports.
|
bumble/pandora/l2cap.py
CHANGED
|
@@ -12,31 +12,21 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
from __future__ import annotations
|
|
15
|
+
|
|
15
16
|
import asyncio
|
|
16
|
-
import grpc
|
|
17
17
|
import json
|
|
18
18
|
import logging
|
|
19
|
+
from asyncio import Future
|
|
20
|
+
from asyncio import Queue as AsyncQueue
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from typing import AsyncGenerator, Optional, Union
|
|
19
23
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
from bumble.pandora import utils
|
|
23
|
-
from bumble.pandora.config import Config
|
|
24
|
-
from bumble.core import OutOfResourcesError, InvalidArgumentError
|
|
25
|
-
from bumble.device import Device
|
|
26
|
-
from bumble.l2cap import (
|
|
27
|
-
ClassicChannel,
|
|
28
|
-
ClassicChannelServer,
|
|
29
|
-
ClassicChannelSpec,
|
|
30
|
-
LeCreditBasedChannel,
|
|
31
|
-
LeCreditBasedChannelServer,
|
|
32
|
-
LeCreditBasedChannelSpec,
|
|
33
|
-
)
|
|
24
|
+
import grpc
|
|
34
25
|
from google.protobuf import any_pb2, empty_pb2 # pytype: disable=pyi-error
|
|
35
26
|
from pandora.l2cap_grpc_aio import L2CAPServicer # pytype: disable=pyi-error
|
|
36
|
-
from pandora.l2cap_pb2 import
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
Channel as PandoraChannel,
|
|
27
|
+
from pandora.l2cap_pb2 import COMMAND_NOT_UNDERSTOOD, INVALID_CID_IN_REQUEST
|
|
28
|
+
from pandora.l2cap_pb2 import Channel as PandoraChannel # pytype: disable=pyi-error
|
|
29
|
+
from pandora.l2cap_pb2 import (
|
|
40
30
|
ConnectRequest,
|
|
41
31
|
ConnectResponse,
|
|
42
32
|
CreditBasedChannelRequest,
|
|
@@ -51,8 +41,19 @@ from pandora.l2cap_pb2 import ( # pytype: disable=pyi-error
|
|
|
51
41
|
WaitDisconnectionRequest,
|
|
52
42
|
WaitDisconnectionResponse,
|
|
53
43
|
)
|
|
54
|
-
|
|
55
|
-
from
|
|
44
|
+
|
|
45
|
+
from bumble.core import InvalidArgumentError, OutOfResourcesError
|
|
46
|
+
from bumble.device import Device
|
|
47
|
+
from bumble.l2cap import (
|
|
48
|
+
ClassicChannel,
|
|
49
|
+
ClassicChannelServer,
|
|
50
|
+
ClassicChannelSpec,
|
|
51
|
+
LeCreditBasedChannel,
|
|
52
|
+
LeCreditBasedChannelServer,
|
|
53
|
+
LeCreditBasedChannelSpec,
|
|
54
|
+
)
|
|
55
|
+
from bumble.pandora import utils
|
|
56
|
+
from bumble.pandora.config import Config
|
|
56
57
|
|
|
57
58
|
L2capChannel = Union[ClassicChannel, LeCreditBasedChannel]
|
|
58
59
|
|
bumble/pandora/security.py
CHANGED
|
@@ -13,24 +13,14 @@
|
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
16
|
+
|
|
16
17
|
import asyncio
|
|
17
18
|
import contextlib
|
|
18
|
-
from collections.abc import Awaitable
|
|
19
|
-
import grpc
|
|
20
19
|
import logging
|
|
20
|
+
from collections.abc import Awaitable
|
|
21
|
+
from typing import Any, AsyncGenerator, AsyncIterator, Callable, Optional, Union
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
from bumble.pandora.config import Config
|
|
24
|
-
from bumble import hci
|
|
25
|
-
from bumble.core import (
|
|
26
|
-
PhysicalTransport,
|
|
27
|
-
ProtocolError,
|
|
28
|
-
InvalidArgumentError,
|
|
29
|
-
)
|
|
30
|
-
import bumble.utils
|
|
31
|
-
from bumble.device import Connection as BumbleConnection, Device
|
|
32
|
-
from bumble.hci import HCI_Error, Role
|
|
33
|
-
from bumble.pairing import PairingConfig, PairingDelegate as BasePairingDelegate
|
|
23
|
+
import grpc
|
|
34
24
|
from google.protobuf import any_pb2 # pytype: disable=pyi-error
|
|
35
25
|
from google.protobuf import empty_pb2 # pytype: disable=pyi-error
|
|
36
26
|
from google.protobuf import wrappers_pb2 # pytype: disable=pyi-error
|
|
@@ -57,7 +47,17 @@ from pandora.security_pb2 import (
|
|
|
57
47
|
WaitSecurityRequest,
|
|
58
48
|
WaitSecurityResponse,
|
|
59
49
|
)
|
|
60
|
-
|
|
50
|
+
|
|
51
|
+
import bumble.utils
|
|
52
|
+
from bumble import hci
|
|
53
|
+
from bumble.core import InvalidArgumentError, PhysicalTransport, ProtocolError
|
|
54
|
+
from bumble.device import Connection as BumbleConnection
|
|
55
|
+
from bumble.device import Device
|
|
56
|
+
from bumble.hci import HCI_Error, Role
|
|
57
|
+
from bumble.pairing import PairingConfig
|
|
58
|
+
from bumble.pairing import PairingDelegate as BasePairingDelegate
|
|
59
|
+
from bumble.pandora import utils
|
|
60
|
+
from bumble.pandora.config import Config
|
|
61
61
|
|
|
62
62
|
|
|
63
63
|
class PairingDelegate(BasePairingDelegate):
|
bumble/pandora/utils.py
CHANGED
|
@@ -13,16 +13,18 @@
|
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
16
|
+
|
|
16
17
|
import contextlib
|
|
17
18
|
import functools
|
|
18
|
-
import grpc
|
|
19
19
|
import inspect
|
|
20
20
|
import logging
|
|
21
|
+
from typing import Any, Generator, MutableMapping, Optional
|
|
22
|
+
|
|
23
|
+
import grpc
|
|
24
|
+
from google.protobuf.message import Message # pytype: disable=pyi-error
|
|
21
25
|
|
|
22
26
|
from bumble.device import Device
|
|
23
27
|
from bumble.hci import Address, AddressType
|
|
24
|
-
from google.protobuf.message import Message # pytype: disable=pyi-error
|
|
25
|
-
from typing import Any, Generator, MutableMapping, Optional
|
|
26
28
|
|
|
27
29
|
ADDRESS_TYPES: dict[str, AddressType] = {
|
|
28
30
|
"public": Address.PUBLIC_DEVICE_ADDRESS,
|
bumble/profiles/aics.py
CHANGED
|
@@ -18,26 +18,27 @@
|
|
|
18
18
|
# Imports
|
|
19
19
|
# -----------------------------------------------------------------------------
|
|
20
20
|
from __future__ import annotations
|
|
21
|
+
|
|
21
22
|
import logging
|
|
22
23
|
import struct
|
|
23
|
-
|
|
24
24
|
from dataclasses import dataclass
|
|
25
25
|
from typing import Optional
|
|
26
26
|
|
|
27
|
-
from bumble
|
|
27
|
+
from bumble import utils
|
|
28
28
|
from bumble.att import ATT_Error
|
|
29
|
+
from bumble.device import Connection
|
|
29
30
|
from bumble.gatt import (
|
|
30
|
-
|
|
31
|
-
Characteristic,
|
|
32
|
-
TemplateService,
|
|
33
|
-
CharacteristicValue,
|
|
31
|
+
GATT_AUDIO_INPUT_CONTROL_POINT_CHARACTERISTIC,
|
|
34
32
|
GATT_AUDIO_INPUT_CONTROL_SERVICE,
|
|
33
|
+
GATT_AUDIO_INPUT_DESCRIPTION_CHARACTERISTIC,
|
|
35
34
|
GATT_AUDIO_INPUT_STATE_CHARACTERISTIC,
|
|
36
|
-
GATT_GAIN_SETTINGS_ATTRIBUTE_CHARACTERISTIC,
|
|
37
|
-
GATT_AUDIO_INPUT_TYPE_CHARACTERISTIC,
|
|
38
35
|
GATT_AUDIO_INPUT_STATUS_CHARACTERISTIC,
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
GATT_AUDIO_INPUT_TYPE_CHARACTERISTIC,
|
|
37
|
+
GATT_GAIN_SETTINGS_ATTRIBUTE_CHARACTERISTIC,
|
|
38
|
+
Attribute,
|
|
39
|
+
Characteristic,
|
|
40
|
+
CharacteristicValue,
|
|
41
|
+
TemplateService,
|
|
41
42
|
)
|
|
42
43
|
from bumble.gatt_adapters import (
|
|
43
44
|
CharacteristicProxy,
|
|
@@ -48,7 +49,6 @@ from bumble.gatt_adapters import (
|
|
|
48
49
|
UTF8CharacteristicProxyAdapter,
|
|
49
50
|
)
|
|
50
51
|
from bumble.gatt_client import ProfileServiceProxy, ServiceProxy
|
|
51
|
-
from bumble import utils
|
|
52
52
|
|
|
53
53
|
# -----------------------------------------------------------------------------
|
|
54
54
|
# Logging
|
bumble/profiles/ams.py
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
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
|
+
|
|
24
|
+
import asyncio
|
|
25
|
+
import dataclasses
|
|
26
|
+
import enum
|
|
27
|
+
import logging
|
|
28
|
+
from typing import Iterable, Optional, Union
|
|
29
|
+
|
|
30
|
+
from bumble import utils
|
|
31
|
+
from bumble.device import Peer
|
|
32
|
+
from bumble.gatt import (
|
|
33
|
+
GATT_AMS_ENTITY_ATTRIBUTE_CHARACTERISTIC,
|
|
34
|
+
GATT_AMS_ENTITY_UPDATE_CHARACTERISTIC,
|
|
35
|
+
GATT_AMS_REMOTE_COMMAND_CHARACTERISTIC,
|
|
36
|
+
GATT_AMS_SERVICE,
|
|
37
|
+
Characteristic,
|
|
38
|
+
TemplateService,
|
|
39
|
+
)
|
|
40
|
+
from bumble.gatt_client import CharacteristicProxy, ProfileServiceProxy, ServiceProxy
|
|
41
|
+
|
|
42
|
+
# -----------------------------------------------------------------------------
|
|
43
|
+
# Logging
|
|
44
|
+
# -----------------------------------------------------------------------------
|
|
45
|
+
logger = logging.getLogger(__name__)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# -----------------------------------------------------------------------------
|
|
49
|
+
# Protocol
|
|
50
|
+
# -----------------------------------------------------------------------------
|
|
51
|
+
class RemoteCommandId(utils.OpenIntEnum):
|
|
52
|
+
PLAY = 0
|
|
53
|
+
PAUSE = 1
|
|
54
|
+
TOGGLE_PLAY_PAUSE = 2
|
|
55
|
+
NEXT_TRACK = 3
|
|
56
|
+
PREVIOUS_TRACK = 4
|
|
57
|
+
VOLUME_UP = 5
|
|
58
|
+
VOLUME_DOWN = 6
|
|
59
|
+
ADVANCE_REPEAT_MODE = 7
|
|
60
|
+
ADVANCE_SHUFFLE_MODE = 8
|
|
61
|
+
SKIP_FORWARD = 9
|
|
62
|
+
SKIP_BACKWARD = 10
|
|
63
|
+
LIKE_TRACK = 11
|
|
64
|
+
DISLIKE_TRACK = 12
|
|
65
|
+
BOOKMARK_TRACK = 13
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class EntityId(utils.OpenIntEnum):
|
|
69
|
+
PLAYER = 0
|
|
70
|
+
QUEUE = 1
|
|
71
|
+
TRACK = 2
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ActionId(utils.OpenIntEnum):
|
|
75
|
+
POSITIVE = 0
|
|
76
|
+
NEGATIVE = 1
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class EntityUpdateFlags(enum.IntFlag):
|
|
80
|
+
TRUNCATED = 1
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class PlayerAttributeId(utils.OpenIntEnum):
|
|
84
|
+
NAME = 0
|
|
85
|
+
PLAYBACK_INFO = 1
|
|
86
|
+
VOLUME = 2
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class QueueAttributeId(utils.OpenIntEnum):
|
|
90
|
+
INDEX = 0
|
|
91
|
+
COUNT = 1
|
|
92
|
+
SHUFFLE_MODE = 2
|
|
93
|
+
REPEAT_MODE = 3
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class ShuffleMode(utils.OpenIntEnum):
|
|
97
|
+
OFF = 0
|
|
98
|
+
ONE = 1
|
|
99
|
+
ALL = 2
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class RepeatMode(utils.OpenIntEnum):
|
|
103
|
+
OFF = 0
|
|
104
|
+
ONE = 1
|
|
105
|
+
ALL = 2
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class TrackAttributeId(utils.OpenIntEnum):
|
|
109
|
+
ARTIST = 0
|
|
110
|
+
ALBUM = 1
|
|
111
|
+
TITLE = 2
|
|
112
|
+
DURATION = 3
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class PlaybackState(utils.OpenIntEnum):
|
|
116
|
+
PAUSED = 0
|
|
117
|
+
PLAYING = 1
|
|
118
|
+
REWINDING = 2
|
|
119
|
+
FAST_FORWARDING = 3
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclasses.dataclass
|
|
123
|
+
class PlaybackInfo:
|
|
124
|
+
playback_state: PlaybackState = PlaybackState.PAUSED
|
|
125
|
+
playback_rate: float = 1.0
|
|
126
|
+
elapsed_time: float = 0.0
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# -----------------------------------------------------------------------------
|
|
130
|
+
# GATT Server-side
|
|
131
|
+
# -----------------------------------------------------------------------------
|
|
132
|
+
class Ams(TemplateService):
|
|
133
|
+
UUID = GATT_AMS_SERVICE
|
|
134
|
+
|
|
135
|
+
remote_command_characteristic: Characteristic
|
|
136
|
+
entity_update_characteristic: Characteristic
|
|
137
|
+
entity_attribute_characteristic: Characteristic
|
|
138
|
+
|
|
139
|
+
def __init__(self) -> None:
|
|
140
|
+
# TODO not the final implementation
|
|
141
|
+
self.remote_command_characteristic = Characteristic(
|
|
142
|
+
GATT_AMS_REMOTE_COMMAND_CHARACTERISTIC,
|
|
143
|
+
Characteristic.Properties.NOTIFY
|
|
144
|
+
| Characteristic.Properties.WRITE_WITHOUT_RESPONSE,
|
|
145
|
+
Characteristic.Permissions.WRITEABLE,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# TODO not the final implementation
|
|
149
|
+
self.entity_update_characteristic = Characteristic(
|
|
150
|
+
GATT_AMS_ENTITY_UPDATE_CHARACTERISTIC,
|
|
151
|
+
Characteristic.Properties.NOTIFY | Characteristic.Properties.WRITE,
|
|
152
|
+
Characteristic.Permissions.WRITEABLE,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# TODO not the final implementation
|
|
156
|
+
self.entity_attribute_characteristic = Characteristic(
|
|
157
|
+
GATT_AMS_ENTITY_ATTRIBUTE_CHARACTERISTIC,
|
|
158
|
+
Characteristic.Properties.READ
|
|
159
|
+
| Characteristic.Properties.WRITE_WITHOUT_RESPONSE,
|
|
160
|
+
Characteristic.Permissions.WRITEABLE | Characteristic.Permissions.READABLE,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
super().__init__(
|
|
164
|
+
[
|
|
165
|
+
self.remote_command_characteristic,
|
|
166
|
+
self.entity_update_characteristic,
|
|
167
|
+
self.entity_attribute_characteristic,
|
|
168
|
+
]
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# -----------------------------------------------------------------------------
|
|
173
|
+
# GATT Client-side
|
|
174
|
+
# -----------------------------------------------------------------------------
|
|
175
|
+
class AmsProxy(ProfileServiceProxy):
|
|
176
|
+
SERVICE_CLASS = Ams
|
|
177
|
+
|
|
178
|
+
# NOTE: these don't use adapters, because the format for write and notifications
|
|
179
|
+
# are different.
|
|
180
|
+
remote_command: CharacteristicProxy[bytes]
|
|
181
|
+
entity_update: CharacteristicProxy[bytes]
|
|
182
|
+
entity_attribute: CharacteristicProxy[bytes]
|
|
183
|
+
|
|
184
|
+
def __init__(self, service_proxy: ServiceProxy):
|
|
185
|
+
self.remote_command = service_proxy.get_required_characteristic_by_uuid(
|
|
186
|
+
GATT_AMS_REMOTE_COMMAND_CHARACTERISTIC
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
self.entity_update = service_proxy.get_required_characteristic_by_uuid(
|
|
190
|
+
GATT_AMS_ENTITY_UPDATE_CHARACTERISTIC
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
self.entity_attribute = service_proxy.get_required_characteristic_by_uuid(
|
|
194
|
+
GATT_AMS_ENTITY_ATTRIBUTE_CHARACTERISTIC
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class AmsClient(utils.EventEmitter):
|
|
199
|
+
EVENT_SUPPORTED_COMMANDS = "supported_commands"
|
|
200
|
+
EVENT_PLAYER_NAME = "player_name"
|
|
201
|
+
EVENT_PLAYER_PLAYBACK_INFO = "player_playback_info"
|
|
202
|
+
EVENT_PLAYER_VOLUME = "player_volume"
|
|
203
|
+
EVENT_QUEUE_COUNT = "queue_count"
|
|
204
|
+
EVENT_QUEUE_INDEX = "queue_index"
|
|
205
|
+
EVENT_QUEUE_SHUFFLE_MODE = "queue_shuffle_mode"
|
|
206
|
+
EVENT_QUEUE_REPEAT_MODE = "queue_repeat_mode"
|
|
207
|
+
EVENT_TRACK_ARTIST = "track_artist"
|
|
208
|
+
EVENT_TRACK_ALBUM = "track_album"
|
|
209
|
+
EVENT_TRACK_TITLE = "track_title"
|
|
210
|
+
EVENT_TRACK_DURATION = "track_duration"
|
|
211
|
+
|
|
212
|
+
supported_commands: set[RemoteCommandId]
|
|
213
|
+
player_name: str = ""
|
|
214
|
+
player_playback_info: PlaybackInfo = PlaybackInfo(PlaybackState.PAUSED, 0.0, 0.0)
|
|
215
|
+
player_volume: float = 1.0
|
|
216
|
+
queue_count: int = 0
|
|
217
|
+
queue_index: int = 0
|
|
218
|
+
queue_shuffle_mode: ShuffleMode = ShuffleMode.OFF
|
|
219
|
+
queue_repeat_mode: RepeatMode = RepeatMode.OFF
|
|
220
|
+
track_artist: str = ""
|
|
221
|
+
track_album: str = ""
|
|
222
|
+
track_title: str = ""
|
|
223
|
+
track_duration: float = 0.0
|
|
224
|
+
|
|
225
|
+
def __init__(self, ams_proxy: AmsProxy) -> None:
|
|
226
|
+
super().__init__()
|
|
227
|
+
self._ams_proxy = ams_proxy
|
|
228
|
+
self._started = False
|
|
229
|
+
self._read_attribute_semaphore = asyncio.Semaphore()
|
|
230
|
+
self.supported_commands = set()
|
|
231
|
+
|
|
232
|
+
@classmethod
|
|
233
|
+
async def for_peer(cls, peer: Peer) -> Optional[AmsClient]:
|
|
234
|
+
ams_proxy = await peer.discover_service_and_create_proxy(AmsProxy)
|
|
235
|
+
if ams_proxy is None:
|
|
236
|
+
return None
|
|
237
|
+
return cls(ams_proxy)
|
|
238
|
+
|
|
239
|
+
async def start(self) -> None:
|
|
240
|
+
logger.debug("subscribing to remote command characteristic")
|
|
241
|
+
await self._ams_proxy.remote_command.subscribe(
|
|
242
|
+
self._on_remote_command_notification
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
logger.debug("subscribing to entity update characteristic")
|
|
246
|
+
await self._ams_proxy.entity_update.subscribe(
|
|
247
|
+
lambda data: utils.AsyncRunner.spawn(
|
|
248
|
+
self._on_entity_update_notification(data)
|
|
249
|
+
)
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
self._started = True
|
|
253
|
+
|
|
254
|
+
async def stop(self) -> None:
|
|
255
|
+
await self._ams_proxy.remote_command.unsubscribe(
|
|
256
|
+
self._on_remote_command_notification
|
|
257
|
+
)
|
|
258
|
+
await self._ams_proxy.entity_update.unsubscribe(
|
|
259
|
+
self._on_entity_update_notification
|
|
260
|
+
)
|
|
261
|
+
self._started = False
|
|
262
|
+
|
|
263
|
+
async def observe(
|
|
264
|
+
self,
|
|
265
|
+
entity: EntityId,
|
|
266
|
+
attributes: Iterable[
|
|
267
|
+
Union[PlayerAttributeId, QueueAttributeId, TrackAttributeId]
|
|
268
|
+
],
|
|
269
|
+
) -> None:
|
|
270
|
+
await self._ams_proxy.entity_update.write_value(
|
|
271
|
+
bytes([entity] + list(attributes)), with_response=True
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
async def command(self, command: RemoteCommandId) -> None:
|
|
275
|
+
await self._ams_proxy.remote_command.write_value(
|
|
276
|
+
bytes([command]), with_response=True
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
async def play(self) -> None:
|
|
280
|
+
await self.command(RemoteCommandId.PLAY)
|
|
281
|
+
|
|
282
|
+
async def pause(self) -> None:
|
|
283
|
+
await self.command(RemoteCommandId.PAUSE)
|
|
284
|
+
|
|
285
|
+
async def toggle_play_pause(self) -> None:
|
|
286
|
+
await self.command(RemoteCommandId.TOGGLE_PLAY_PAUSE)
|
|
287
|
+
|
|
288
|
+
async def next_track(self) -> None:
|
|
289
|
+
await self.command(RemoteCommandId.NEXT_TRACK)
|
|
290
|
+
|
|
291
|
+
async def previous_track(self) -> None:
|
|
292
|
+
await self.command(RemoteCommandId.PREVIOUS_TRACK)
|
|
293
|
+
|
|
294
|
+
async def volume_up(self) -> None:
|
|
295
|
+
await self.command(RemoteCommandId.VOLUME_UP)
|
|
296
|
+
|
|
297
|
+
async def volume_down(self) -> None:
|
|
298
|
+
await self.command(RemoteCommandId.VOLUME_DOWN)
|
|
299
|
+
|
|
300
|
+
async def advance_repeat_mode(self) -> None:
|
|
301
|
+
await self.command(RemoteCommandId.ADVANCE_REPEAT_MODE)
|
|
302
|
+
|
|
303
|
+
async def advance_shuffle_mode(self) -> None:
|
|
304
|
+
await self.command(RemoteCommandId.ADVANCE_SHUFFLE_MODE)
|
|
305
|
+
|
|
306
|
+
async def skip_forward(self) -> None:
|
|
307
|
+
await self.command(RemoteCommandId.SKIP_FORWARD)
|
|
308
|
+
|
|
309
|
+
async def skip_backward(self) -> None:
|
|
310
|
+
await self.command(RemoteCommandId.SKIP_BACKWARD)
|
|
311
|
+
|
|
312
|
+
async def like_track(self) -> None:
|
|
313
|
+
await self.command(RemoteCommandId.LIKE_TRACK)
|
|
314
|
+
|
|
315
|
+
async def dislike_track(self) -> None:
|
|
316
|
+
await self.command(RemoteCommandId.DISLIKE_TRACK)
|
|
317
|
+
|
|
318
|
+
async def bookmark_track(self) -> None:
|
|
319
|
+
await self.command(RemoteCommandId.BOOKMARK_TRACK)
|
|
320
|
+
|
|
321
|
+
def _on_remote_command_notification(self, data: bytes) -> None:
|
|
322
|
+
supported_commands = [RemoteCommandId(command) for command in data]
|
|
323
|
+
logger.debug(
|
|
324
|
+
f"supported commands: {[command.name for command in supported_commands]}"
|
|
325
|
+
)
|
|
326
|
+
for command in supported_commands:
|
|
327
|
+
self.supported_commands.add(command)
|
|
328
|
+
|
|
329
|
+
self.emit(self.EVENT_SUPPORTED_COMMANDS)
|
|
330
|
+
|
|
331
|
+
async def _on_entity_update_notification(self, data: bytes) -> None:
|
|
332
|
+
entity = EntityId(data[0])
|
|
333
|
+
flags = EntityUpdateFlags(data[2])
|
|
334
|
+
value = data[3:]
|
|
335
|
+
|
|
336
|
+
if flags & EntityUpdateFlags.TRUNCATED:
|
|
337
|
+
logger.debug("truncated attribute, fetching full value")
|
|
338
|
+
|
|
339
|
+
# Write the entity and attribute we're interested in
|
|
340
|
+
# (protected by a semaphore, so that we only read one attribute at a time)
|
|
341
|
+
async with self._read_attribute_semaphore:
|
|
342
|
+
await self._ams_proxy.entity_attribute.write_value(
|
|
343
|
+
data[:2], with_response=True
|
|
344
|
+
)
|
|
345
|
+
value = await self._ams_proxy.entity_attribute.read_value()
|
|
346
|
+
|
|
347
|
+
if entity == EntityId.PLAYER:
|
|
348
|
+
player_attribute = PlayerAttributeId(data[1])
|
|
349
|
+
if player_attribute == PlayerAttributeId.NAME:
|
|
350
|
+
self.player_name = value.decode()
|
|
351
|
+
self.emit(self.EVENT_PLAYER_NAME)
|
|
352
|
+
elif player_attribute == PlayerAttributeId.PLAYBACK_INFO:
|
|
353
|
+
playback_state_str, playback_rate_str, elapsed_time_str = (
|
|
354
|
+
value.decode().split(",")
|
|
355
|
+
)
|
|
356
|
+
self.player_playback_info = PlaybackInfo(
|
|
357
|
+
PlaybackState(int(playback_state_str)),
|
|
358
|
+
float(playback_rate_str),
|
|
359
|
+
float(elapsed_time_str),
|
|
360
|
+
)
|
|
361
|
+
self.emit(self.EVENT_PLAYER_PLAYBACK_INFO)
|
|
362
|
+
elif player_attribute == PlayerAttributeId.VOLUME:
|
|
363
|
+
self.player_volume = float(value.decode())
|
|
364
|
+
self.emit(self.EVENT_PLAYER_VOLUME)
|
|
365
|
+
else:
|
|
366
|
+
logger.warning(f"received unknown player attribute {player_attribute}")
|
|
367
|
+
|
|
368
|
+
elif entity == EntityId.QUEUE:
|
|
369
|
+
queue_attribute = QueueAttributeId(data[1])
|
|
370
|
+
if queue_attribute == QueueAttributeId.COUNT:
|
|
371
|
+
self.queue_count = int(value)
|
|
372
|
+
self.emit(self.EVENT_QUEUE_COUNT)
|
|
373
|
+
elif queue_attribute == QueueAttributeId.INDEX:
|
|
374
|
+
self.queue_index = int(value)
|
|
375
|
+
self.emit(self.EVENT_QUEUE_INDEX)
|
|
376
|
+
elif queue_attribute == QueueAttributeId.REPEAT_MODE:
|
|
377
|
+
self.queue_repeat_mode = RepeatMode(int(value))
|
|
378
|
+
self.emit(self.EVENT_QUEUE_REPEAT_MODE)
|
|
379
|
+
elif queue_attribute == QueueAttributeId.SHUFFLE_MODE:
|
|
380
|
+
self.queue_shuffle_mode = ShuffleMode(int(value))
|
|
381
|
+
self.emit(self.EVENT_QUEUE_SHUFFLE_MODE)
|
|
382
|
+
else:
|
|
383
|
+
logger.warning(f"received unknown queue attribute {queue_attribute}")
|
|
384
|
+
|
|
385
|
+
elif entity == EntityId.TRACK:
|
|
386
|
+
track_attribute = TrackAttributeId(data[1])
|
|
387
|
+
if track_attribute == TrackAttributeId.ARTIST:
|
|
388
|
+
self.track_artist = value.decode()
|
|
389
|
+
self.emit(self.EVENT_TRACK_ARTIST)
|
|
390
|
+
elif track_attribute == TrackAttributeId.ALBUM:
|
|
391
|
+
self.track_album = value.decode()
|
|
392
|
+
self.emit(self.EVENT_TRACK_ALBUM)
|
|
393
|
+
elif track_attribute == TrackAttributeId.TITLE:
|
|
394
|
+
self.track_title = value.decode()
|
|
395
|
+
self.emit(self.EVENT_TRACK_TITLE)
|
|
396
|
+
elif track_attribute == TrackAttributeId.DURATION:
|
|
397
|
+
self.track_duration = float(value.decode())
|
|
398
|
+
self.emit(self.EVENT_TRACK_DURATION)
|
|
399
|
+
else:
|
|
400
|
+
logger.warning(f"received unknown track attribute {track_attribute}")
|
|
401
|
+
|
|
402
|
+
else:
|
|
403
|
+
logger.warning(f"received unknown attribute ID {data[1]}")
|