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# Imports
17# -----------------------------------------------------------------------------
18import asyncio
19import sys
20import os
21import logging
22import json
23import websockets
24import struct
25
26from bumble.device import Device
27from bumble.transport import open_transport_or_link
28from bumble.core import (
29    BT_BR_EDR_TRANSPORT,
30    BT_L2CAP_PROTOCOL_ID,
31    BT_HUMAN_INTERFACE_DEVICE_SERVICE,
32    BT_HIDP_PROTOCOL_ID,
33)
34from bumble.hid import (
35    Device as HID_Device,
36    HID_CONTROL_PSM,
37    HID_INTERRUPT_PSM,
38    Message,
39)
40from bumble.sdp import (
41    DataElement,
42    ServiceAttribute,
43    SDP_PUBLIC_BROWSE_ROOT,
44    SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
45    SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
46    SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
47    SDP_LANGUAGE_BASE_ATTRIBUTE_ID_LIST_ATTRIBUTE_ID,
48    SDP_ADDITIONAL_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
49    SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
50    SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
51)
52
53# -----------------------------------------------------------------------------
54# SDP attributes for Bluetooth HID devices
55SDP_HID_SERVICE_NAME_ATTRIBUTE_ID = 0x0100
56SDP_HID_SERVICE_DESCRIPTION_ATTRIBUTE_ID = 0x0101
57SDP_HID_PROVIDER_NAME_ATTRIBUTE_ID = 0x0102
58SDP_HID_DEVICE_RELEASE_NUMBER_ATTRIBUTE_ID = 0x0200  # [DEPRECATED]
59SDP_HID_PARSER_VERSION_ATTRIBUTE_ID = 0x0201
60SDP_HID_DEVICE_SUBCLASS_ATTRIBUTE_ID = 0x0202
61SDP_HID_COUNTRY_CODE_ATTRIBUTE_ID = 0x0203
62SDP_HID_VIRTUAL_CABLE_ATTRIBUTE_ID = 0x0204
63SDP_HID_RECONNECT_INITIATE_ATTRIBUTE_ID = 0x0205
64SDP_HID_DESCRIPTOR_LIST_ATTRIBUTE_ID = 0x0206
65SDP_HID_LANGID_BASE_LIST_ATTRIBUTE_ID = 0x0207
66SDP_HID_SDP_DISABLE_ATTRIBUTE_ID = 0x0208  # [DEPRECATED]
67SDP_HID_BATTERY_POWER_ATTRIBUTE_ID = 0x0209
68SDP_HID_REMOTE_WAKE_ATTRIBUTE_ID = 0x020A
69SDP_HID_PROFILE_VERSION_ATTRIBUTE_ID = 0x020B  # DEPRECATED]
70SDP_HID_SUPERVISION_TIMEOUT_ATTRIBUTE_ID = 0x020C
71SDP_HID_NORMALLY_CONNECTABLE_ATTRIBUTE_ID = 0x020D
72SDP_HID_BOOT_DEVICE_ATTRIBUTE_ID = 0x020E
73SDP_HID_SSR_HOST_MAX_LATENCY_ATTRIBUTE_ID = 0x020F
74SDP_HID_SSR_HOST_MIN_TIMEOUT_ATTRIBUTE_ID = 0x0210
75
76# Refer to HID profile specification v1.1.1, "5.3 Service Discovery Protocol (SDP)" for details
77# HID SDP attribute values
78LANGUAGE = 0x656E  # 0x656E uint16 “en” (English)
79ENCODING = 0x6A  # 0x006A uint16 UTF-8 encoding
80PRIMARY_LANGUAGE_BASE_ID = 0x100  # 0x0100 uint16 PrimaryLanguageBaseID
81VERSION_NUMBER = 0x0101  # 0x0101 uint16 version number (v1.1)
82SERVICE_NAME = b'Bumble HID'
83SERVICE_DESCRIPTION = b'Bumble'
84PROVIDER_NAME = b'Bumble'
85HID_PARSER_VERSION = 0x0111  # uint16 0x0111 (v1.1.1)
86HID_DEVICE_SUBCLASS = 0xC0  # Combo keyboard/pointing device
87HID_COUNTRY_CODE = 0x21  # 0x21 Uint8, USA
88HID_VIRTUAL_CABLE = True  # Virtual cable enabled
89HID_RECONNECT_INITIATE = True  #  Reconnect initiate enabled
90REPORT_DESCRIPTOR_TYPE = 0x22  # 0x22 Type = Report Descriptor
91HID_LANGID_BASE_LANGUAGE = 0x0409  # 0x0409 Language = English (United States)
92HID_LANGID_BASE_BLUETOOTH_STRING_OFFSET = 0x100  # 0x0100 Default
93HID_BATTERY_POWER = True  #  Battery power enabled
94HID_REMOTE_WAKE = True  #  Remote wake enabled
95HID_SUPERVISION_TIMEOUT = 0xC80  # uint16 0xC80 (2s)
96HID_NORMALLY_CONNECTABLE = True  #  Normally connectable enabled
97HID_BOOT_DEVICE = True  #  Boot device support enabled
98HID_SSR_HOST_MAX_LATENCY = 0x640  # uint16 0x640 (1s)
99HID_SSR_HOST_MIN_TIMEOUT = 0xC80  # uint16 0xC80 (2s)
100HID_REPORT_MAP = bytes(  # Text String, 50 Octet Report Descriptor
101    # pylint: disable=line-too-long
102    [
103        0x05,
104        0x01,  # Usage Page (Generic Desktop Ctrls)
105        0x09,
106        0x06,  # Usage (Keyboard)
107        0xA1,
108        0x01,  # Collection (Application)
109        0x85,
110        0x01,  # . Report ID (1)
111        0x05,
112        0x07,  # . Usage Page (Kbrd/Keypad)
113        0x19,
114        0xE0,  # . Usage Minimum (0xE0)
115        0x29,
116        0xE7,  # . Usage Maximum (0xE7)
117        0x15,
118        0x00,  # . Logical Minimum (0)
119        0x25,
120        0x01,  # . Logical Maximum (1)
121        0x75,
122        0x01,  # . Report Size (1)
123        0x95,
124        0x08,  # . Report Count (8)
125        0x81,
126        0x02,  # . Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
127        0x95,
128        0x01,  # . Report Count (1)
129        0x75,
130        0x08,  # . Report Size (8)
131        0x81,
132        0x03,  # . Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
133        0x95,
134        0x05,  # . Report Count (5)
135        0x75,
136        0x01,  # . Report Size (1)
137        0x05,
138        0x08,  # . Usage Page (LEDs)
139        0x19,
140        0x01,  # . Usage Minimum (Num Lock)
141        0x29,
142        0x05,  # . Usage Maximum (Kana)
143        0x91,
144        0x02,  # . Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
145        0x95,
146        0x01,  # . Report Count (1)
147        0x75,
148        0x03,  # . Report Size (3)
149        0x91,
150        0x03,  # . Output (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
151        0x95,
152        0x06,  # . Report Count (6)
153        0x75,
154        0x08,  # . Report Size (8)
155        0x15,
156        0x00,  # . Logical Minimum (0)
157        0x25,
158        0x65,  # . Logical Maximum (101)
159        0x05,
160        0x07,  # . Usage Page (Kbrd/Keypad)
161        0x19,
162        0x00,  # . Usage Minimum (0x00)
163        0x29,
164        0x65,  # . Usage Maximum (0x65)
165        0x81,
166        0x00,  # . Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
167        0xC0,  # End Collection
168        0x05,
169        0x01,  # Usage Page (Generic Desktop Ctrls)
170        0x09,
171        0x02,  # Usage (Mouse)
172        0xA1,
173        0x01,  # Collection (Application)
174        0x85,
175        0x02,  # . Report ID (2)
176        0x09,
177        0x01,  # . Usage (Pointer)
178        0xA1,
179        0x00,  # . Collection (Physical)
180        0x05,
181        0x09,  # .   Usage Page (Button)
182        0x19,
183        0x01,  # .   Usage Minimum (0x01)
184        0x29,
185        0x03,  # .   Usage Maximum (0x03)
186        0x15,
187        0x00,  # .   Logical Minimum (0)
188        0x25,
189        0x01,  # .   Logical Maximum (1)
190        0x95,
191        0x03,  # .   Report Count (3)
192        0x75,
193        0x01,  # .   Report Size (1)
194        0x81,
195        0x02,  # .   Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
196        0x95,
197        0x01,  # .   Report Count (1)
198        0x75,
199        0x05,  # .   Report Size (5)
200        0x81,
201        0x03,  # .   Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
202        0x05,
203        0x01,  # .   Usage Page (Generic Desktop Ctrls)
204        0x09,
205        0x30,  # .   Usage (X)
206        0x09,
207        0x31,  # .   Usage (Y)
208        0x15,
209        0x81,  # .   Logical Minimum (-127)
210        0x25,
211        0x7F,  # .   Logical Maximum (127)
212        0x75,
213        0x08,  # .   Report Size (8)
214        0x95,
215        0x02,  # .   Report Count (2)
216        0x81,
217        0x06,  # .   Input (Data,Var,Rel,No Wrap,Linear,Preferred State,No Null Position)
218        0xC0,  # . End Collection
219        0xC0,  # End Collection
220    ]
221)
222
223
224# Default protocol mode set to report protocol
225protocol_mode = Message.ProtocolMode.REPORT_PROTOCOL
226
227
228# -----------------------------------------------------------------------------
229def sdp_records():
230    service_record_handle = 0x00010002
231    return {
232        service_record_handle: [
233            ServiceAttribute(
234                SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
235                DataElement.unsigned_integer_32(service_record_handle),
236            ),
237            ServiceAttribute(
238                SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
239                DataElement.sequence([DataElement.uuid(SDP_PUBLIC_BROWSE_ROOT)]),
240            ),
241            ServiceAttribute(
242                SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
243                DataElement.sequence(
244                    [DataElement.uuid(BT_HUMAN_INTERFACE_DEVICE_SERVICE)]
245                ),
246            ),
247            ServiceAttribute(
248                SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
249                DataElement.sequence(
250                    [
251                        DataElement.sequence(
252                            [
253                                DataElement.uuid(BT_L2CAP_PROTOCOL_ID),
254                                DataElement.unsigned_integer_16(HID_CONTROL_PSM),
255                            ]
256                        ),
257                        DataElement.sequence(
258                            [
259                                DataElement.uuid(BT_HIDP_PROTOCOL_ID),
260                            ]
261                        ),
262                    ]
263                ),
264            ),
265            ServiceAttribute(
266                SDP_LANGUAGE_BASE_ATTRIBUTE_ID_LIST_ATTRIBUTE_ID,
267                DataElement.sequence(
268                    [
269                        DataElement.unsigned_integer_16(LANGUAGE),
270                        DataElement.unsigned_integer_16(ENCODING),
271                        DataElement.unsigned_integer_16(PRIMARY_LANGUAGE_BASE_ID),
272                    ]
273                ),
274            ),
275            ServiceAttribute(
276                SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
277                DataElement.sequence(
278                    [
279                        DataElement.sequence(
280                            [
281                                DataElement.uuid(BT_HUMAN_INTERFACE_DEVICE_SERVICE),
282                                DataElement.unsigned_integer_16(VERSION_NUMBER),
283                            ]
284                        ),
285                    ]
286                ),
287            ),
288            ServiceAttribute(
289                SDP_ADDITIONAL_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
290                DataElement.sequence(
291                    [
292                        DataElement.sequence(
293                            [
294                                DataElement.sequence(
295                                    [
296                                        DataElement.uuid(BT_L2CAP_PROTOCOL_ID),
297                                        DataElement.unsigned_integer_16(
298                                            HID_INTERRUPT_PSM
299                                        ),
300                                    ]
301                                ),
302                                DataElement.sequence(
303                                    [
304                                        DataElement.uuid(BT_HIDP_PROTOCOL_ID),
305                                    ]
306                                ),
307                            ]
308                        ),
309                    ]
310                ),
311            ),
312            ServiceAttribute(
313                SDP_HID_SERVICE_NAME_ATTRIBUTE_ID,
314                DataElement(DataElement.TEXT_STRING, SERVICE_NAME),
315            ),
316            ServiceAttribute(
317                SDP_HID_SERVICE_DESCRIPTION_ATTRIBUTE_ID,
318                DataElement(DataElement.TEXT_STRING, SERVICE_DESCRIPTION),
319            ),
320            ServiceAttribute(
321                SDP_HID_PROVIDER_NAME_ATTRIBUTE_ID,
322                DataElement(DataElement.TEXT_STRING, PROVIDER_NAME),
323            ),
324            ServiceAttribute(
325                SDP_HID_PARSER_VERSION_ATTRIBUTE_ID,
326                DataElement.unsigned_integer_32(HID_PARSER_VERSION),
327            ),
328            ServiceAttribute(
329                SDP_HID_DEVICE_SUBCLASS_ATTRIBUTE_ID,
330                DataElement.unsigned_integer_32(HID_DEVICE_SUBCLASS),
331            ),
332            ServiceAttribute(
333                SDP_HID_COUNTRY_CODE_ATTRIBUTE_ID,
334                DataElement.unsigned_integer_32(HID_COUNTRY_CODE),
335            ),
336            ServiceAttribute(
337                SDP_HID_VIRTUAL_CABLE_ATTRIBUTE_ID,
338                DataElement.boolean(HID_VIRTUAL_CABLE),
339            ),
340            ServiceAttribute(
341                SDP_HID_RECONNECT_INITIATE_ATTRIBUTE_ID,
342                DataElement.boolean(HID_RECONNECT_INITIATE),
343            ),
344            ServiceAttribute(
345                SDP_HID_DESCRIPTOR_LIST_ATTRIBUTE_ID,
346                DataElement.sequence(
347                    [
348                        DataElement.sequence(
349                            [
350                                DataElement.unsigned_integer_16(REPORT_DESCRIPTOR_TYPE),
351                                DataElement(DataElement.TEXT_STRING, HID_REPORT_MAP),
352                            ]
353                        ),
354                    ]
355                ),
356            ),
357            ServiceAttribute(
358                SDP_HID_LANGID_BASE_LIST_ATTRIBUTE_ID,
359                DataElement.sequence(
360                    [
361                        DataElement.sequence(
362                            [
363                                DataElement.unsigned_integer_16(
364                                    HID_LANGID_BASE_LANGUAGE
365                                ),
366                                DataElement.unsigned_integer_16(
367                                    HID_LANGID_BASE_BLUETOOTH_STRING_OFFSET
368                                ),
369                            ]
370                        ),
371                    ]
372                ),
373            ),
374            ServiceAttribute(
375                SDP_HID_BATTERY_POWER_ATTRIBUTE_ID,
376                DataElement.boolean(HID_BATTERY_POWER),
377            ),
378            ServiceAttribute(
379                SDP_HID_REMOTE_WAKE_ATTRIBUTE_ID,
380                DataElement.boolean(HID_REMOTE_WAKE),
381            ),
382            ServiceAttribute(
383                SDP_HID_SUPERVISION_TIMEOUT_ATTRIBUTE_ID,
384                DataElement.unsigned_integer_16(HID_SUPERVISION_TIMEOUT),
385            ),
386            ServiceAttribute(
387                SDP_HID_NORMALLY_CONNECTABLE_ATTRIBUTE_ID,
388                DataElement.boolean(HID_NORMALLY_CONNECTABLE),
389            ),
390            ServiceAttribute(
391                SDP_HID_BOOT_DEVICE_ATTRIBUTE_ID,
392                DataElement.boolean(HID_BOOT_DEVICE),
393            ),
394            ServiceAttribute(
395                SDP_HID_SSR_HOST_MAX_LATENCY_ATTRIBUTE_ID,
396                DataElement.unsigned_integer_16(HID_SSR_HOST_MAX_LATENCY),
397            ),
398            ServiceAttribute(
399                SDP_HID_SSR_HOST_MIN_TIMEOUT_ATTRIBUTE_ID,
400                DataElement.unsigned_integer_16(HID_SSR_HOST_MIN_TIMEOUT),
401            ),
402        ]
403    }
404
405
406# -----------------------------------------------------------------------------
407async def get_stream_reader(pipe) -> asyncio.StreamReader:
408    loop = asyncio.get_event_loop()
409    reader = asyncio.StreamReader(loop=loop)
410    protocol = asyncio.StreamReaderProtocol(reader)
411    await loop.connect_read_pipe(lambda: protocol, pipe)
412    return reader
413
414
415class DeviceData:
416    def __init__(self) -> None:
417        self.keyboardData = bytearray(
418            [0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
419        )
420        self.mouseData = bytearray([0x02, 0x00, 0x00, 0x00])
421
422
423# Device's live data - Mouse and Keyboard will be stored in this
424deviceData = DeviceData()
425
426
427# -----------------------------------------------------------------------------
428async def keyboard_device(hid_device: HID_Device):
429
430    # Start a Websocket server to receive events from a web page
431    async def serve(websocket, _path):
432        global deviceData
433        while True:
434            try:
435                message = await websocket.recv()
436                print('Received: ', str(message))
437                parsed = json.loads(message)
438                message_type = parsed['type']
439                if message_type == 'keydown':
440                    # Only deal with keys a to z for now
441                    key = parsed['key']
442                    if len(key) == 1:
443                        code = ord(key)
444                        if ord('a') <= code <= ord('z'):
445                            hid_code = 0x04 + code - ord('a')
446                            deviceData.keyboardData = bytearray(
447                                [
448                                    0x01,
449                                    0x00,
450                                    0x00,
451                                    hid_code,
452                                    0x00,
453                                    0x00,
454                                    0x00,
455                                    0x00,
456                                    0x00,
457                                ]
458                            )
459                            hid_device.send_data(deviceData.keyboardData)
460                elif message_type == 'keyup':
461                    deviceData.keyboardData = bytearray(
462                        [0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
463                    )
464                    hid_device.send_data(deviceData.keyboardData)
465                elif message_type == "mousemove":
466                    # logical min and max values
467                    log_min = -127
468                    log_max = 127
469                    x = parsed['x']
470                    y = parsed['y']
471                    # limiting x and y values within logical max and min range
472                    x = max(log_min, min(log_max, x))
473                    y = max(log_min, min(log_max, y))
474                    deviceData.mouseData = bytearray([0x02, 0x00]) + struct.pack(
475                        ">bb", x, y
476                    )
477                    hid_device.send_data(deviceData.mouseData)
478            except websockets.exceptions.ConnectionClosedOK:
479                pass
480
481    # pylint: disable-next=no-member
482    await websockets.serve(serve, 'localhost', 8989)
483    await asyncio.get_event_loop().create_future()
484
485
486# -----------------------------------------------------------------------------
487async def main() -> None:
488    if len(sys.argv) < 3:
489        print(
490            'Usage: python run_hid_device.py <device-config> <transport-spec> <command>'
491            '  where <command> is one of:\n'
492            '  test-mode (run with menu enabled for testing)\n'
493            '  web (run a keyboard with keypress input from a web page, '
494            'see keyboard.html'
495        )
496        print('example: python run_hid_device.py hid_keyboard.json usb:0 web')
497        print('example: python run_hid_device.py hid_keyboard.json usb:0 test-mode')
498
499        return
500
501    async def handle_virtual_cable_unplug():
502        hid_host_bd_addr = str(hid_device.remote_device_bd_address)
503        await hid_device.disconnect_interrupt_channel()
504        await hid_device.disconnect_control_channel()
505        await device.keystore.delete(hid_host_bd_addr)  # type: ignore
506        connection = hid_device.connection
507        if connection is not None:
508            await connection.disconnect()
509
510    def on_hid_data_cb(pdu: bytes):
511        print(f'Received Data, PDU: {pdu.hex()}')
512
513    def on_get_report_cb(
514        report_id: int, report_type: int, buffer_size: int
515    ) -> HID_Device.GetSetStatus:
516        retValue = hid_device.GetSetStatus()
517        print(
518            "GET_REPORT report_id: "
519            + str(report_id)
520            + "report_type: "
521            + str(report_type)
522            + "buffer_size:"
523            + str(buffer_size)
524        )
525        if report_type == Message.ReportType.INPUT_REPORT:
526            if report_id == 1:
527                retValue.data = deviceData.keyboardData[1:]
528                retValue.status = hid_device.GetSetReturn.SUCCESS
529            elif report_id == 2:
530                retValue.data = deviceData.mouseData[1:]
531                retValue.status = hid_device.GetSetReturn.SUCCESS
532            else:
533                retValue.status = hid_device.GetSetReturn.REPORT_ID_NOT_FOUND
534
535            if buffer_size:
536                data_len = buffer_size - 1
537                retValue.data = retValue.data[:data_len]
538        elif report_type == Message.ReportType.OUTPUT_REPORT:
539            # This sample app has nothing to do with the report received, to enable PTS
540            # testing, we will return single byte random data.
541            retValue.data = bytearray([0x11])
542            retValue.status = hid_device.GetSetReturn.SUCCESS
543        elif report_type == Message.ReportType.FEATURE_REPORT:
544            retValue.status = hid_device.GetSetReturn.ERR_INVALID_PARAMETER
545        elif report_type == Message.ReportType.OTHER_REPORT:
546            if report_id == 3:
547                retValue.status = hid_device.GetSetReturn.REPORT_ID_NOT_FOUND
548        else:
549            retValue.status = hid_device.GetSetReturn.FAILURE
550
551        return retValue
552
553    def on_set_report_cb(
554        report_id: int, report_type: int, report_size: int, data: bytes
555    ) -> HID_Device.GetSetStatus:
556        print(
557            "SET_REPORT report_id: "
558            + str(report_id)
559            + "report_type: "
560            + str(report_type)
561            + "report_size "
562            + str(report_size)
563            + "data:"
564            + str(data)
565        )
566        if report_type == Message.ReportType.FEATURE_REPORT:
567            status = HID_Device.GetSetReturn.ERR_INVALID_PARAMETER
568        elif report_type == Message.ReportType.INPUT_REPORT:
569            if report_id == 1 and report_size != len(deviceData.keyboardData):
570                status = HID_Device.GetSetReturn.ERR_INVALID_PARAMETER
571            elif report_id == 2 and report_size != len(deviceData.mouseData):
572                status = HID_Device.GetSetReturn.ERR_INVALID_PARAMETER
573            elif report_id == 3:
574                status = HID_Device.GetSetReturn.REPORT_ID_NOT_FOUND
575            else:
576                status = HID_Device.GetSetReturn.SUCCESS
577        else:
578            status = HID_Device.GetSetReturn.SUCCESS
579
580        return HID_Device.GetSetStatus(status=status)
581
582    def on_get_protocol_cb() -> HID_Device.GetSetStatus:
583        return HID_Device.GetSetStatus(
584            data=bytes([protocol_mode]),
585            status=hid_device.GetSetReturn.SUCCESS,
586        )
587
588    def on_set_protocol_cb(protocol: int) -> HID_Device.GetSetStatus:
589        # We do not support SET_PROTOCOL.
590        print(f"SET_PROTOCOL report_id: {protocol}")
591        return HID_Device.GetSetStatus(
592            status=hid_device.GetSetReturn.ERR_UNSUPPORTED_REQUEST
593        )
594
595    def on_virtual_cable_unplug_cb():
596        print('Received Virtual Cable Unplug')
597        asyncio.create_task(handle_virtual_cable_unplug())
598
599    print('<<< connecting to HCI...')
600    async with await open_transport_or_link(sys.argv[2]) as hci_transport:
601        print('<<< connected')
602
603        # Create a device
604        device = Device.from_config_file_with_hci(
605            sys.argv[1], hci_transport.source, hci_transport.sink
606        )
607        device.classic_enabled = True
608
609        # Create and register HID device
610        hid_device = HID_Device(device)
611
612        # Register for  call backs
613        hid_device.on('interrupt_data', on_hid_data_cb)
614
615        hid_device.register_get_report_cb(on_get_report_cb)
616        hid_device.register_set_report_cb(on_set_report_cb)
617        hid_device.register_get_protocol_cb(on_get_protocol_cb)
618        hid_device.register_set_protocol_cb(on_set_protocol_cb)
619
620        # Register for virtual cable unplug call back
621        hid_device.on('virtual_cable_unplug', on_virtual_cable_unplug_cb)
622
623        # Setup the SDP to advertise HID Device service
624        device.sdp_service_records = sdp_records()
625
626        # Start the controller
627        await device.power_on()
628
629        # Start being discoverable and connectable
630        await device.set_discoverable(True)
631        await device.set_connectable(True)
632
633        async def menu():
634            reader = await get_stream_reader(sys.stdin)
635            while True:
636                print(
637                    "\n************************ HID Device Menu *****************************\n"
638                )
639                print(" 1. Connect Control Channel")
640                print(" 2. Connect Interrupt Channel")
641                print(" 3. Disconnect Control Channel")
642                print(" 4. Disconnect Interrupt Channel")
643                print(" 5. Send Report on Interrupt Channel")
644                print(" 6. Virtual Cable Unplug")
645                print(" 7. Disconnect device")
646                print(" 8. Delete Bonding")
647                print(" 9. Re-connect to device")
648                print("10. Exit ")
649                print("\nEnter your choice : \n")
650
651                choice = await reader.readline()
652                choice = choice.decode('utf-8').strip()
653
654                if choice == '1':
655                    await hid_device.connect_control_channel()
656
657                elif choice == '2':
658                    await hid_device.connect_interrupt_channel()
659
660                elif choice == '3':
661                    await hid_device.disconnect_control_channel()
662
663                elif choice == '4':
664                    await hid_device.disconnect_interrupt_channel()
665
666                elif choice == '5':
667                    print(" 1. Report ID 0x01")
668                    print(" 2. Report ID 0x02")
669                    print(" 3. Invalid Report ID")
670
671                    choice1 = await reader.readline()
672                    choice1 = choice1.decode('utf-8').strip()
673
674                    if choice1 == '1':
675                        data = bytearray(
676                            [0x01, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00]
677                        )
678                        hid_device.send_data(data)
679                        data = bytearray(
680                            [0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
681                        )
682                        hid_device.send_data(data)
683
684                    elif choice1 == '2':
685                        data = bytearray([0x02, 0x00, 0x00, 0xF6])
686                        hid_device.send_data(data)
687                        data = bytearray([0x02, 0x00, 0x00, 0x00])
688                        hid_device.send_data(data)
689
690                    elif choice1 == '3':
691                        data = bytearray([0x00, 0x00, 0x00, 0x00])
692                        hid_device.send_data(data)
693                        data = bytearray([0x00, 0x00, 0x00, 0x00])
694                        hid_device.send_data(data)
695
696                    else:
697                        print('Incorrect option selected')
698
699                elif choice == '6':
700                    hid_device.virtual_cable_unplug()
701                    try:
702                        hid_host_bd_addr = str(hid_device.remote_device_bd_address)
703                        await device.keystore.delete(hid_host_bd_addr)
704                    except KeyError:
705                        print('Device not found or Device already unpaired.')
706
707                elif choice == '7':
708                    connection = hid_device.connection
709                    if connection is not None:
710                        await connection.disconnect()
711                    else:
712                        print("Already disconnected from device")
713
714                elif choice == '8':
715                    try:
716                        hid_host_bd_addr = str(hid_device.remote_device_bd_address)
717                        await device.keystore.delete(hid_host_bd_addr)
718                    except KeyError:
719                        print('Device NOT found or Device already unpaired.')
720
721                elif choice == '9':
722                    hid_host_bd_addr = str(hid_device.remote_device_bd_address)
723                    connection = await device.connect(
724                        hid_host_bd_addr, transport=BT_BR_EDR_TRANSPORT
725                    )
726                    await connection.authenticate()
727                    await connection.encrypt()
728
729                elif choice == '10':
730                    sys.exit("Exit successful")
731
732                else:
733                    print("Invalid option selected.")
734
735        if (len(sys.argv) > 3) and (sys.argv[3] == 'test-mode'):
736            # Test mode for PTS/Unit testing
737            await menu()
738        else:
739            # default option is using keyboard.html (web)
740            print("Executing in Web mode")
741            await keyboard_device(hid_device)
742
743        await hci_transport.source.wait_for_termination()
744
745
746# -----------------------------------------------------------------------------
747logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
748asyncio.run(main())
749