1# Copyright 2023 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"""Host grpc interface."""
15
16import asyncio
17import logging
18from typing import AsyncGenerator
19import uuid as uuid_module
20
21from floss.pandora.floss import adapter_client
22from floss.pandora.floss import advertising_client
23from floss.pandora.floss import floss_enums
24from floss.pandora.floss import gatt_client
25from floss.pandora.floss import scanner_client
26from floss.pandora.floss import utils
27from floss.pandora.server import bluetooth as bluetooth_module
28from google.protobuf import empty_pb2
29import grpc
30from pandora import host_grpc_aio
31from pandora import host_pb2
32from pandora import security_grpc_aio
33
34
35class HostService(host_grpc_aio.HostServicer):
36    """Service to trigger Bluetooth Host procedures.
37
38    This class implements the Pandora bluetooth test interfaces,
39    where the meta class definition is automatically generated by the protobuf.
40    The interface definition can be found in:
41    https://cs.android.com/android/platform/superproject/+/main:external
42    /pandora/bt-test-interfaces/pandora/host.proto
43    """
44
45    def __init__(self, server: grpc.aio.Server, bluetooth: bluetooth_module.Bluetooth,
46                 security: security_grpc_aio.SecurityServicer):
47        self.server = server
48        self.bluetooth = bluetooth
49        self.security = security
50        self.waited_connections = set()
51        self.initiated_le_connection = set()
52
53    async def FactoryReset(self, request: empty_pb2.Empty, context: grpc.ServicerContext) -> empty_pb2.Empty:
54        self.waited_connections.clear()
55        self.initiated_le_connection.clear()
56
57        devices = self.bluetooth.get_bonded_devices()
58        if devices is None:
59            logging.error('Failed to call get_bonded_devices.')
60        else:
61            for device in devices:
62                address = device['address']
63                logging.info('Forget device %s', address)
64                self.bluetooth.forget_device(address)
65
66        asyncio.create_task(self.server.stop(None))
67        return empty_pb2.Empty()
68
69    async def Reset(self, request: empty_pb2.Empty, context: grpc.ServicerContext) -> empty_pb2.Empty:
70        self.waited_connections.clear()
71        self.initiated_le_connection.clear()
72        self.bluetooth.reset()
73        return empty_pb2.Empty()
74
75    async def ReadLocalAddress(self, request: empty_pb2.Empty,
76                               context: grpc.ServicerContext) -> host_pb2.ReadLocalAddressResponse:
77        address = self.bluetooth.get_address()
78        return host_pb2.ReadLocalAddressResponse(address=utils.address_to(address))
79
80    async def Connect(self, request: host_pb2.ConnectRequest,
81                      context: grpc.ServicerContext) -> host_pb2.ConnectResponse:
82
83        class ConnectionObserver(adapter_client.BluetoothConnectionCallbacks):
84            """Observer to observe the connection state."""
85
86            def __init__(self, client: adapter_client, task):
87                self.client = client
88                self.task = task
89
90            @utils.glib_callback()
91            def on_device_connected(self, remote_device):
92                address, _ = remote_device
93                if address != self.task['address']:
94                    return
95
96                if self.client.is_bonded(address):
97                    future = self.task['connect_device']
98                    future.get_loop().call_soon_threadsafe(future.set_result, (True, None))
99
100        class PairingObserver(adapter_client.BluetoothCallbacks):
101            """Observer to observe the bond state."""
102
103            def __init__(self, client: adapter_client, security: security_grpc_aio.SecurityServicer, task):
104                self.client = client
105                self.security = security
106                self.task = task
107
108            @utils.glib_callback()
109            def on_bond_state_changed(self, status, address, state):
110                if address != self.task['address']:
111                    return
112
113                if status != 0:
114                    future = self.task['create_bond']
115                    future.get_loop().call_soon_threadsafe(
116                        future.set_result, (False, f'{address} failed to bond. Status: {status}, State: {state}'))
117                    return
118
119                if state == floss_enums.BondState.BONDED:
120                    if not self.client.is_connected(self.task['address']):
121                        logging.info('%s calling connect_all_enabled_profiles', address)
122                        if not self.client.connect_all_enabled_profiles(self.task['address']):
123                            future = self.task['create_bond']
124                            future.get_loop().call_soon_threadsafe(
125                                future.set_result,
126                                (False, f'{self.task["address"]} failed on connect_all_enabled_profiles'))
127                    else:
128                        future = self.task['create_bond']
129                        future.get_loop().call_soon_threadsafe(future.set_result, (True, None))
130
131            @utils.glib_callback()
132            def on_ssp_request(self, remote_device, class_of_device, variant, passkey):
133                address, _ = remote_device
134                if address != self.task['address']:
135                    return
136
137                if variant in (floss_enums.PairingVariant.CONSENT, floss_enums.PairingVariant.PASSKEY_CONFIRMATION):
138                    self.client.set_pairing_confirmation(address,
139                                                         True,
140                                                         method_callback=self.on_set_pairing_confirmation)
141
142            @utils.glib_callback()
143            def on_set_pairing_confirmation(self, err, result):
144                if err or not result:
145                    future = self.task['create_bond']
146                    future.get_loop().call_soon_threadsafe(
147                        future.set_result, (False, f'Pairing confirmation failed: err: {err}, result: {result}'))
148
149        address = utils.address_from(request.address)
150
151        if not self.bluetooth.is_connected(address):
152            name = None
153            observer = None
154            try:
155                if self.bluetooth.is_bonded(address):
156                    connect_device = asyncio.get_running_loop().create_future()
157                    observer = ConnectionObserver(self.bluetooth.adapter_client, {
158                        'connect_device': connect_device,
159                        'address': address
160                    })
161                    name = utils.create_observer_name(observer)
162                    self.bluetooth.adapter_client.register_callback_observer(name, observer)
163
164                    self.bluetooth.connect_device(address)
165                    success, reason = await connect_device
166
167                    if not success:
168                        await context.abort(grpc.StatusCode.UNKNOWN,
169                                            f'Failed to connect to the {address}. Reason: {reason}.')
170                else:
171                    if not self.security.manually_confirm:
172                        create_bond = asyncio.get_running_loop().create_future()
173                        observer = PairingObserver(
174                            self.bluetooth.adapter_client,
175                            self.security,
176                            {
177                                'create_bond': create_bond,
178                                'address': address
179                            },
180                        )
181                        name = utils.create_observer_name(observer)
182                        self.bluetooth.adapter_client.register_callback_observer(name, observer)
183
184                    if not self.bluetooth.create_bond(address, floss_enums.BtTransport.BREDR):
185                        await context.abort(grpc.StatusCode.UNKNOWN, 'Failed to call create_bond.')
186
187                    if not self.security.manually_confirm:
188                        success, reason = await create_bond
189
190                        if not success:
191                            await context.abort(grpc.StatusCode.UNKNOWN,
192                                                f'Failed to connect to the {address}. Reason: {reason}.')
193
194                        if self.bluetooth.is_bonded(address) and self.bluetooth.is_connected(address):
195                            self.bluetooth.connect_device(address)
196            finally:
197                self.bluetooth.adapter_client.unregister_callback_observer(name, observer)
198
199        return host_pb2.ConnectResponse(
200            connection=utils.connection_to(utils.Connection(address, floss_enums.BtTransport.BREDR)))
201
202    async def WaitConnection(self, request: host_pb2.WaitConnectionRequest,
203                             context: grpc.ServicerContext) -> host_pb2.WaitConnectionResponse:
204
205        class ConnectionObserver(adapter_client.BluetoothConnectionCallbacks):
206            """Observer to observe the connection state."""
207
208            def __init__(self, task):
209                self.task = task
210
211            @utils.glib_callback()
212            def on_device_connected(self, remote_device):
213                address, _ = remote_device
214                if address != self.task['address']:
215                    return
216
217                future = self.task['wait_connection']
218                future.get_loop().call_soon_threadsafe(future.set_result, address)
219
220        if not request.address:
221            await context.abort(grpc.StatusCode.INVALID_ARGUMENT, 'Request address field must be set.')
222
223        address = utils.address_from(request.address)
224
225        if not self.bluetooth.is_connected(address) or address not in self.waited_connections:
226            try:
227                wait_connection = asyncio.get_running_loop().create_future()
228                observer = ConnectionObserver({'wait_connection': wait_connection, 'address': address})
229                name = utils.create_observer_name(observer)
230                self.bluetooth.adapter_client.register_callback_observer(name, observer)
231
232                await wait_connection
233            finally:
234                self.bluetooth.adapter_client.unregister_callback_observer(name, observer)
235            self.waited_connections.add(address)
236
237        return host_pb2.WaitConnectionResponse(
238            connection=utils.connection_to(utils.Connection(address, floss_enums.BtTransport.BREDR)))
239
240    async def ConnectLE(self, request: host_pb2.ConnectLERequest,
241                        context: grpc.ServicerContext) -> host_pb2.ConnectLEResponse:
242
243        class ConnectionObserver(gatt_client.GattClientCallbacks):
244            """Observer to observe the connection state."""
245
246            def __init__(self, task):
247                self.task = task
248
249            @utils.glib_callback()
250            def on_client_connection_state(self, status, client_id, connected, addr):
251                if addr != self.task['address']:
252                    return
253
254                future = self.task['connect_le_device']
255                if status != floss_enums.GattStatus.SUCCESS:
256                    future.get_loop().call_soon_threadsafe(future.set_result,
257                                                           (False, f'{address} failed to connect. Status: {status}.'))
258                    return
259
260                future.get_loop().call_soon_threadsafe(future.set_result, (connected, None))
261
262        if not request.address:
263            await context.abort(grpc.StatusCode.INVALID_ARGUMENT, 'Request address field must be set.')
264
265        own_address_type = request.own_address_type
266        if own_address_type not in (host_pb2.RANDOM, host_pb2.RESOLVABLE_OR_RANDOM):
267            await context.abort(grpc.StatusCode.UNIMPLEMENTED, f'Unsupported OwnAddressType: {own_address_type}.')
268
269        address = utils.address_from(request.address)
270        self.initiated_le_connection.add(address)
271        try:
272            connect_le_device = asyncio.get_running_loop().create_future()
273            observer = ConnectionObserver({'connect_le_device': connect_le_device, 'address': address})
274            name = utils.create_observer_name(observer)
275            self.bluetooth.gatt_client.register_callback_observer(name, observer)
276            self.bluetooth.gatt_connect(address, True, floss_enums.BtTransport.LE)
277            connected, reason = await connect_le_device
278            if not connected:
279                await context.abort(grpc.StatusCode.UNKNOWN,
280                                    f'Failed to connect to the address: {address}. Reason: {reason}.')
281        finally:
282            self.bluetooth.gatt_client.unregister_callback_observer(name, observer)
283
284        return host_pb2.ConnectLEResponse(
285            connection=utils.connection_to(utils.Connection(address, floss_enums.BtTransport.LE)))
286
287    async def Disconnect(self, request: host_pb2.DisconnectRequest, context: grpc.ServicerContext) -> empty_pb2.Empty:
288        address = utils.connection_from(request.connection).address
289
290        if self.bluetooth.is_connected(address):
291            self.bluetooth.disconnect_device(address)
292        return empty_pb2.Empty()
293
294    async def WaitDisconnection(self, request: host_pb2.WaitDisconnectionRequest,
295                                context: grpc.ServicerContext) -> empty_pb2.Empty:
296
297        class ConnectionObserver(adapter_client.BluetoothConnectionCallbacks):
298            """Observer to observe the connection state."""
299
300            def __init__(self, task):
301                self.task = task
302
303            @utils.glib_callback()
304            def on_device_disconnected(self, remote_device):
305                address, _ = remote_device
306                if address != self.task['address']:
307                    return
308
309                future = self.task['wait_disconnection']
310                future.get_loop().call_soon_threadsafe(future.set_result, address)
311
312        if not request.address:
313            await context.abort(grpc.StatusCode.INVALID_ARGUMENT, 'Request address field must be set.')
314
315        address = utils.address_from(request.address)
316
317        if self.bluetooth.is_connected(address):
318            try:
319                wait_disconnection = asyncio.get_running_loop().create_future()
320                observer = ConnectionObserver({'wait_disconnection': wait_disconnection, 'address': address})
321                name = utils.create_observer_name(observer)
322                self.bluetooth.adapter_client.register_callback_observer(name, observer)
323                await wait_disconnection
324            finally:
325                self.bluetooth.adapter_client.unregister_callback_observer(name, observer)
326
327        return empty_pb2.Empty()
328
329    async def Advertise(self, request: host_pb2.AdvertiseRequest,
330                        context: grpc.ServicerContext) -> AsyncGenerator[host_pb2.AdvertiseResponse, None]:
331        parameters = {
332            'connectable': request.connectable,
333            'scannable': True,
334            'is_legacy': True,  # ROOTCANAL: Extended advertising ignored because the scanner is legacy.
335            'is_anonymous': False,
336            'include_tx_power': True,
337            'primary_phy': 1,
338            'secondary_phy': 1,
339            'interval': request.interval,
340            'tx_power_level': 127,  # 0x7f
341            'own_address_type': -1,  # default
342        }
343
344        primary_phy = request.primary_phy
345        if primary_phy == host_pb2.PRIMARY_1M:
346            parameters['primary_phy'] = floss_enums.LePhy.PHY1M
347        elif primary_phy == host_pb2.PRIMARY_CODED:
348            parameters['primary_phy'] = floss_enums.LePhy.PHY_CODED
349
350        secondary_phy = request.secondary_phy
351        if secondary_phy == host_pb2.SECONDARY_NONE:
352            parameters['secondary_phy'] = floss_enums.LePhy.INVALID
353        elif secondary_phy == host_pb2.SECONDARY_1M:
354            parameters['secondary_phy'] = floss_enums.LePhy.PHY1M
355        elif secondary_phy == host_pb2.SECONDARY_2M:
356            parameters['secondary_phy'] = floss_enums.LePhy.PHY2M
357        elif secondary_phy == host_pb2.SECONDARY_CODED:
358            parameters['secondary_phy'] = floss_enums.LePhy.PHY_CODED
359
360        own_address_type = request.own_address_type
361        if own_address_type in (host_pb2.PUBLIC, host_pb2.RESOLVABLE_OR_PUBLIC):
362            parameters['own_address_type'] = floss_enums.OwnAddressType.PUBLIC
363        elif own_address_type in (host_pb2.RANDOM, host_pb2.RESOLVABLE_OR_RANDOM):
364            parameters['own_address_type'] = floss_enums.OwnAddressType.RANDOM
365
366        # TODO: b/289480188 - Support more data and scan response data if needed.
367        advertise_data = utils.advertise_data_from(request.data)
368
369        class AdvertisingObserver(advertising_client.BluetoothAdvertisingCallbacks):
370            """Observer to observe the advertising state."""
371
372            def __init__(self, task):
373                self.task = task
374
375            @utils.glib_callback()
376            def on_advertising_set_started(self, reg_id, advertiser_id, tx_power, status):
377                if reg_id != self.task['reg_id']:
378                    return
379
380                if status is None or floss_enums.GattStatus(status) != floss_enums.GattStatus.SUCCESS:
381                    logging.error('Failed to start advertising.')
382                    advertiser_id = None
383
384                future = self.task['start_advertising']
385                future.get_loop().call_soon_threadsafe(future.set_result, advertiser_id)
386
387        class ConnectionObserver(adapter_client.BluetoothConnectionCallbacks):
388            """Observer to observe all connections."""
389
390            def __init__(self, loop: asyncio.AbstractEventLoop, task):
391                self.loop = loop
392                self.task = task
393
394            @utils.glib_callback()
395            def on_device_connected(self, remote_device):
396                address, _ = remote_device
397                asyncio.run_coroutine_threadsafe(self.task['connections'].put(address), self.loop)
398
399        started_ids = []
400        observers = []
401        try:
402            if request.connectable:
403                connections = asyncio.Queue()
404                observer = ConnectionObserver(asyncio.get_running_loop(), {'connections': connections})
405                name = utils.create_observer_name(observer)
406                self.bluetooth.adapter_client.register_callback_observer(name, observer)
407                observers.append((name, observer))
408
409            reg_id = self.bluetooth.start_advertising_set(parameters, advertise_data, None, None, None, 0, 0)
410
411            advertising_request = {'start_advertising': asyncio.get_running_loop().create_future(), 'reg_id': reg_id}
412            observer = AdvertisingObserver(advertising_request)
413            name = utils.create_observer_name(observer)
414            self.bluetooth.advertising_client.register_callback_observer(name, observer)
415            observers.append((name, observer))
416
417            advertiser_id = await asyncio.wait_for(advertising_request['start_advertising'], timeout=5)
418            if advertiser_id is None:
419                await context.abort(grpc.StatusCode.UNKNOWN, 'Failed to start advertising.')
420
421            started_ids.append(advertiser_id)
422
423            while True:
424                if not request.connectable:
425                    await asyncio.sleep(1)
426                    continue
427
428                logging.info('Advertise: Wait for LE connection...')
429                address = await connections.get()
430                logging.info('Advertise: Connected to %s', address)
431
432                yield host_pb2.AdvertiseResponse(
433                    connection=utils.connection_to(utils.Connection(address, floss_enums.BtTransport.LE)))
434
435                await asyncio.sleep(1)
436        finally:
437            for name, observer in observers:
438                self.bluetooth.adapter_client.unregister_callback_observer(name, observer)
439                self.bluetooth.advertising_client.unregister_callback_observer(name, observer)
440
441            for started in started_ids:
442                self.bluetooth.stop_advertising_set(started)
443
444    async def Scan(self, request: host_pb2.ScanRequest,
445                   context: grpc.ServicerContext) -> AsyncGenerator[host_pb2.ScanningResponse, None]:
446
447        class ScanObserver(scanner_client.BluetoothScannerCallbacks):
448            """Observer to observer the scan state and scan results."""
449
450            def __init__(self, loop: asyncio.AbstractEventLoop, task):
451                self.loop = loop
452                self.task = task
453
454            @utils.glib_callback()
455            def on_scanner_registered(self, uuid, scanner_id, status):
456                uuid = uuid_module.UUID(bytes=bytes(uuid))
457                if uuid != self.task['uuid']:
458                    return
459
460                if floss_enums.GattStatus(status) != floss_enums.GattStatus.SUCCESS:
461                    logging.error('Failed to register scanner! uuid: {uuid}')
462                    scanner_id = None
463
464                future = self.task['register_scanner']
465                future.get_loop().call_soon_threadsafe(future.set_result, scanner_id)
466
467            @utils.glib_callback()
468            def on_scan_result(self, scan_result):
469                asyncio.run_coroutine_threadsafe(self.task['scan_results'].put(scan_result), self.loop)
470
471        scanner_id = None
472        name = None
473        observer = None
474        try:
475            uuid = self.bluetooth.register_scanner()
476            scan = {
477                'register_scanner': asyncio.get_running_loop().create_future(),
478                'uuid': uuid,
479                'scan_results': asyncio.Queue()
480            }
481            observer = ScanObserver(asyncio.get_running_loop(), scan)
482            name = utils.create_observer_name(observer)
483            self.bluetooth.scanner_client.register_callback_observer(name, observer)
484
485            scanner_id = await asyncio.wait_for(scan['register_scanner'], timeout=10)
486
487            self.bluetooth.start_scan(scanner_id)
488            while True:
489                scan_result = await scan['scan_results'].get()
490
491                response = host_pb2.ScanningResponse()
492                response.tx_power = scan_result['tx_power']
493                response.rssi = scan_result['rssi']
494                response.sid = scan_result['advertising_sid']
495                response.periodic_advertising_interval = scan_result['periodic_adv_int']
496
497                if scan_result['primary_phy'] == floss_enums.LePhy.PHY1M:
498                    response.primary_phy = host_pb2.PRIMARY_1M
499                elif scan_result['primary_phy'] == floss_enums.LePhy.PHY_CODED:
500                    response.primary_phy = host_pb2.PRIMARY_CODED
501                else:
502                    pass
503
504                if scan_result['secondary_phy'] == floss_enums.LePhy.INVALID:
505                    response.secondary_phy = host_pb2.SECONDARY_NONE
506                elif scan_result['secondary_phy'] == floss_enums.LePhy.PHY1M:
507                    response.secondary_phy = host_pb2.SECONDARY_1M
508                elif scan_result['secondary_phy'] == floss_enums.LePhy.PHY2M:
509                    response.secondary_phy = host_pb2.SECONDARY_2M
510                elif scan_result['secondary_phy'] == floss_enums.LePhy.PHY_CODED:
511                    response.secondary_phy = host_pb2.SECONDARY_CODED
512
513                address = bytes.fromhex(scan_result['address'].replace(':', ''))
514                if scan_result['addr_type'] == floss_enums.BleAddressType.BLE_ADDR_PUBLIC:
515                    response.public = address
516                elif scan_result['addr_type'] == floss_enums.BleAddressType.BLE_ADDR_RANDOM:
517                    response.random = address
518                elif scan_result['addr_type'] == floss_enums.BleAddressType.BLE_ADDR_PUBLIC_ID:
519                    response.public_identity = address
520                elif scan_result['addr_type'] == floss_enums.BleAddressType.BLE_ADDR_RANDOM_ID:
521                    response.random_static_identity = address
522
523                data = utils.parse_advertiging_data(scan_result['adv_data'])
524                response.data.CopyFrom(data)
525
526                # TODO: b/289480188 - Support more data if needed.
527                mode = host_pb2.NOT_DISCOVERABLE
528                if scan_result['flags'] & (1 << 0):
529                    mode = host_pb2.DISCOVERABLE_LIMITED
530                elif scan_result['flags'] & (1 << 1):
531                    mode = host_pb2.DISCOVERABLE_GENERAL
532                else:
533                    mode = host_pb2.NOT_DISCOVERABLE
534                response.data.le_discoverability_mode = mode
535
536                yield response
537        finally:
538            if scanner_id is not None:
539                self.bluetooth.stop_scan(scanner_id)
540            if name is not None and observer is not None:
541                self.bluetooth.scanner_client.unregister_callback_observer(name, observer)
542
543    async def Inquiry(self, request: empty_pb2.Empty,
544                      context: grpc.ServicerContext) -> AsyncGenerator[host_pb2.InquiryResponse, None]:
545
546        class InquiryResultObserver(adapter_client.BluetoothCallbacks):
547            """Observer to observe all inquiry results."""
548
549            def __init__(self, loop: asyncio.AbstractEventLoop, task):
550                self.loop = loop
551                self.task = task
552
553            @utils.glib_callback()
554            def on_device_found(self, remote_device):
555                address, _ = remote_device
556                asyncio.run_coroutine_threadsafe(self.task['inquiry_results'].put(address), self.loop)
557
558        class DiscoveryObserver(adapter_client.BluetoothCallbacks):
559            """Observer to observe discovery state."""
560
561            def __init__(self, task):
562                self.task = task
563
564            @utils.glib_callback()
565            def on_discovering_changed(self, discovering):
566                if discovering == self.task['discovering']:
567                    future = self.task['start_inquiry']
568                    future.get_loop().call_soon_threadsafe(future.set_result, discovering)
569
570        observers = []
571        try:
572            if not self.bluetooth.is_discovering():
573                inquriy = {'start_inquiry': asyncio.get_running_loop().create_future(), 'discovering': True}
574                observer = DiscoveryObserver(inquriy)
575                name = utils.create_observer_name(observer)
576                self.bluetooth.adapter_client.register_callback_observer(name, observer)
577                observers.append((name, observer))
578
579                self.bluetooth.start_discovery()
580                await asyncio.wait_for(inquriy['start_inquiry'], timeout=10)
581
582            inquiry_results = asyncio.Queue()
583            observer = InquiryResultObserver(asyncio.get_running_loop(), {'inquiry_results': inquiry_results})
584            name = utils.create_observer_name(observer)
585            self.bluetooth.adapter_client.register_callback_observer(name, observer)
586            observers.append((name, observer))
587
588            while True:
589                address = await inquiry_results.get()
590                yield host_pb2.InquiryResponse(address=utils.address_to(address))
591        finally:
592            self.bluetooth.stop_discovery()
593            for name, observer in observers:
594                self.bluetooth.adapter_client.unregister_callback_observer(name, observer)
595
596    async def SetDiscoverabilityMode(self, request: host_pb2.SetDiscoverabilityModeRequest,
597                                     context: grpc.ServicerContext) -> empty_pb2.Empty:
598        mode = request.mode
599        duration = 600  # Sets general, limited default to 60s. This is unused by the non-discoverable mode.
600        self.bluetooth.set_discoverable(mode, duration)
601        return empty_pb2.Empty()
602
603    async def SetConnectabilityMode(self, request: host_pb2.SetConnectabilityModeRequest,
604                                    context: grpc.ServicerContext) -> empty_pb2.Empty:
605        mode = request.mode
606        self.bluetooth.set_connectable(mode)
607        return empty_pb2.Empty()
608