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