bumble 0.0.154__py3-none-any.whl → 0.0.156__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/device.py CHANGED
@@ -954,12 +954,16 @@ class Device(CompositeEventEmitter):
954
954
  config.load_from_file(filename)
955
955
  return cls(config=config)
956
956
 
957
+ @classmethod
958
+ def from_config_with_hci(cls, config, hci_source, hci_sink):
959
+ host = Host(controller_source=hci_source, controller_sink=hci_sink)
960
+ return cls(config=config, host=host)
961
+
957
962
  @classmethod
958
963
  def from_config_file_with_hci(cls, filename, hci_source, hci_sink):
959
964
  config = DeviceConfiguration()
960
965
  config.load_from_file(filename)
961
- host = Host(controller_source=hci_source, controller_sink=hci_sink)
962
- return cls(config=config, host=host)
966
+ return cls.from_config_with_hci(config, hci_source, hci_sink)
963
967
 
964
968
  def __init__(
965
969
  self,
@@ -2441,7 +2445,7 @@ class Device(CompositeEventEmitter):
2441
2445
 
2442
2446
  if result.status != HCI_COMMAND_STATUS_PENDING:
2443
2447
  logger.warning(
2444
- 'HCI_Set_Connection_Encryption_Command failed: '
2448
+ 'HCI_Remote_Name_Request_Command failed: '
2445
2449
  f'{HCI_Constant.error_name(result.status)}'
2446
2450
  )
2447
2451
  raise HCI_StatusError(result)
@@ -3094,7 +3098,16 @@ class Device(CompositeEventEmitter):
3094
3098
  def on_pairing_start(self, connection: Connection) -> None:
3095
3099
  connection.emit('pairing_start')
3096
3100
 
3097
- def on_pairing(self, connection: Connection, keys: PairingKeys, sc: bool) -> None:
3101
+ def on_pairing(
3102
+ self,
3103
+ connection: Connection,
3104
+ identity_address: Optional[Address],
3105
+ keys: PairingKeys,
3106
+ sc: bool,
3107
+ ) -> None:
3108
+ if identity_address is not None:
3109
+ connection.peer_resolvable_address = connection.peer_address
3110
+ connection.peer_address = identity_address
3098
3111
  connection.sc = sc
3099
3112
  connection.authenticated = True
3100
3113
  connection.emit('pairing', keys)
bumble/hci.py CHANGED
@@ -62,7 +62,7 @@ def map_null_terminated_utf8_string(utf8_bytes):
62
62
  try:
63
63
  terminator = utf8_bytes.find(0)
64
64
  if terminator < 0:
65
- return utf8_bytes
65
+ terminator = len(utf8_bytes)
66
66
  return utf8_bytes[0:terminator].decode('utf8')
67
67
  except UnicodeDecodeError:
68
68
  return utf8_bytes
@@ -1795,6 +1795,16 @@ class Address:
1795
1795
  def to_bytes(self):
1796
1796
  return self.address_bytes
1797
1797
 
1798
+ def to_string(self, with_type_qualifier=True):
1799
+ '''
1800
+ String representation of the address, MSB first, with an optional type
1801
+ qualifier.
1802
+ '''
1803
+ result = ':'.join([f'{x:02X}' for x in reversed(self.address_bytes)])
1804
+ if not with_type_qualifier or not self.is_public:
1805
+ return result
1806
+ return result + '/P'
1807
+
1798
1808
  def __bytes__(self):
1799
1809
  return self.to_bytes()
1800
1810
 
@@ -1808,13 +1818,7 @@ class Address:
1808
1818
  )
1809
1819
 
1810
1820
  def __str__(self):
1811
- '''
1812
- String representation of the address, MSB first
1813
- '''
1814
- result = ':'.join([f'{x:02X}' for x in reversed(self.address_bytes)])
1815
- if not self.is_public:
1816
- return result
1817
- return result + '/P'
1821
+ return self.to_string()
1818
1822
 
1819
1823
 
1820
1824
  # Predefined address values
@@ -5373,7 +5377,7 @@ class HCI_AclDataPacket:
5373
5377
  def __str__(self):
