bumble 0.0.198__py3-none-any.whl → 0.0.199__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,295 @@
1
+ # Copyright 2021-2022 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
+ # -----------------------------------------------------------------------------
17
+ # Imports
18
+ # -----------------------------------------------------------------------------
19
+ import enum
20
+ import struct
21
+ import logging
22
+ from typing import List, Optional, Callable, Union, Any
23
+
24
+ from bumble import l2cap
25
+ from bumble import utils
26
+ from bumble import gatt
27
+ from bumble import gatt_client
28
+ from bumble.core import AdvertisingData
29
+ from bumble.device import Device, Connection
30
+
31
+ # -----------------------------------------------------------------------------
32
+ # Logging
33
+ # -----------------------------------------------------------------------------
34
+ _logger = logging.getLogger(__name__)
35
+
36
+
37
+ # -----------------------------------------------------------------------------
38
+ # Constants
39
+ # -----------------------------------------------------------------------------
40
+ class DeviceCapabilities(enum.IntFlag):
41
+ IS_RIGHT = 0x01
42
+ IS_DUAL = 0x02
43
+ CSIS_SUPPORTED = 0x04
44
+
45
+
46
+ class FeatureMap(enum.IntFlag):
47
+ LE_COC_AUDIO_OUTPUT_STREAMING_SUPPORTED = 0x01
48
+
49
+
50
+ class AudioType(utils.OpenIntEnum):
51
+ UNKNOWN = 0x00
52
+ RINGTONE = 0x01
53
+ PHONE_CALL = 0x02
54
+ MEDIA = 0x03
55
+
56
+
57
+ class OpCode(utils.OpenIntEnum):
58
+ START = 1
59
+ STOP = 2
60
+ STATUS = 3
61
+
62
+
63
+ class Codec(utils.OpenIntEnum):
64
+ G_722_16KHZ = 1
65
+
66
+
67
+ class SupportedCodecs(enum.IntFlag):
68
+ G_722_16KHZ = 1 << Codec.G_722_16KHZ
69
+
70
+
71
+ class PeripheralStatus(utils.OpenIntEnum):
72
+ """Status update on the other peripheral."""
73
+
74
+ OTHER_PERIPHERAL_DISCONNECTED = 1
75
+ OTHER_PERIPHERAL_CONNECTED = 2
76
+ CONNECTION_PARAMETER_UPDATED = 3
77
+
78
+
79
+ class AudioStatus(utils.OpenIntEnum):
80
+ """Status report field for the audio control point."""
81
+
82
+ OK = 0
83
+ UNKNOWN_COMMAND = -1
84
+ ILLEGAL_PARAMETERS = -2
85
+
86
+
87
+ # -----------------------------------------------------------------------------
88
+ class AshaService(gatt.TemplateService):
89
+ UUID = gatt.GATT_ASHA_SERVICE
90
+
91
+ audio_sink: Optional[Callable[[bytes], Any]]
92
+ active_codec: Optional[Codec] = None
93
+ audio_type: Optional[AudioType] = None
94
+ volume: Optional[int] = None
95
+ other_state: Optional[int] = None
96
+ connection: Optional[Connection] = None
97
+
98
+ def __init__(
99
+ self,
100
+ capability: int,
101
+ hisyncid: Union[List[int], bytes],
102
+ device: Device,
103
+ psm: int = 0,
104
+ audio_sink: Optional[Callable[[bytes], Any]] = None,
105
+ feature_map: int = FeatureMap.LE_COC_AUDIO_OUTPUT_STREAMING_SUPPORTED,
106
+ protocol_version: int = 0x01,
107
+ render_delay_milliseconds: int = 0,
108
+ supported_codecs: int = SupportedCodecs.G_722_16KHZ,
109
+ ) -> None:
110
+ if len(hisyncid) != 8:
111
+ _logger.warning('HiSyncId should have a length of 8, got %d', len(hisyncid))
112
+
113
+ self.hisyncid = bytes(hisyncid)
114
+ self.capability = capability
115
+ self.device = device
116
+ self.audio_out_data = b''
117
+ self.psm = psm # a non-zero psm is mainly for testing purpose
118
+ self.audio_sink = audio_sink
119
+ self.protocol_version = protocol_version
120
+
121
+ self.read_only_properties_characteristic = gatt.Characteristic(
122
+ gatt.GATT_ASHA_READ_ONLY_PROPERTIES_CHARACTERISTIC,
123
+ gatt.Characteristic.Properties.READ,
124
+ gatt.Characteristic.READABLE,
125
+ struct.pack(
126
+ "<BB8sBH2sH",
127
+ protocol_version,
128
+ capability,
129
+ self.hisyncid,
130
+ feature_map,
131
+ render_delay_milliseconds,
132
+ b'\x00\x00',
133
+ supported_codecs,
134
+ ),
135
+ )
136
+
137
+ self.audio_control_point_characteristic = gatt.Characteristic(
138
+ gatt.GATT_ASHA_AUDIO_CONTROL_POINT_CHARACTERISTIC,
139
+ gatt.Characteristic.Properties.WRITE
140
+ | gatt.Characteristic.Properties.WRITE_WITHOUT_RESPONSE,
141
+ gatt.Characteristic.WRITEABLE,
142
+ gatt.CharacteristicValue(write=self._on_audio_control_point_write),
143
+ )
144
+ self.audio_status_characteristic = gatt.Characteristic(
145
+ gatt.GATT_ASHA_AUDIO_STATUS_CHARACTERISTIC,
146
+ gatt.Characteristic.Properties.READ | gatt.Characteristic.Properties.NOTIFY,
147
+ gatt.Characteristic.READABLE,
148
+ bytes([AudioStatus.OK]),
149
+ )
150
+ self.volume_characteristic = gatt.Characteristic(
151
+ gatt.GATT_ASHA_VOLUME_CHARACTERISTIC,
152
+ gatt.Characteristic.Properties.WRITE_WITHOUT_RESPONSE,
153
+ gatt.Characteristic.WRITEABLE,
154
+ gatt.CharacteristicValue(write=self._on_volume_write),
155
+ )
156
+
157
+ # let the server find a free PSM
158
+ self.psm = device.create_l2cap_server(
159
+ spec=l2cap.LeCreditBasedChannelSpec(psm=self.psm, max_credits=8),
160
+ handler=self._on_connection,
161
+ ).psm
162
+ self.le_psm_out_characteristic = gatt.Characteristic(
163
+ gatt.GATT_ASHA_LE_PSM_OUT_CHARACTERISTIC,
164
+ gatt.Characteristic.Properties.READ,
165
+ gatt.Characteristic.READABLE,
166
+ struct.pack('<H', self.psm),
167
+ )
168
+
169
+ characteristics = [
170
+ self.read_only_properties_characteristic,
171
+ self.audio_control_point_characteristic,
172
+ self.audio_status_characteristic,
173
+ self.volume_characteristic,
174
+ self.le_psm_out_characteristic,
175
+ ]
176
+
177
+ super().__init__(characteristics)
178
+
179
+ def get_advertising_data(self) -> bytes:
180
+ # Advertisement only uses 4 least significant bytes of the HiSyncId.
181
+ return bytes(
182
+ AdvertisingData(
183
+ [
184
+ (
185
+ AdvertisingData.SERVICE_DATA_16_BIT_UUID,
186
+ bytes(gatt.GATT_ASHA_SERVICE)
187
+ + bytes([self.protocol_version, self.capability])
188
+ + self.hisyncid[:4],
189
+ ),
190
+ ]
191
+ )
192
+ )
193
+
194
+ # Handler for audio control commands
195
+ async def _on_audio_control_point_write(
196
+ self, connection: Optional[Connection], value: bytes
197
+ ) -> None:
198
+ _logger.debug(f'--- AUDIO CONTROL POINT Write:{value.hex()}')
199
+ opcode = value[0]
200
+ if opcode == OpCode.START:
201
+ # Start
202
+ self.active_codec = Codec(value[1])
203
+ self.audio_type = AudioType(value[2])
204
+ self.volume = value[3]
205
+ self.other_state = value[4]
206
+ _logger.debug(
207
+ f'### START: codec={self.active_codec.name}, '
208
+ f'audio_type={self.audio_type.name}, '
209
+ f'volume={self.volume}, '
210
+ f'other_state={self.other_state}'
211
+ )
212
+ self.emit('started')
213
+ elif opcode == OpCode.STOP:
214
+ _logger.debug('### STOP')
215
+ self.active_codec = None
216
+ self.audio_type = None
217
+ self.volume = None
218
+ self.other_state = None
219
+ self.emit('stopped')
220
+ elif opcode == OpCode.STATUS:
221
+ _logger.debug('### STATUS: %s', PeripheralStatus(value[1]).name)
222
+
223
+ if self.connection is None and connection:
224
+ self.connection = connection
225
+
226
+ def on_disconnection(_reason) -> None:
227
+ self.connection = None
228
+ self.active_codec = None
229
+ self.audio_type = None
230
+ self.volume = None
231
+ self.other_state = None
232
+ self.emit('disconnected')
233
+
234
+ connection.once('disconnection', on_disconnection)
235
+
236
+ # OPCODE_STATUS does not need audio status point update
237
+ if opcode != OpCode.STATUS:
238
+ await self.device.notify_subscribers(
239
+ self.audio_status_characteristic, force=True
240
+ )
241
+
242
+ # Handler for volume control
243
+ def _on_volume_write(self, connection: Optional[Connection], value: bytes) -> None:
244
+ _logger.debug(f'--- VOLUME Write:{value[0]}')
245
+ self.volume = value[0]
246
+ self.emit('volume_changed')
247
+
248
+ # Register an L2CAP CoC server
249
+ def _on_connection(self, channel: l2cap.LeCreditBasedChannel) -> None:
250
+ def on_data(data: bytes) -> None:
251
+ if self.audio_sink: # pylint: disable=not-callable
252
+ self.audio_sink(data)
253
+
254
+ channel.sink = on_data
255
+
256
+
257
+ # -----------------------------------------------------------------------------
258
+ class AshaServiceProxy(gatt_client.ProfileServiceProxy):
259
+ SERVICE_CLASS = AshaService
260
+ read_only_properties_characteristic: gatt_client.CharacteristicProxy
261
+ audio_control_point_characteristic: gatt_client.CharacteristicProxy
262
+ audio_status_point_characteristic: gatt_client.CharacteristicProxy
263
+ volume_characteristic: gatt_client.CharacteristicProxy
264
+ psm_characteristic: gatt_client.CharacteristicProxy
265
+
266
+ def __init__(self, service_proxy: gatt_client.ServiceProxy) -> None:
267
+ self.service_proxy = service_proxy
268
+
269
+ for uuid, attribute_name in (
270
+ (
271
+ gatt.GATT_ASHA_READ_ONLY_PROPERTIES_CHARACTERISTIC,
272
+ 'read_only_properties_characteristic',
273
+ ),
274
+ (
275
+ gatt.GATT_ASHA_AUDIO_CONTROL_POINT_CHARACTERISTIC,
276
+ 'audio_control_point_characteristic',
277
+ ),
278
+ (
279
+ gatt.GATT_ASHA_AUDIO_STATUS_CHARACTERISTIC,
280
+ 'audio_status_point_characteristic',
281
+ ),
282
+ (
283
+ gatt.GATT_ASHA_VOLUME_CHARACTERISTIC,
284
+ 'volume_characteristic',
285
+ ),
286
+ (
287
+ gatt.GATT_ASHA_LE_PSM_OUT_CHARACTERISTIC,
288
+ 'psm_characteristic',
289
+ ),
290
+ ):
291
+ if not (
292
+ characteristics := self.service_proxy.get_characteristics_by_uuid(uuid)
293
+ ):
294
+ raise gatt.InvalidServiceError(f"Missing {uuid} Characteristic")
295
+ setattr(self, attribute_name, characteristics[0])