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