5374
5378
  return (
5375
5379
  f'{color("ACL", "blue")}: '
5376
- f'handle=0x{self.connection_handle:04x}'
5380
+ f'handle=0x{self.connection_handle:04x}, '
5377
5381
  f'pb={self.pb_flag}, bc={self.bc_flag}, '
5378
5382
  f'data_total_length={self.data_total_length}, '
5379
5383
  f'data={self.data.hex()}'
bumble/hfp.py CHANGED
@@ -18,10 +18,11 @@
18
18
  import logging
19
19
  import asyncio
20
20
  import collections
21
+ from typing import Union
21
22
 
23
+ from . import rfcomm
22
24
  from .colors import color
23
25
 
24
-
25
26
  # -----------------------------------------------------------------------------
26
27
  # Logging
27
28
  # -----------------------------------------------------------------------------
@@ -34,7 +35,12 @@ logger = logging.getLogger(__name__)
34
35
 
35
36
  # -----------------------------------------------------------------------------
36
37
  class HfpProtocol:
37
- def __init__(self, dlc):
38
+ dlc: rfcomm.DLC
39
+ buffer: str
40
+ lines: collections.deque
41
+ lines_available: asyncio.Event
42
+
43
+ def __init__(self, dlc: rfcomm.DLC) -> None:
38
44
  self.dlc = dlc
39
45
  self.buffer = ''
40
46
  self.lines = collections.deque()
@@ -42,7 +48,7 @@ class HfpProtocol:
42
48
 
43
49
  dlc.sink = self.feed
44
50
 
45
- def feed(self, data):
51
+ def feed(self, data: Union[bytes, str]) -> None:
46
52
  # Convert the data to a string if needed
47
53
  if isinstance(data, bytes):
48
54
  data = data.decode('utf-8')
@@ -57,19 +63,19 @@ class HfpProtocol:
57
63
  if len(line) > 0:
58
64
  self.on_line(line)
59
65
 
60
- def on_line(self, line):
66
+ def on_line(self, line: str) -> None:
61
67
  self.lines.append(line)
62
68
  self.lines_available.set()
63
69
 
64
- def send_command_line(self, line):
70
+ def send_command_line(self, line: str) -> None:
65
71
  logger.debug(color(f'>>> {line}', 'yellow'))
66
72
  self.dlc.write(line + '\r')
67
73
 
68
- def send_response_line(self, line):
74
+ def send_response_line(self, line: str) -> None:
69
75
  logger.debug(color(f'>>> {line}', 'yellow'))
70
76
  self.dlc.write('\r\n' + line + '\r\n')
71
77
 
72
- async def next_line(self):
78
+ async def next_line(self) -> str:
73
79
  await self.lines_available.wait()
74
80
  line = self.lines.popleft()
75
81
  if not self.lines:
@@ -77,7 +83,7 @@ class HfpProtocol:
77
83
  logger.debug(color(f'<<< {line}', 'green'))
78
84
  return line
79
85
 
80
- async def initialize_service(self):
86
+ async def initialize_service(self) -> None:
81
87
  # Perform Service Level Connection Initialization
82
88
  self.send_command_line('AT+BRSF=2072') # Retrieve Supported Features
83
89
  await (self.next_line())
bumble/host.py CHANGED
@@ -62,6 +62,7 @@ from .hci import (
62
62
  HCI_Read_Local_Version_Information_Command,
63
63
  HCI_Reset_Command,
64
64
  HCI_Set_Event_Mask_Command,
65
+ map_null_terminated_utf8_string,
65
66
  )
66
67
  from .core import (
67
68
  BT_BR_EDR_TRANSPORT,
@@ -887,7 +888,12 @@ class Host(AbortableEventEmitter):
887
888
  if event.status != HCI_SUCCESS:
888
889
  self.emit('remote_name_failure', event.bd_addr, event.status)
889
890
  else:
890
- self.emit('remote_name', event.bd_addr, event.remote_name)
891
+ utf8_name = event.remote_name
892
+ terminator = utf8_name.find(0)
893
+ if terminator >= 0:
894
+ utf8_name = utf8_name[0:terminator]
895
+
896
+ self.emit('remote_name', event.bd_addr, utf8_name)
891
897
 
892
898
  def on_hci_remote_host_supported_features_notification_event(self, event):
893
899
  self.emit(
bumble/keys.py CHANGED
@@ -190,10 +190,44 @@ class KeyStore:
190
190
 
191
191
  # -----------------------------------------------------------------------------
192
192
  class JsonKeyStore(KeyStore):
193
+ """
194
+ KeyStore implementation that is backed by a JSON file.
195
+
196
+ This implementation supports storing a hierarchy of key sets in a single file.
197
+ A key set is a representation of a PairingKeys object. Each key set is stored
198
+ in a map, with the address of paired peer as the key. Maps are themselves grouped
199
+ into namespaces, grouping pairing keys by controller addresses.
200
+ The JSON object model looks like:
201
+ {
202
+ "<namespace>": {
203
+ "peer-address": {
204
+ "address_type": <n>,
205
+ "irk" : {
206
+ "authenticated": <true/false>,
207
+ "value": "hex-encoded-key"
208
+ },
209
+ ... other keys ...
210
+ },
211
+ ... other peers ...
212
+ }
213
+ ... other namespaces ...
214
+ }
215
+
216
+ A namespace is typically the BD_ADDR of a controller, since that is a convenient
217
+ unique identifier, but it may be something else.
218
+ A special namespace, called the "default" namespace, is used when instantiating this
219
+ class without a namespace. With the default namespace, reading from a file will
220
+ load an existing namespace if there is only one, which may be convenient for reading
221
+ from a file with a single key set and for which the namespace isn't known. If the
222
+ file does not include any existing key set, or if there are more than one and none
223
+ has the default name, a new one will be created with the name "__DEFAULT__".
224
+ """
225
+
193
226
  APP_NAME = 'Bumble'
194
227
  APP_AUTHOR = 'Google'
195
228
  KEYS_DIR = 'Pairing'
196
229
  DEFAULT_NAMESPACE = '__DEFAULT__'
230
+ DEFAULT_BASE_NAME = "keys"
197
231
 
198
232
  def __init__(self, namespace, filename=None):
199
233
  self.namespace = namespace if namespace is not None else self.DEFAULT_NAMESPACE
@@ -208,8 +242,9 @@ class JsonKeyStore(KeyStore):
208
242
  self.directory_name = os.path.join(
209
243
  appdirs.user_data_dir(self.APP_NAME, self.APP_AUTHOR), self.KEYS_DIR
210
244
  )
245
+ base_name = self.DEFAULT_BASE_NAME if namespace is None else self.namespace
211
246
  json_filename = (
212
- f'{self.namespace}.json'.lower().replace(':', '-').replace('/p', '-p')
247
+ f'{base_name}.json'.lower().replace(':', '-').replace('/p', '-p')
213
248
  )
214
249
  self.filename = os.path.join(self.directory_name, json_filename)
215
250
  else:
@@ -219,11 +254,13 @@ class JsonKeyStore(KeyStore):
219
254
  logger.debug(f'JSON keystore: {self.filename}')
220
255
 
221
256
  @staticmethod
222
- def from_device(device: Device) -> Optional[JsonKeyStore]:
223
- if not device.config.keystore:
224
- return None
225
-
226
- params = device.config.keystore.split(':', 1)[1:]
257
+ def from_device(device: Device, filename=None) -> Optional[JsonKeyStore]:
258
+ if not filename:
259
+ # Extract the filename from the config if there is one
260
+ if device.config.keystore is not None:
261
+ params = device.config.keystore.split(':', 1)[1:]
262
+ if params:
263
+ filename = params[0]
227
264
 
228
265
  # Use a namespace based on the device address
229
266
  if device.public_address not in (Address.ANY, Address.ANY_RANDOM):
@@ -232,19 +269,31 @@ class JsonKeyStore(KeyStore):
232
269
  namespace = str(device.random_address)
233
270
  else:
234
271
  namespace = JsonKeyStore.DEFAULT_NAMESPACE
235
- if params:
236
- filename = params[0]
237
- else:
238
- filename = None
239
272
 
240
273
  return JsonKeyStore(namespace, filename)
241
274
 
242
275
  async def load(self):
276
+ # Try to open the file, without failing. If the file does not exist, it
277
+ # will be created upon saving.
243
278
  try:
244
279
  with open(self.filename, 'r', encoding='utf-8') as json_file:
245
- return json.load(json_file)
280
+ db = json.load(json_file)
246
281
  except FileNotFoundError:
247
- return {}
282
+ db = {}
283
+
284
+ # First, look for a namespace match
285
+ if self.namespace in db:
286
+ return (db, db[self.namespace])
287
+
288
+ # Then, if the namespace is the default namespace, and there's
289
+ # only one entry in the db, use that
290
+ if self.namespace == self.DEFAULT_NAMESPACE and len(db) == 1:
291
+ return next(iter(db.items()))
292
+
293
+ # Finally, just create an empty key map for the namespace
294
+ key_map = {}
295
+ db[self.namespace] = key_map
296
+ return (db, key_map)
248
297
 
249
298
  async def save(self, db):
250
299
  # Create the directory if it doesn't exist
@@ -260,53 +309,30 @@ class JsonKeyStore(KeyStore):
260
309
  os.replace(temp_filename, self.filename)
261
310
 
262
311
  async def delete(self, name: str) -> None:
263
- db = await self.load()
264
-
265
- namespace = db.get(self.namespace)
266
- if namespace is None:
267
- raise KeyError(name)
268
-
269
- del namespace[name]
312
+ db, key_map = await self.load()
313
+ del key_map[name]
270
314
  await self.save(db)
271
315
 
272
316
  async def update(self, name, keys):
273
- db = await self.load()
274
-
275
- namespace = db.setdefault(self.namespace, {})
276
- namespace.setdefault(name, {}).update(keys.to_dict())
277
-
317
+ db, key_map = await self.load()
318
+ key_map.setdefault(name, {}).update(keys.to_dict())
278
319
  await self.save(db)
279
320
 
280
321
  async def get_all(self):
281
- db = await self.load()
282
-
283
- namespace = db.get(self.namespace)
284
- if namespace is None:
285
- return []
286
-
287
- return [
288
- (name, PairingKeys.from_dict(keys)) for (name, keys) in namespace.items()
289
- ]
322
+ _, key_map = await self.load()
323
+ return [(name, PairingKeys.from_dict(keys)) for (name, keys) in key_map.items()]
290
324
 
291
325
  async def delete_all(self):
292
- db = await self.load()
293
-
294
- db.pop(self.namespace, None)
295
-
326
+ db, key_map = await self.load()
327
+ key_map.clear()
296
328
  await self.save(db)
297
329
 
298
330
  async def get(self, name: str) -> Optional[PairingKeys]:
299
- db = await self.load()
300
-
301
- namespace = db.get(self.namespace)
302
- if namespace is None:
303
- return None
304
-
305
- keys = namespace.get(name)
306
- if keys is None:
331
+ _, key_map = await self.load()
332
+ if name not in key_map:
307
333
  return None
308
334
 
309
- return PairingKeys.from_dict(keys)
335
+ return PairingKeys.from_dict(key_map[name])
310
336
 
311
337
 
312
338
  # -----------------------------------------------------------------------------
bumble/pandora/host.py CHANGED
@@ -43,7 +43,8 @@ from bumble.hci import (
43
43
  HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR,
44
44
  Address,
45
45
  )
46
- from google.protobuf import any_pb2, empty_pb2 # pytype: disable=pyi-error
46
+ from google.protobuf import any_pb2 # pytype: disable=pyi-error
47
+ from google.protobuf import empty_pb2 # pytype: disable=pyi-error
47
48
  from pandora.host_grpc_aio import HostServicer
48
49
  from pandora.host_pb2 import (
49
50
  NOT_CONNECTABLE,
@@ -29,12 +29,9 @@ from bumble.device import Connection as BumbleConnection, Device
29
29
  from bumble.hci import HCI_Error
30
30
  from bumble.pairing import PairingConfig, PairingDelegate as BasePairingDelegate
31
31
  from contextlib import suppress
32
- from google.protobuf import (
33
- any_pb2,
34
- empty_pb2,
35
- wrappers_pb2,
36
- ) # pytype: disable=pyi-error
37
- from google.protobuf.wrappers_pb2 import BoolValue # pytype: disable=pyi-error
32
+ from google.protobuf import any_pb2 # pytype: disable=pyi-error
33
+ from google.protobuf import empty_pb2 # pytype: disable=pyi-error
34
+ from google.protobuf import wrappers_pb2 # pytype: disable=pyi-error
38
35
  from pandora.host_pb2 import Connection
39
36
  from pandora.security_grpc_aio import SecurityServicer, SecurityStorageServicer
40
37
  from pandora.security_pb2 import (
@@ -513,7 +510,7 @@ class SecurityStorageService(SecurityStorageServicer):
513
510
  else:
514
511
  is_bonded = False
515
512
 
516
- return BoolValue(value=is_bonded)
513
+ return wrappers_pb2.BoolValue(value=is_bonded)
517
514
 
518
515
  @utils.rpc
519
516
  async def DeleteBond(