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