From ba85dcbda5d69054c19e9aa43fb6d8ea1377737c Mon Sep 17 00:00:00 2001 From: dhavan Date: Wed, 22 Nov 2023 11:06:27 +0000 Subject: [PATCH 01/19] Get the changes from hid_device to bumble_hid_device Modified the get_report_cb --- bumble/classic3.json | 5 + bumble/hid.py | 235 ++++++++++--- examples/classic3.json | 5 + examples/run_hid_device.py | 705 +++++++++++++++++++++++++++++++++++++ examples/run_hid_host.py | 67 ++-- 5 files changed, 953 insertions(+), 64 deletions(-) create mode 100644 bumble/classic3.json create mode 100644 examples/classic3.json create mode 100644 examples/run_hid_device.py diff --git a/bumble/classic3.json b/bumble/classic3.json new file mode 100644 index 00000000..b7b14096 --- /dev/null +++ b/bumble/classic3.json @@ -0,0 +1,5 @@ +{ + "name": "Bumble HID Keyboard", + "class_of_device": 9664, + "keystore": "JsonKeyStore" +} diff --git a/bumble/hid.py b/bumble/hid.py index 87126584..32053233 100644 --- a/bumble/hid.py +++ b/bumble/hid.py @@ -60,6 +60,7 @@ class Message: NOT_READY = 0x01 ERR_INVALID_REPORT_ID = 0x02 ERR_UNSUPPORTED_REQUEST = 0x03 + ERR_INVALID_PARAMETER = 0x04 ERR_UNKNOWN = 0x0E ERR_FATAL = 0x0F @@ -101,12 +102,12 @@ class GetReportMessage(Message): def __bytes__(self) -> bytes: packet_bytes = bytearray() packet_bytes.append(self.report_id) - packet_bytes.extend( - [(self.buffer_size & 0xFF), ((self.buffer_size >> 8) & 0xFF)] - ) - if self.report_type == Message.ReportType.OTHER_REPORT: + if self.buffer_size == 0: return self.header(self.report_type) + packet_bytes else: + packet_bytes.extend( + [(self.buffer_size & 0xFF), ((self.buffer_size >> 8) & 0xFF)] + ) return self.header(0x08 | self.report_type) + packet_bytes @@ -119,7 +120,17 @@ class SetReportMessage(Message): def __bytes__(self) -> bytes: return self.header(self.report_type) + self.data +@dataclass +class SendControlData(Message): + report_type: int + data: bytes + message_type = Message.MessageType.DATA + def __bytes__(self) -> bytes: + packet_bytes = bytearray() + + packet_bytes.extend(self.data) + return self.header(self.report_type) + packet_bytes @dataclass class GetProtocolMessage(Message): message_type = Message.MessageType.GET_PROTOCOL @@ -136,6 +147,15 @@ class SetProtocolMessage(Message): def __bytes__(self) -> bytes: return self.header(self.protocol_mode) +@dataclass +class GetProtocolReplyMessage(Message): + protocol_mode: int + message_type = Message.MessageType.DATA + + def __bytes__(self) -> bytes: + packet_bytes = bytearray() + packet_bytes.append(self.protocol_mode) + return self.header(Message.ReportType.OTHER_REPORT) + packet_bytes @dataclass class Suspend(Message): @@ -160,25 +180,41 @@ class VirtualCableUnplug(Message): def __bytes__(self) -> bytes: return self.header(Message.ControlCommand.VIRTUAL_CABLE_UNPLUG) - +#Device sends input report, host sends output report. @dataclass class SendData(Message): data: bytes + report_type: int message_type = Message.MessageType.DATA def __bytes__(self) -> bytes: - return self.header(Message.ReportType.OUTPUT_REPORT) + self.data + return self.header(self.report_type) + self.data + +@dataclass +class SendHandshakeMessage(Message): + result_code: int + message_type = Message.MessageType.HANDSHAKE + + def __bytes__(self) -> bytes: + return self.header(self.result_code) # ----------------------------------------------------------------------------- -class Host(EventEmitter): +class HID(EventEmitter): l2cap_ctrl_channel: Optional[l2cap.ClassicChannel] l2cap_intr_channel: Optional[l2cap.ClassicChannel] - def __init__(self, device: Device, connection: Connection) -> None: + class Role(enum.IntEnum): + HOST = 0x00 + DEVICE = 0x01 + + + def __init__(self, device: Device, role: int) -> None: super().__init__() self.device = device - self.connection = connection + self.connection = None + self.remote_device_bd_address = None + self.role = role self.l2cap_ctrl_channel = None self.l2cap_intr_channel = None @@ -187,6 +223,8 @@ class Host(EventEmitter): device.register_l2cap_server(HID_CONTROL_PSM, self.on_connection) device.register_l2cap_server(HID_INTERRUPT_PSM, self.on_connection) + device.on('connection', self.on_device_connection) + async def connect_control_channel(self) -> None: # Create a new L2CAP connection - control channel try: @@ -229,9 +267,18 @@ class Host(EventEmitter): self.l2cap_ctrl_channel = None await channel.disconnect() + def on_device_connection(self, connection: Connection) -> None: + self.connection = connection + self.remote_device_bd_address = connection.peer_address + connection.on('disconnection', self.on_disconnection) + def on_connection(self, l2cap_channel: l2cap.ClassicChannel) -> None: logger.debug(f'+++ New L2CAP connection: {l2cap_channel}') l2cap_channel.on('open', lambda: self.on_l2cap_channel_open(l2cap_channel)) + l2cap_channel.on('close', lambda: self.on_l2cap_channel_close(l2cap_channel)) + + def on_disconnection(self, reason: int) -> None: + self.connection = None def on_l2cap_channel_open(self, l2cap_channel: l2cap.ClassicChannel) -> None: if l2cap_channel.psm == HID_CONTROL_PSM: @@ -242,37 +289,159 @@ class Host(EventEmitter): self.l2cap_intr_channel.sink = self.on_intr_pdu logger.debug(f'$$$ L2CAP channel open: {l2cap_channel}') + def on_l2cap_channel_close(self, l2cap_channel: l2cap.ClassicChannel) -> None: + if l2cap_channel.psm == HID_CONTROL_PSM: + self.l2cap_ctrl_channel = None + else: + self.l2cap_intr_channel = None + logger.debug(f'$$$ L2CAP channel close: {l2cap_channel}') + def on_ctrl_pdu(self, pdu: bytes) -> None: logger.debug(f'<<< HID CONTROL PDU: {pdu.hex()}') # Here we will receive all kinds of packets, parse and then call respective callbacks - message_type = pdu[0] >> 4 param = pdu[0] & 0x0F + message_type = pdu[0] >> 4 if message_type == Message.MessageType.HANDSHAKE: logger.debug(f'<<< HID HANDSHAKE: {Message.Handshake(param).name}') self.emit('handshake', Message.Handshake(param)) + elif message_type == Message.MessageType.GET_REPORT: + logger.debug('<<< HID GET REPORT') + self.handle_get_report(pdu) + + elif message_type == Message.MessageType.SET_REPORT: + logger.debug('<<< HID SET REPORT') + report_type = pdu[0] & 3 + report = pdu[2:] + report_id = pdu[1] + logger.debug(report_id) + logger.debug(report_type) + #TODO: to check for size mentioned in report descriptor + self.emit('set_report', report_id, report) + elif message_type == Message.MessageType.GET_PROTOCOL: + logger.debug('<<< HID GET PROTOCOL') + self.emit('get_protocol') + elif message_type == Message.MessageType.SET_PROTOCOL: + logger.debug('<<< HID SET PROTOCOL') + self.emit('set_protocol', param) elif message_type == Message.MessageType.DATA: logger.debug('<<< HID CONTROL DATA') - self.emit('data', pdu) + self.emit('control_data', pdu) elif message_type == Message.MessageType.CONTROL: if param == Message.ControlCommand.SUSPEND: logger.debug('<<< HID SUSPEND') - self.emit('suspend', pdu) + self.emit('suspend') elif param == Message.ControlCommand.EXIT_SUSPEND: logger.debug('<<< HID EXIT SUSPEND') - self.emit('exit_suspend', pdu) + self.emit('exit_suspend') elif param == Message.ControlCommand.VIRTUAL_CABLE_UNPLUG: logger.debug('<<< HID VIRTUAL CABLE UNPLUG') self.emit('virtual_cable_unplug') else: logger.debug('<<< HID CONTROL OPERATION UNSUPPORTED') else: - logger.debug('<<< HID CONTROL DATA') - self.emit('data', pdu) + logger.debug('<<< HID MESSAGE TYPE UNSUPPORTED') + self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST) def on_intr_pdu(self, pdu: bytes) -> None: logger.debug(f'<<< HID INTERRUPT PDU: {pdu.hex()}') - self.emit("data", pdu) + self.emit("interrupt_data", pdu) + + def send_pdu_on_ctrl(self, msg: bytes) -> None: + self.l2cap_ctrl_channel.send_pdu(msg) # type: ignore + + def send_pdu_on_intr(self, msg: bytes) -> None: + self.l2cap_intr_channel.send_pdu(msg) # type: ignore + + def send_data(self, data: bytes) -> None: + if self.role == HID.Role.HOST: + report_type = Message.ReportType.OUTPUT_REPORT + else: + report_type = Message.ReportType.INPUT_REPORT + msg = SendData(data, report_type) + hid_message = bytes(msg) + if self.l2cap_intr_channel is not None: + logger.debug(f'>>> HID INTERRUPT SEND DATA, PDU: {hid_message.hex()}') + self.send_pdu_on_intr(hid_message) + + def virtual_cable_unplug(self) -> None: + msg = VirtualCableUnplug() + hid_message = bytes(msg) + logger.debug(f'>>> HID CONTROL VIRTUAL CABLE UNPLUG, PDU: {hid_message.hex()}') + self.send_pdu_on_ctrl(hid_message) + + +# ----------------------------------------------------------------------------- + + + +class Device(HID): + class ReportStatus(enum.IntEnum): + FAILURE = 0x00 + REPORT_ID_NOT_FOUND = 0x01 + ERR_UNSUPPORTED_REQUEST = 0x02 + ERR_UNKNOWN = 0x03 + SUCCESS = 0xff + + + class GetReportStatus(): + def __init__(self) -> None: + self.status = 0 + self.data=None + + def __init__(self, device: Device) -> None: + super().__init__(device, HID.Role.DEVICE) + + def send_handshake_message(self, result_code: int) -> None: + msg = SendHandshakeMessage(result_code) + hid_message = bytes(msg) + logger.debug(f'>>> HID HANDSHAKE MESSAGE, PDU: {hid_message.hex()}') + self.send_pdu_on_ctrl(hid_message) + + def send_control_data(self,report_type: int, data: bytes): + msg = SendControlData(report_type= report_type, data=data) + hid_message = bytes(msg) + logger.debug(f'>>> HID CONTROL DATA: {hid_message.hex()}') + self.send_pdu_on_ctrl(hid_message) + + def handle_get_report(self, pdu: bytes): + ret = self.GetReportStatus() + report_type=pdu[0] & 0x03 + buffer_flag = (pdu[0] & 0x08) >> 3 + report_id = pdu[1] + logger.debug("buffer_flag: " + str(buffer_flag)) + if(buffer_flag == 1): + buffer_size = (pdu[3] << 8) | pdu[2] + else: + buffer_size = 0 + + if(self.get_report_cb != None): + ret = self.get_report_cb(report_id, report_type, buffer_size) + + if(ret.status == self.ReportStatus.FAILURE): + self.send_handshake_message(Message.Handshake.ERR_UNKNOWN) + elif(ret.status == self.ReportStatus.SUCCESS): + data = bytearray() + data.append(report_id) + data.extend(ret.data) + #TODO Check the data size and MTU size here and only then send out + #the message + self.send_control_data(report_type=report_type, data = data) + elif(ret.status == self.ReportStatus.REPORT_ID_NOT_FOUND): + self.send_handshake_message(Message.Handshake.ERR_INVALID_REPORT_ID) + elif(ret.status == self.ReportStatus.ERR_UNSUPPORTED_REQUEST): + self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST) + else: + logger.debug("GetReport callback not registered !!") + + + def register_get_report_cb(self,cb): + self.get_report_cb=cb + logger.debug("GetReport callback registered successfully") +# ----------------------------------------------------------------------------- +class Host(HID): + def __init__(self, device: Device) -> None: + super().__init__(device, HID.Role.HOST) def get_report(self, report_type: int, report_id: int, buffer_size: int) -> None: msg = GetReportMessage( @@ -282,52 +451,32 @@ class Host(EventEmitter): logger.debug(f'>>> HID CONTROL GET REPORT, PDU: {hid_message.hex()}') self.send_pdu_on_ctrl(hid_message) - def set_report(self, report_type: int, data: bytes): + def set_report(self, report_type: int, data: bytes) -> None: msg = SetReportMessage(report_type=report_type, data=data) hid_message = bytes(msg) logger.debug(f'>>> HID CONTROL SET REPORT, PDU:{hid_message.hex()}') self.send_pdu_on_ctrl(hid_message) - def get_protocol(self): + def get_protocol(self) -> None: msg = GetProtocolMessage() hid_message = bytes(msg) logger.debug(f'>>> HID CONTROL GET PROTOCOL, PDU: {hid_message.hex()}') self.send_pdu_on_ctrl(hid_message) - def set_protocol(self, protocol_mode: int): + def set_protocol(self, protocol_mode: int) -> None: msg = SetProtocolMessage(protocol_mode=protocol_mode) hid_message = bytes(msg) logger.debug(f'>>> HID CONTROL SET PROTOCOL, PDU: {hid_message.hex()}') self.send_pdu_on_ctrl(hid_message) - def send_pdu_on_ctrl(self, msg: bytes) -> None: - assert self.l2cap_ctrl_channel - self.l2cap_ctrl_channel.send_pdu(msg) - - def send_pdu_on_intr(self, msg: bytes) -> None: - assert self.l2cap_intr_channel - self.l2cap_intr_channel.send_pdu(msg) - - def send_data(self, data): - msg = SendData(data) - hid_message = bytes(msg) - logger.debug(f'>>> HID INTERRUPT SEND DATA, PDU: {hid_message.hex()}') - self.send_pdu_on_intr(hid_message) - - def suspend(self): + def suspend(self) -> None: msg = Suspend() hid_message = bytes(msg) logger.debug(f'>>> HID CONTROL SUSPEND, PDU:{hid_message.hex()}') - self.send_pdu_on_ctrl(msg) + self.send_pdu_on_ctrl(hid_message) - def exit_suspend(self): + def exit_suspend(self) -> None: msg = ExitSuspend() hid_message = bytes(msg) logger.debug(f'>>> HID CONTROL EXIT SUSPEND, PDU:{hid_message.hex()}') - self.send_pdu_on_ctrl(msg) - - def virtual_cable_unplug(self): - msg = VirtualCableUnplug() - hid_message = bytes(msg) - logger.debug(f'>>> HID CONTROL VIRTUAL CABLE UNPLUG, PDU: {hid_message.hex()}') - self.send_pdu_on_ctrl(msg) + self.send_pdu_on_ctrl(hid_message) diff --git a/examples/classic3.json b/examples/classic3.json new file mode 100644 index 00000000..b7b14096 --- /dev/null +++ b/examples/classic3.json @@ -0,0 +1,5 @@ +{ + "name": "Bumble HID Keyboard", + "class_of_device": 9664, + "keystore": "JsonKeyStore" +} diff --git a/examples/run_hid_device.py b/examples/run_hid_device.py new file mode 100644 index 00000000..40d58a97 --- /dev/null +++ b/examples/run_hid_device.py @@ -0,0 +1,705 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import asyncio +import sys +import os +import logging +import json +import websockets +from bumble.colors import color + +from bumble.device import Device +from bumble.transport import open_transport_or_link +from bumble.core import ( + BT_BR_EDR_TRANSPORT, + BT_L2CAP_PROTOCOL_ID, + BT_HUMAN_INTERFACE_DEVICE_SERVICE, + BT_HIDP_PROTOCOL_ID, + UUID, +) +from bumble.hci import Address +from bumble.hid import ( + Device as HID_Device, + HID_CONTROL_PSM, + HID_INTERRUPT_PSM, + Message, +) +from bumble.sdp import ( + Client as SDP_Client, + DataElement, + ServiceAttribute, + SDP_PUBLIC_BROWSE_ROOT, + SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID, + SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID, + SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID, + SDP_ALL_ATTRIBUTES_RANGE, + SDP_LANGUAGE_BASE_ATTRIBUTE_ID_LIST_ATTRIBUTE_ID, + SDP_ADDITIONAL_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID, + SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID, + SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID, +) +from bumble.utils import AsyncRunner + +# ----------------------------------------------------------------------------- +# SDP attributes for Bluetooth HID devices +SDP_HID_SERVICE_NAME_ATTRIBUTE_ID = 0x0100 +SDP_HID_SERVICE_DESCRIPTION_ATTRIBUTE_ID = 0x0101 +SDP_HID_PROVIDER_NAME_ATTRIBUTE_ID = 0x0102 +SDP_HID_DEVICE_RELEASE_NUMBER_ATTRIBUTE_ID = 0x0200 # [DEPRECATED] +SDP_HID_PARSER_VERSION_ATTRIBUTE_ID = 0x0201 +SDP_HID_DEVICE_SUBCLASS_ATTRIBUTE_ID = 0x0202 +SDP_HID_COUNTRY_CODE_ATTRIBUTE_ID = 0x0203 +SDP_HID_VIRTUAL_CABLE_ATTRIBUTE_ID = 0x0204 +SDP_HID_RECONNECT_INITIATE_ATTRIBUTE_ID = 0x0205 +SDP_HID_DESCRIPTOR_LIST_ATTRIBUTE_ID = 0x0206 +SDP_HID_LANGID_BASE_LIST_ATTRIBUTE_ID = 0x0207 +SDP_HID_SDP_DISABLE_ATTRIBUTE_ID = 0x0208 # [DEPRECATED] +SDP_HID_BATTERY_POWER_ATTRIBUTE_ID = 0x0209 +SDP_HID_REMOTE_WAKE_ATTRIBUTE_ID = 0x020A +SDP_HID_PROFILE_VERSION_ATTRIBUTE_ID = 0x020B # DEPRECATED] +SDP_HID_SUPERVISION_TIMEOUT_ATTRIBUTE_ID = 0x020C +SDP_HID_NORMALLY_CONNECTABLE_ATTRIBUTE_ID = 0x020D +SDP_HID_BOOT_DEVICE_ATTRIBUTE_ID = 0x020E +SDP_HID_SSR_HOST_MAX_LATENCY_ATTRIBUTE_ID = 0x020F +SDP_HID_SSR_HOST_MIN_TIMEOUT_ATTRIBUTE_ID = 0x0210 + +# HID SDP attribute values +LANGUAGE = 0x656e +ENCODING = 0x6a +PRIMARY_LANGUAGE_BASE_ID = 0x100 +VERSION_NUMBER = 0x0101 +SERVICE_NAME = b'Bumble HID' +SERVICE_DESCRIPTION = b'Bumble' +PROVIDER_NAME = b'Bumble' +HID_PARSER_VERSION = 0x0111 +HID_DEVICE_SUBCLASS = 0xC0 +HID_COUNTRY_CODE = 0x21 +HID_VIRTUAL_CABLE = True +HID_RECONNECT_INITIATE = True +REPORT_DESCRIPTOR_TYPE = 0x22 + + +HID_REPORT_MAP = bytes( + # pylint: disable=line-too-long + [ + 0x05, + 0x01, # Usage Page (Generic Desktop Ctrls) + 0x09, + 0x06, # Usage (Keyboard) + 0xA1, + 0x01, # Collection (Application) + 0x85, + 0x01, # . Report ID (1) + 0x05, + 0x07, # . Usage Page (Kbrd/Keypad) + 0x19, + 0xE0, # . Usage Minimum (0xE0) + 0x29, + 0xE7, # . Usage Maximum (0xE7) + 0x15, + 0x00, # . Logical Minimum (0) + 0x25, + 0x01, # . Logical Maximum (1) + 0x75, + 0x01, # . Report Size (1) + 0x95, + 0x08, # . Report Count (8) + 0x81, + 0x02, # . Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) + 0x95, + 0x01, # . Report Count (1) + 0x75, + 0x08, # . Report Size (8) + 0x81, + 0x03, # . Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) + 0x95, + 0x05, # . Report Count (5) + 0x75, + 0x01, # . Report Size (1) + 0x05, + 0x08, # . Usage Page (LEDs) + 0x19, + 0x01, # . Usage Minimum (Num Lock) + 0x29, + 0x05, # . Usage Maximum (Kana) + 0x91, + 0x02, # . Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) + 0x95, + 0x01, # . Report Count (1) + 0x75, + 0x03, # . Report Size (3) + 0x91, + 0x03, # . Output (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) + 0x95, + 0x06, # . Report Count (6) + 0x75, + 0x08, # . Report Size (8) + 0x15, + 0x00, # . Logical Minimum (0) + 0x25, + 0x65, # . Logical Maximum (101) + 0x05, + 0x07, # . Usage Page (Kbrd/Keypad) + 0x19, + 0x00, # . Usage Minimum (0x00) + 0x29, + 0x65, # . Usage Maximum (0x65) + 0x81, + 0x00, # . Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) + 0xC0, # End Collection + 0x05, + 0x01, # Usage Page (Generic Desktop Ctrls) + 0x09, + 0x02, # Usage (Mouse) + 0xA1, + 0x01, # Collection (Application) + 0x85, + 0x02, # . Report ID (2) + 0x09, + 0x01, # . Usage (Pointer) + 0xA1, + 0x00, # . Collection (Physical) + 0x05, + 0x09, # . Usage Page (Button) + 0x19, + 0x01, # . Usage Minimum (0x01) + 0x29, + 0x03, # . Usage Maximum (0x03) + 0x15, + 0x00, # . Logical Minimum (0) + 0x25, + 0x01, # . Logical Maximum (1) + 0x95, + 0x03, # . Report Count (3) + 0x75, + 0x01, # . Report Size (1) + 0x81, + 0x02, # . Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) + 0x95, + 0x01, # . Report Count (1) + 0x75, + 0x05, # . Report Size (5) + 0x81, + 0x03, # . Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) + 0x05, + 0x01, # . Usage Page (Generic Desktop Ctrls) + 0x09, + 0x30, # . Usage (X) + 0x09, + 0x31, # . Usage (Y) + 0x15, + 0x81, # . Logical Minimum (-127) + 0x25, + 0x7F, # . Logical Maximum (127) + 0x75, + 0x08, # . Report Size (8) + 0x95, + 0x02, # . Report Count (2) + 0x81, + 0x06, # . Input (Data,Var,Rel,No Wrap,Linear,Preferred State,No Null Position) + 0xC0, # . End Collection + 0xC0, # End Collection + ] +) +HID_LANGID_BASE_LANGUAGE = 0x0409 +HID_LANGID_BASE_BLUETOOTH_STRING_OFFSET = 0x100 +HID_BATTERY_POWER = True +HID_REMOTE_WAKE = True +HID_SUPERVISION_TIMEOUT = 0xC80 +HID_NORMALLY_CONNECTABLE = True +HID_BOOT_DEVICE = True +HID_SSR_HOST_MAX_LATENCY = 0x640 +HID_SSR_HOST_MIN_TIMEOUT = 0xC80 + +# Default protocol mode set to report protocol +protocol_mode = Message.ProtocolMode.REPORT_PROTOCOL + +# ----------------------------------------------------------------------------- +def sdp_records(): + service_record_handle = 0x00010002 + return { + service_record_handle: [ + ServiceAttribute( + SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID, + DataElement.unsigned_integer_32(service_record_handle), + ), + ServiceAttribute( + SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID, + DataElement.sequence([DataElement.uuid(SDP_PUBLIC_BROWSE_ROOT)]), + ), + ServiceAttribute( + SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID, + DataElement.sequence( + [DataElement.uuid(BT_HUMAN_INTERFACE_DEVICE_SERVICE)] + ), + ), + ServiceAttribute( + SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID, + DataElement.sequence( + [ + DataElement.sequence( + [ + DataElement.uuid(BT_L2CAP_PROTOCOL_ID), + DataElement.unsigned_integer_16(HID_CONTROL_PSM), + ] + ), + DataElement.sequence( + [ + DataElement.uuid(BT_HIDP_PROTOCOL_ID), + ] + ), + ] + ), + ), + ServiceAttribute( + SDP_LANGUAGE_BASE_ATTRIBUTE_ID_LIST_ATTRIBUTE_ID, + DataElement.sequence( + [ + DataElement.unsigned_integer_16(LANGUAGE), + DataElement.unsigned_integer_16(ENCODING), + DataElement.unsigned_integer_16(PRIMARY_LANGUAGE_BASE_ID), + ] + ), + ), + ServiceAttribute( + SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID, + DataElement.sequence( + [ + DataElement.sequence( + [ + DataElement.uuid(BT_HUMAN_INTERFACE_DEVICE_SERVICE), + DataElement.unsigned_integer_16(VERSION_NUMBER), + ] + ), + ] + ), + ), + ServiceAttribute( + SDP_ADDITIONAL_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID, + DataElement.sequence( + [ + DataElement.sequence( + [ + DataElement.sequence( + [ + DataElement.uuid(BT_L2CAP_PROTOCOL_ID), + DataElement.unsigned_integer_16(HID_INTERRUPT_PSM), + ] + ), + DataElement.sequence( + [ + DataElement.uuid(BT_HIDP_PROTOCOL_ID), + ] + ), + ] + ), + ] + ), + ), + ServiceAttribute( + SDP_HID_SERVICE_NAME_ATTRIBUTE_ID, + DataElement(DataElement.TEXT_STRING, SERVICE_NAME), + ), + ServiceAttribute( + SDP_HID_SERVICE_DESCRIPTION_ATTRIBUTE_ID, + DataElement(DataElement.TEXT_STRING, SERVICE_DESCRIPTION), + ), + ServiceAttribute( + SDP_HID_PROVIDER_NAME_ATTRIBUTE_ID, + DataElement(DataElement.TEXT_STRING, PROVIDER_NAME), + ), + ServiceAttribute( + SDP_HID_PARSER_VERSION_ATTRIBUTE_ID, + DataElement.unsigned_integer_32(HID_PARSER_VERSION), + ), + ServiceAttribute( + SDP_HID_DEVICE_SUBCLASS_ATTRIBUTE_ID, + DataElement.unsigned_integer_32(HID_DEVICE_SUBCLASS), + ), + ServiceAttribute( + SDP_HID_COUNTRY_CODE_ATTRIBUTE_ID, + DataElement.unsigned_integer_32(HID_COUNTRY_CODE), + ), + ServiceAttribute( + SDP_HID_VIRTUAL_CABLE_ATTRIBUTE_ID, + DataElement.boolean(HID_VIRTUAL_CABLE), + ), + ServiceAttribute( + SDP_HID_RECONNECT_INITIATE_ATTRIBUTE_ID, + DataElement.boolean(HID_RECONNECT_INITIATE), + ), + ServiceAttribute( + SDP_HID_DESCRIPTOR_LIST_ATTRIBUTE_ID, + DataElement.sequence( + [ + DataElement.sequence( + [ + DataElement.unsigned_integer_16(REPORT_DESCRIPTOR_TYPE), + DataElement(DataElement.TEXT_STRING, HID_REPORT_MAP), + ] + ), + ] + ), + ), + ServiceAttribute( + SDP_HID_LANGID_BASE_LIST_ATTRIBUTE_ID, + DataElement.sequence( + [ + DataElement.sequence( + [ + DataElement.unsigned_integer_16(HID_LANGID_BASE_LANGUAGE), + DataElement.unsigned_integer_16(HID_LANGID_BASE_BLUETOOTH_STRING_OFFSET), + ] + ), + ] + ), + ), + ServiceAttribute( + SDP_HID_BATTERY_POWER_ATTRIBUTE_ID, + DataElement.boolean(HID_BATTERY_POWER), + ), + ServiceAttribute( + SDP_HID_REMOTE_WAKE_ATTRIBUTE_ID, + DataElement.boolean(HID_REMOTE_WAKE), + ), + ServiceAttribute( + SDP_HID_SUPERVISION_TIMEOUT_ATTRIBUTE_ID, + DataElement.unsigned_integer_16(HID_SUPERVISION_TIMEOUT), + ), + ServiceAttribute( + SDP_HID_NORMALLY_CONNECTABLE_ATTRIBUTE_ID, + DataElement.boolean(HID_NORMALLY_CONNECTABLE), + ), + ServiceAttribute( + SDP_HID_BOOT_DEVICE_ATTRIBUTE_ID, + DataElement.boolean(HID_BOOT_DEVICE), + ), + ServiceAttribute( + SDP_HID_SSR_HOST_MAX_LATENCY_ATTRIBUTE_ID, + DataElement.unsigned_integer_16(HID_SSR_HOST_MAX_LATENCY), + ), + ServiceAttribute( + SDP_HID_SSR_HOST_MIN_TIMEOUT_ATTRIBUTE_ID, + DataElement.unsigned_integer_16(HID_SSR_HOST_MIN_TIMEOUT), + ), + ] + } + + +# ----------------------------------------------------------------------------- +async def get_stream_reader(pipe) -> asyncio.StreamReader: + loop = asyncio.get_event_loop() + reader = asyncio.StreamReader(loop=loop) + protocol = asyncio.StreamReaderProtocol(reader) + await loop.connect_read_pipe(lambda: protocol, pipe) + return reader + + +keyboardData=bytearray([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) +mouseData=bytearray([0x00, 0x00, 0x00, 0x00]) + +# ----------------------------------------------------------------------------- +async def keyboard_device(hid_device, command): + if command == 'web': + # Start a Websocket server to receive events from a web page + async def serve(websocket, _path): + global keyboardData + global mouseData + while True: + try: + message = await websocket.recv() + print('Received: ', str(message)) + parsed = json.loads(message) + message_type = parsed['type'] + if message_type == 'keydown': + # Only deal with keys a to z for now + key = parsed['key'] + if len(key) == 1: + code = ord(key) + if ord('a') <= code <= ord('z'): + hid_code = 0x04 + code - ord('a') + keyboardData = bytearray([0x01, 0x00, 0x00, hid_code, 0x00, 0x00, 0x00, 0x00, 0x00]) + hid_device.send_data(keyboardData) + elif message_type == 'keyup': + keyboardData = bytearray([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + hid_device.send_data(keyboardData) + elif message_type == "mousemove": + x = parsed['x'] + if x > 127: + x = 127 + elif x < -127: + x = -127 + y = parsed['y'] + if y > 127: + y = 127 + elif y < -127: + y = -127 + x_cord = x.to_bytes(signed = True) + y_cord = y.to_bytes(signed = True) + mouseData = bytearray([0x02, 0x00]) + x_cord + y_cord + hid_device.send_data(mouseData) + except websockets.exceptions.ConnectionClosedOK: + pass + + # pylint: disable-next=no-member + await websockets.serve(serve, 'localhost', 8989) + await asyncio.get_event_loop().create_future() + else: + message = bytes('hello', 'ascii') + while True: + for letter in message: + await asyncio.sleep(3.0) + + # Keypress for the letter + keycode = 0x04 + letter - 0x61 + keyboardData = bytearray([0x01, 0x00, 0x00, keycode, 0x00, 0x00, 0x00, 0x00, 0x00]) + hid_device.send_data(keyboardData) + + # Key release + keyboardData = bytearray([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + hid_device.send_data(keyboardData) + + +# ----------------------------------------------------------------------------- +async def main(): + if len(sys.argv) < 3: + print( + 'Usage: python run_hid_device.py ' + ' where is one of:\n' + ' test-mode (run with menu enabled for testing)\n' + ' web (run a keyboard with keypress input from a web page, ' + 'see keyboard.html' + ) + print('example: python run_hid_device.py classic3.json usb:0 web') + print('example: python run_hid_device.py classic3.json usb:0 test-mode') + + return + + async def handle_virtual_cable_unplug(): + hid_host_bd_addr = str(hid_device.remote_device_bd_address) + await hid_device.disconnect_interrupt_channel() + await hid_device.disconnect_control_channel() + await device.keystore.delete(hid_host_bd_addr) #type: ignore + connection = hid_device.connection + if connection is not None: + await connection.disconnect() + + def on_hid_data_cb(pdu): + print(f'Received Data, PDU: {pdu.hex()}') + + def on_set_report_cb(report_id: int, report: bytes): + if (report_id > 2) or (report_id == 0): + hid_device.send_handshake_message(Message.Handshake.ERR_INVALID_REPORT_ID) + print("Warning: Report ID Not Supported") + else: + hid_device.send_handshake_message(Message.Handshake.SUCCESSFUL) + print('Set Report, Report ID: ', report_id) + print('Report:', report) + + def on_get_protocol_cb(): + if HID_BOOT_DEVICE: + data = protocol_mode.to_bytes() + hid_device.send_control_data(Message.ReportType.OTHER_REPORT, data) + else: + hid_device.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST) + + def on_get_report_cb(report_id,report_type, buffer_size): + retValue = hid_device.GetReportStatus() + + if report_type == Message.ReportType.INPUT_REPORT: + print("GET_REPORT - inputType") + if report_id == 1: + retValue.data = keyboardData + retValue.status = hid_device.ReportStatus.SUCCESS + elif report_id == 2: + retValue.data = mouseData + retValue.status = hid_device.ReportStatus.SUCCESS + else: + retValue.status = hid_device.ReportStatus.REPORT_ID_NOT_FOUND + + if(buffer_size): + data_len = buffer_size -1 + retValue.data = retValue.data[:data_len] + elif report_type == Message.ReportType.OUTPUT_REPORT: + print("GET_REPORT - outputType") + #This sample app has nothing to do with the report received, to enable PTS + #testing, we will return single byte random data. + retValue.data = bytearray([0x11]) + retValue.status = hid_device.ReportStatus.SUCCESS + + elif report_type == Message.ReportType.FEATURE_REPORT: + #TBD - not requried for PTS testing + print("GET_REPORT - FeatureReport") + retValue.status = hid_device.ReportStatus.ERR_UNSUPPORTED_REQUEST + + else: + retValue.status = hid_device.ReportStatus.FAILURE + + return retValue + + def on_set_protocol_cb(param): + if HID_BOOT_DEVICE: + global protocol_mode + protocol_mode = Message.ProtocolMode(param) + hid_device.send_handshake_message(Message.Handshake.SUCCESSFUL) + else: + hid_device.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST) + + def on_virtual_cable_unplug_cb(): + print(f'Received Virtual Cable Unplug') + asyncio.create_task(handle_virtual_cable_unplug()) + + print('<<< connecting to HCI...') + async with await open_transport_or_link(sys.argv[2]) as (hci_source, hci_sink): + print('<<< connected') + + # Create a device + device = Device.from_config_file_with_hci(sys.argv[1], hci_source, hci_sink) + device.classic_enabled = True + + # Create and register HID device + hid_device = HID_Device(device) + + # Register for call backs + hid_device.on('interrupt_data', on_hid_data_cb) + hid_device.on('set_report', on_set_report_cb) + hid_device.on('get_protocol', on_get_protocol_cb) + hid_device.on('set_protocol', on_set_protocol_cb) + + hid_device.register_get_report_cb(on_get_report_cb) + + # Register for virtual cable unplug call back + hid_device.on('virtual_cable_unplug', on_virtual_cable_unplug_cb) + + # Setup the SDP to advertise HID Device service + device.sdp_service_records = sdp_records() + + # Start the controller + await device.power_on() + + # Start being discoverable and connectable + await device.set_discoverable(True) + await device.set_connectable(True) + + async def menu(): + reader = await get_stream_reader(sys.stdin) + while True: + print("\n************************ HID Device Menu *****************************\n") + print(" 1. Connect Control Channel") + print(" 2. Connect Interrupt Channel") + print(" 3. Disconnect Control Channel") + print(" 4. Disconnect Interrupt Channel") + print(" 5. Send Report") + print(" 6. Virtual Cable Unplug") + print(" 7. Disconnect device") + print(" 8. Delete Bonding") + print(" 9. Re-connect to device") + print("\nEnter your choice : \n") + + choice = await reader.readline() + choice = choice.decode('utf-8').strip() + + if choice == '1': + await hid_device.connect_control_channel() + + elif choice == '2': + await hid_device.connect_interrupt_channel() + + elif choice == '3': + await hid_device.disconnect_control_channel() + + elif choice == '4': + await hid_device.disconnect_interrupt_channel() + + elif choice == '5': + print(" 1. Report ID 0x01") + print(" 2. Report ID 0x02") + + choice1 = await reader.readline() + choice1 = choice1.decode('utf-8').strip() + + if choice1 == '1': + data = bytearray([0x01, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00]) + hid_device.send_data(data) + data = bytearray([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + hid_device.send_data(data) + + elif choice1 == '2': + data = bytearray([0x02, 0x00, 0x00, 0xf6]) + hid_device.send_data(data) + data = bytearray([0x02, 0x00, 0x00, 0x00]) + hid_device.send_data(data) + + else: + print('Incorrect option selected') + + elif choice == '6': + hid_device.virtual_cable_unplug() + try: + hid_host_bd_addr = str(hid_device.remote_device_bd_address) + await device.keystore.delete(hid_host_bd_addr) + except KeyError: + print('Device not found or Device already unpaired.') + + elif choice == '7': + connection = hid_device.connection + if connection is not None: + await connection.disconnect() + else: + print("Already disconnected from device") + + elif choice == '8': + try: + hid_host_bd_addr = str(hid_device.remote_device_bd_address) + await device.keystore.delete(hid_host_bd_addr) + except KeyError: + print('Device not found or Device already unpaired.') + + elif choice == '9': + hid_host_bd_addr = str(hid_device.remote_device_bd_address) + connection = await device.connect(hid_host_bd_addr, transport=BT_BR_EDR_TRANSPORT) + await connection.authenticate() + await connection.encrypt() + + else: + print("Invalid option selected.") + + if len(sys.argv) > 3: + command = sys.argv[3] + + if command == 'test-mode': + # Enabling menu for testing + await menu() + + elif command == 'web': + # Run as a keyboard device + await keyboard_device(hid_device, command) + + else: + print("Command incorrect. Switching to default: web") + await keyboard_device(hid_device, 'web') + + else: + await keyboard_device(hid_device, 'web') + + await hci_source.wait_for_termination() + + +# ----------------------------------------------------------------------------- +logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper()) +asyncio.run(main()) diff --git a/examples/run_hid_host.py b/examples/run_hid_host.py index a174444d..7076bdde 100644 --- a/examples/run_hid_host.py +++ b/examples/run_hid_host.py @@ -290,7 +290,10 @@ async def main(): print('example: run_hid_host.py classic1.json usb:0 E1:CA:72:48:C4:E8/P') return - def on_hid_data_cb(pdu): + def on_hid_control_data_cb(pdu: bytes): + print(f'Received Control Data, PDU: {pdu.hex()}') + + def on_hid_interrupt_data_cb(pdu: bytes): report_type = pdu[0] & 0x0F if len(pdu) == 1: print(color(f'Warning: No report received', 'yellow')) @@ -310,15 +313,17 @@ async def main(): if (report_length <= 1) or (report_id == 0): return - - if report_type == Message.ReportType.INPUT_REPORT: + #Parse report over interrupt channel + if (report_type == Message.ReportType.INPUT_REPORT): ReportParser.parse_input_report(pdu[1:]) # type: ignore async def handle_virtual_cable_unplug(): await hid_host.disconnect_interrupt_channel() await hid_host.disconnect_control_channel() await device.keystore.delete(target_address) # type: ignore - await connection.disconnect() + connection = hid_host.connection + if connection is not None: + await connection.disconnect() def on_hid_virtual_cable_unplug_cb(): asyncio.create_task(handle_virtual_cable_unplug()) @@ -330,6 +335,18 @@ async def main(): # Create a device device = Device.from_config_file_with_hci(sys.argv[1], hci_source, hci_sink) device.classic_enabled = True + + # Create HID host and start it + print('@@@ Starting HID Host...') + hid_host = Host(device) + + # Register for HID data call back + hid_host.on('interrupt_data', on_hid_interrupt_data_cb) + hid_host.on('control_data', on_hid_control_data_cb) + + # Register for virtual cable unplug call back + hid_host.on('virtual_cable_unplug', on_hid_virtual_cable_unplug_cb) + await device.power_on() # Connect to a peer @@ -350,15 +367,6 @@ async def main(): await get_hid_device_sdp_record(device, connection) - # Create HID host and start it - print('@@@ Starting HID Host...') - hid_host = Host(device, connection) - - # Register for HID data call back - hid_host.on('data', on_hid_data_cb) - - # Register for virtual cable unplug call back - hid_host.on('virtual_cable_unplug', on_hid_virtual_cable_unplug_cb) async def menu(): reader = await get_stream_reader(sys.stdin) @@ -399,24 +407,40 @@ async def main(): await hid_host.disconnect_interrupt_channel() elif choice == '5': - print(" 1. Report ID 0x02") - print(" 2. Report ID 0x03") - print(" 3. Report ID 0x05") + print(" 1. Report ID 0x02 - Input, Mouse") + print(" 2. Report ID 0x03 - Input, Keyboard") + print(" 3. Report ID 0x05 - Input, Invalid ReportId") + print(" 4. Report ID 0x02 - Output") + print(" 5. Report ID 0x05 - Feature") choice1 = await reader.readline() choice1 = choice1.decode('utf-8').strip() if choice1 == '1': - hid_host.get_report(1, 2, 3) + hid_host.get_report(1, 2, 0) elif choice1 == '2': - hid_host.get_report(2, 3, 2) - + hid_host.get_report(1, 1, 0) + elif choice1 == '3': - hid_host.get_report(3, 5, 3) + hid_host.get_report(1, 5, 0) + + elif choice1 == '4': + hid_host.get_report(2, 1, 0) + elif choice1 == '5': + hid_host.get_report(3, 5, 0) + + elif choice1 == '6': + hid_host.get_report(1, 2, 3) + + elif choice1 == '7': + hid_host.get_report(2, 3, 2) + + elif choice1 == '8': + hid_host.get_report(3, 5, 3) else: print('Incorrect option selected') - + elif choice == '6': print(" 1. Report type 1 and Report id 0x01") print(" 2. Report type 2 and Report id 0x03") @@ -489,6 +513,7 @@ async def main(): hid_host.virtual_cable_unplug() try: await device.keystore.delete(target_address) + print("Unpair successful") except KeyError: print('Device not found or Device already unpaired.') From 4c49ef94037ff748c5d8bf9b69012980d7f46151 Mon Sep 17 00:00:00 2001 From: dhavan Date: Wed, 22 Nov 2023 12:31:34 +0000 Subject: [PATCH 02/19] SET_REPORT implemented --- bumble/hid.py | 26 +++++++++++++++++--------- examples/run_hid_device.py | 16 +++++++++++----- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/bumble/hid.py b/bumble/hid.py index 32053233..23fdfb3a 100644 --- a/bumble/hid.py +++ b/bumble/hid.py @@ -308,16 +308,9 @@ class HID(EventEmitter): elif message_type == Message.MessageType.GET_REPORT: logger.debug('<<< HID GET REPORT') self.handle_get_report(pdu) - elif message_type == Message.MessageType.SET_REPORT: logger.debug('<<< HID SET REPORT') - report_type = pdu[0] & 3 - report = pdu[2:] - report_id = pdu[1] - logger.debug(report_id) - logger.debug(report_type) - #TODO: to check for size mentioned in report descriptor - self.emit('set_report', report_id, report) + self.handle_set_report(pdu) elif message_type == Message.MessageType.GET_PROTOCOL: logger.debug('<<< HID GET PROTOCOL') self.emit('get_protocol') @@ -433,11 +426,26 @@ class Device(HID): self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST) else: logger.debug("GetReport callback not registered !!") - def register_get_report_cb(self,cb): self.get_report_cb=cb logger.debug("GetReport callback registered successfully") + + def handle_set_report(self, pdu: bytes): + if(self.set_report_cb != None): + report_type=pdu[0] & 0x03 + report_id = pdu[1] + report_data = pdu[2:] + ret = self.set_report_cb(report_id, report_type, report_data) + if(ret.status == self.ReportStatus.SUCCESS): + self.send_handshake_message(Message.Handshake.SUCCESSFUL) + else: + self.send_handshake_message(Message.Handshake.ERR_INVALID_PARAMETER) + + def register_set_report_cb(self, cb): + self.set_report_cb=cb + logger.debug("SetReport callback registered successfully") + # ----------------------------------------------------------------------------- class Host(HID): def __init__(self, device: Device) -> None: diff --git a/examples/run_hid_device.py b/examples/run_hid_device.py index 40d58a97..6e855cee 100644 --- a/examples/run_hid_device.py +++ b/examples/run_hid_device.py @@ -520,9 +520,9 @@ async def main(): def on_get_report_cb(report_id,report_type, buffer_size): retValue = hid_device.GetReportStatus() - + print("GET_REPORT report_id: " + str(report_id) +"report_type: "+ str(report_type)+ + "buffer_size:" + str(buffer_size)) if report_type == Message.ReportType.INPUT_REPORT: - print("GET_REPORT - inputType") if report_id == 1: retValue.data = keyboardData retValue.status = hid_device.ReportStatus.SUCCESS @@ -536,7 +536,6 @@ async def main(): data_len = buffer_size -1 retValue.data = retValue.data[:data_len] elif report_type == Message.ReportType.OUTPUT_REPORT: - print("GET_REPORT - outputType") #This sample app has nothing to do with the report received, to enable PTS #testing, we will return single byte random data. retValue.data = bytearray([0x11]) @@ -544,13 +543,20 @@ async def main(): elif report_type == Message.ReportType.FEATURE_REPORT: #TBD - not requried for PTS testing - print("GET_REPORT - FeatureReport") retValue.status = hid_device.ReportStatus.ERR_UNSUPPORTED_REQUEST else: retValue.status = hid_device.ReportStatus.FAILURE return retValue + + def on_set_report_cb(report_id, report_type, data): + retValue = hid_device.GetReportStatus() + print("SET_REPORT report_id: " + str(report_id) +"report_type: "+ str(report_type)+ + "data:" + str(data)) + retValue.status = hid_device.ReportStatus.SUCCESS + return retValue + def on_set_protocol_cb(param): if HID_BOOT_DEVICE: @@ -577,11 +583,11 @@ async def main(): # Register for call backs hid_device.on('interrupt_data', on_hid_data_cb) - hid_device.on('set_report', on_set_report_cb) hid_device.on('get_protocol', on_get_protocol_cb) hid_device.on('set_protocol', on_set_protocol_cb) hid_device.register_get_report_cb(on_get_report_cb) + hid_device.register_set_report_cb(on_set_report_cb) # Register for virtual cable unplug call back hid_device.on('virtual_cable_unplug', on_virtual_cable_unplug_cb) From dc410b14c49f40d53b38b45cc739ae7f055cdd25 Mon Sep 17 00:00:00 2001 From: dhavan Date: Wed, 22 Nov 2023 16:05:33 +0000 Subject: [PATCH 03/19] SET_REPORT and GET_REPORT implemented --- bumble/hid.py | 44 ++++++++++++++++++++++++++++++++++---- examples/run_hid_device.py | 39 ++++++++++++--------------------- 2 files changed, 54 insertions(+), 29 deletions(-) diff --git a/bumble/hid.py b/bumble/hid.py index 23fdfb3a..9d77b5fa 100644 --- a/bumble/hid.py +++ b/bumble/hid.py @@ -313,10 +313,10 @@ class HID(EventEmitter): self.handle_set_report(pdu) elif message_type == Message.MessageType.GET_PROTOCOL: logger.debug('<<< HID GET PROTOCOL') - self.emit('get_protocol') + self.handle_get_protocol(pdu) elif message_type == Message.MessageType.SET_PROTOCOL: logger.debug('<<< HID SET PROTOCOL') - self.emit('set_protocol', param) + self.handle_set_protocol(pdu) elif message_type == Message.MessageType.DATA: logger.debug('<<< HID CONTROL DATA') self.emit('control_data', pdu) @@ -426,6 +426,7 @@ class Device(HID): self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST) else: logger.debug("GetReport callback not registered !!") + self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST) def register_get_report_cb(self,cb): self.get_report_cb=cb @@ -439,12 +440,47 @@ class Device(HID): ret = self.set_report_cb(report_id, report_type, report_data) if(ret.status == self.ReportStatus.SUCCESS): self.send_handshake_message(Message.Handshake.SUCCESSFUL) - else: - self.send_handshake_message(Message.Handshake.ERR_INVALID_PARAMETER) + return + else: + logger.debug("SetReport callback not registered !!") + + self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST) def register_set_report_cb(self, cb): self.set_report_cb=cb logger.debug("SetReport callback registered successfully") + + def handle_get_protocol(self, pdu: bytes): + ret = self.GetReportStatus() + if(self.get_protocol_cb != None): + ret=self.get_protocol_cb() + if(ret.status == self.ReportStatus.SUCCESS): + self.send_control_data(Message.ReportType.OTHER_REPORT, ret.data) + return + else: + logger.debug("GetProtocol callback not registered !!") + + self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST) + + def register_get_protocol_cb(self, cb): + self.get_protocol_cb=cb + logger.debug("GetProtocol callback registered successfully") + + def handle_set_protocol(self, pdu: bytes): + ret = self.GetReportStatus() + if(self.set_protocol_cb != None): + ret=self.set_protocol_cb(pdu[0] & 0x01) + if(ret.status == self.ReportStatus.SUCCESS): + return + else: + logger.debug("SetProtocol callback not registered !!") + + self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST) + + + def register_set_protocol_cb(self, cb): + self.set_protocol_cb=cb + logger.debug("SetProtocol callback registered successfully") # ----------------------------------------------------------------------------- class Host(HID): diff --git a/examples/run_hid_device.py b/examples/run_hid_device.py index 6e855cee..a97faa96 100644 --- a/examples/run_hid_device.py +++ b/examples/run_hid_device.py @@ -502,22 +502,6 @@ async def main(): def on_hid_data_cb(pdu): print(f'Received Data, PDU: {pdu.hex()}') - def on_set_report_cb(report_id: int, report: bytes): - if (report_id > 2) or (report_id == 0): - hid_device.send_handshake_message(Message.Handshake.ERR_INVALID_REPORT_ID) - print("Warning: Report ID Not Supported") - else: - hid_device.send_handshake_message(Message.Handshake.SUCCESSFUL) - print('Set Report, Report ID: ', report_id) - print('Report:', report) - - def on_get_protocol_cb(): - if HID_BOOT_DEVICE: - data = protocol_mode.to_bytes() - hid_device.send_control_data(Message.ReportType.OTHER_REPORT, data) - else: - hid_device.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST) - def on_get_report_cb(report_id,report_type, buffer_size): retValue = hid_device.GetReportStatus() print("GET_REPORT report_id: " + str(report_id) +"report_type: "+ str(report_type)+ @@ -558,13 +542,18 @@ async def main(): return retValue - def on_set_protocol_cb(param): - if HID_BOOT_DEVICE: - global protocol_mode - protocol_mode = Message.ProtocolMode(param) - hid_device.send_handshake_message(Message.Handshake.SUCCESSFUL) - else: - hid_device.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST) + def on_get_protocol_cb(): + retValue = hid_device.GetReportStatus() + retValue.data=protocol_mode.to_bytes() + retValue.status=hid_device.ReportStatus.SUCCESS + return retValue + + def on_set_protocol_cb(protocol): + retValue = hid_device.GetReportStatus() + #We do not support SET_PROTOCOL + print("SET_PROTOCOL report_id: " + str(protocol)) + retValue.status=hid_device.ReportStatus.ERR_UNSUPPORTED_REQUEST + return retValue def on_virtual_cable_unplug_cb(): print(f'Received Virtual Cable Unplug') @@ -583,11 +572,11 @@ async def main(): # Register for call backs hid_device.on('interrupt_data', on_hid_data_cb) - hid_device.on('get_protocol', on_get_protocol_cb) - hid_device.on('set_protocol', on_set_protocol_cb) hid_device.register_get_report_cb(on_get_report_cb) hid_device.register_set_report_cb(on_set_report_cb) + hid_device.register_get_protocol_cb(on_get_protocol_cb) + hid_device.register_set_protocol_cb(on_set_protocol_cb) # Register for virtual cable unplug call back hid_device.on('virtual_cable_unplug', on_virtual_cable_unplug_cb) From d6cefdff8e0d8b3cfb340cfa266b7416b4642320 Mon Sep 17 00:00:00 2001 From: dhavan Date: Wed, 22 Nov 2023 17:14:24 +0000 Subject: [PATCH 04/19] Renamed the status message class --- bumble/hid.py | 25 ++++++++++++------------- examples/run_hid_device.py | 26 +++++++++++++------------- 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/bumble/hid.py b/bumble/hid.py index 9d77b5fa..cd99d71e 100644 --- a/bumble/hid.py +++ b/bumble/hid.py @@ -298,7 +298,6 @@ class HID(EventEmitter): def on_ctrl_pdu(self, pdu: bytes) -> None: logger.debug(f'<<< HID CONTROL PDU: {pdu.hex()}') - # Here we will receive all kinds of packets, parse and then call respective callbacks param = pdu[0] & 0x0F message_type = pdu[0] >> 4 @@ -369,7 +368,7 @@ class HID(EventEmitter): class Device(HID): - class ReportStatus(enum.IntEnum): + class GetSetReturn(enum.IntEnum): FAILURE = 0x00 REPORT_ID_NOT_FOUND = 0x01 ERR_UNSUPPORTED_REQUEST = 0x02 @@ -377,7 +376,7 @@ class Device(HID): SUCCESS = 0xff - class GetReportStatus(): + class GetSetStatus(): def __init__(self) -> None: self.status = 0 self.data=None @@ -398,7 +397,7 @@ class Device(HID): self.send_pdu_on_ctrl(hid_message) def handle_get_report(self, pdu: bytes): - ret = self.GetReportStatus() + ret = self.GetSetStatus() report_type=pdu[0] & 0x03 buffer_flag = (pdu[0] & 0x08) >> 3 report_id = pdu[1] @@ -411,18 +410,18 @@ class Device(HID): if(self.get_report_cb != None): ret = self.get_report_cb(report_id, report_type, buffer_size) - if(ret.status == self.ReportStatus.FAILURE): + if(ret.status == self.GetSetReturn.FAILURE): self.send_handshake_message(Message.Handshake.ERR_UNKNOWN) - elif(ret.status == self.ReportStatus.SUCCESS): + elif(ret.status == self.GetSetReturn.SUCCESS): data = bytearray() data.append(report_id) data.extend(ret.data) #TODO Check the data size and MTU size here and only then send out #the message self.send_control_data(report_type=report_type, data = data) - elif(ret.status == self.ReportStatus.REPORT_ID_NOT_FOUND): + elif(ret.status == self.GetSetReturn.REPORT_ID_NOT_FOUND): self.send_handshake_message(Message.Handshake.ERR_INVALID_REPORT_ID) - elif(ret.status == self.ReportStatus.ERR_UNSUPPORTED_REQUEST): + elif(ret.status == self.GetSetReturn.ERR_UNSUPPORTED_REQUEST): self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST) else: logger.debug("GetReport callback not registered !!") @@ -438,7 +437,7 @@ class Device(HID): report_id = pdu[1] report_data = pdu[2:] ret = self.set_report_cb(report_id, report_type, report_data) - if(ret.status == self.ReportStatus.SUCCESS): + if(ret.status == self.GetSetReturn.SUCCESS): self.send_handshake_message(Message.Handshake.SUCCESSFUL) return else: @@ -451,10 +450,10 @@ class Device(HID): logger.debug("SetReport callback registered successfully") def handle_get_protocol(self, pdu: bytes): - ret = self.GetReportStatus() + ret = self.GetSetStatus() if(self.get_protocol_cb != None): ret=self.get_protocol_cb() - if(ret.status == self.ReportStatus.SUCCESS): + if(ret.status == self.GetSetReturn.SUCCESS): self.send_control_data(Message.ReportType.OTHER_REPORT, ret.data) return else: @@ -467,10 +466,10 @@ class Device(HID): logger.debug("GetProtocol callback registered successfully") def handle_set_protocol(self, pdu: bytes): - ret = self.GetReportStatus() + ret = self.GetSetStatus() if(self.set_protocol_cb != None): ret=self.set_protocol_cb(pdu[0] & 0x01) - if(ret.status == self.ReportStatus.SUCCESS): + if(ret.status == self.GetSetReturn.SUCCESS): return else: logger.debug("SetProtocol callback not registered !!") diff --git a/examples/run_hid_device.py b/examples/run_hid_device.py index a97faa96..cedf15ed 100644 --- a/examples/run_hid_device.py +++ b/examples/run_hid_device.py @@ -503,18 +503,18 @@ async def main(): print(f'Received Data, PDU: {pdu.hex()}') def on_get_report_cb(report_id,report_type, buffer_size): - retValue = hid_device.GetReportStatus() + retValue = hid_device.GetSetStatus() print("GET_REPORT report_id: " + str(report_id) +"report_type: "+ str(report_type)+ "buffer_size:" + str(buffer_size)) if report_type == Message.ReportType.INPUT_REPORT: if report_id == 1: retValue.data = keyboardData - retValue.status = hid_device.ReportStatus.SUCCESS + retValue.status = hid_device.GetSetReturn.SUCCESS elif report_id == 2: retValue.data = mouseData - retValue.status = hid_device.ReportStatus.SUCCESS + retValue.status = hid_device.GetSetReturn.SUCCESS else: - retValue.status = hid_device.ReportStatus.REPORT_ID_NOT_FOUND + retValue.status = hid_device.GetSetReturn.REPORT_ID_NOT_FOUND if(buffer_size): data_len = buffer_size -1 @@ -523,36 +523,36 @@ async def main(): #This sample app has nothing to do with the report received, to enable PTS #testing, we will return single byte random data. retValue.data = bytearray([0x11]) - retValue.status = hid_device.ReportStatus.SUCCESS + retValue.status = hid_device.GetSetReturn.SUCCESS elif report_type == Message.ReportType.FEATURE_REPORT: #TBD - not requried for PTS testing - retValue.status = hid_device.ReportStatus.ERR_UNSUPPORTED_REQUEST + retValue.status = hid_device.GetSetReturn.ERR_UNSUPPORTED_REQUEST else: - retValue.status = hid_device.ReportStatus.FAILURE + retValue.status = hid_device.GetSetReturn.FAILURE return retValue def on_set_report_cb(report_id, report_type, data): - retValue = hid_device.GetReportStatus() + retValue = hid_device.GetSetStatus() print("SET_REPORT report_id: " + str(report_id) +"report_type: "+ str(report_type)+ "data:" + str(data)) - retValue.status = hid_device.ReportStatus.SUCCESS + retValue.status = hid_device.GetSetReturn.SUCCESS return retValue def on_get_protocol_cb(): - retValue = hid_device.GetReportStatus() + retValue = hid_device.GetSetStatus() retValue.data=protocol_mode.to_bytes() - retValue.status=hid_device.ReportStatus.SUCCESS + retValue.status=hid_device.GetSetReturn.SUCCESS return retValue def on_set_protocol_cb(protocol): - retValue = hid_device.GetReportStatus() + retValue = hid_device.GetSetStatus() #We do not support SET_PROTOCOL print("SET_PROTOCOL report_id: " + str(protocol)) - retValue.status=hid_device.ReportStatus.ERR_UNSUPPORTED_REQUEST + retValue.status=hid_device.GetSetReturn.ERR_UNSUPPORTED_REQUEST return retValue def on_virtual_cable_unplug_cb(): From dc18595c8a1615f7a00862fcc558523d53a374db Mon Sep 17 00:00:00 2001 From: dhavan Date: Thu, 23 Nov 2023 05:02:16 +0000 Subject: [PATCH 05/19] MTU size check added --- bumble/hid.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/bumble/hid.py b/bumble/hid.py index cd99d71e..72a66287 100644 --- a/bumble/hid.py +++ b/bumble/hid.py @@ -224,7 +224,7 @@ class HID(EventEmitter): device.register_l2cap_server(HID_INTERRUPT_PSM, self.on_connection) device.on('connection', self.on_device_connection) - + async def connect_control_channel(self) -> None: # Create a new L2CAP connection - control channel try: @@ -383,6 +383,10 @@ class Device(HID): def __init__(self, device: Device) -> None: super().__init__(device, HID.Role.DEVICE) + self.get_report_cb = None + self.set_report_cb = None + self.get_protocol_cb = None + self.set_protocol_cb = None def send_handshake_message(self, result_code: int) -> None: msg = SendHandshakeMessage(result_code) @@ -416,9 +420,11 @@ class Device(HID): data = bytearray() data.append(report_id) data.extend(ret.data) - #TODO Check the data size and MTU size here and only then send out - #the message - self.send_control_data(report_type=report_type, data = data) + if(len(data) Date: Thu, 23 Nov 2023 06:10:52 +0000 Subject: [PATCH 06/19] deleted: bumble/classic3.json modified: examples/keyboard.html --- bumble/classic3.json | 5 ----- examples/keyboard.html | 6 +++--- 2 files changed, 3 insertions(+), 8 deletions(-) delete mode 100644 bumble/classic3.json diff --git a/bumble/classic3.json b/bumble/classic3.json deleted file mode 100644 index b7b14096..00000000 --- a/bumble/classic3.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "Bumble HID Keyboard", - "class_of_device": 9664, - "keystore": "JsonKeyStore" -} diff --git a/examples/keyboard.html b/examples/keyboard.html index 6ad83a7f..7d44a031 100644 --- a/examples/keyboard.html +++ b/examples/keyboard.html @@ -40,9 +40,9 @@ } } function onMouseMove(event) { - //console.log(event.clientX, event.clientY) - mouseInfo.innerText = `MOUSE: x=${event.clientX}, y=${event.clientY}` - send({ type:'mousemove', x: event.clientX, y: event.clientY }) + //console.log(event.movementX, event.movementY) + mouseInfo.innerText = `MOUSE: x=${event.movementX}, y=${event.movementY}` + send({ type:'mousemove', x: event.movementX, y: event.movementY }) } function onKeyDown(event) { From caf04373f34c15067a765b9130f7d6131bea449b Mon Sep 17 00:00:00 2001 From: dhavan Date: Thu, 23 Nov 2023 08:00:37 +0000 Subject: [PATCH 07/19] keyboard data moved to DeviceData class --- examples/run_hid_device.py | 72 +++++++++++++++++++++++--------------- 1 file changed, 44 insertions(+), 28 deletions(-) diff --git a/examples/run_hid_device.py b/examples/run_hid_device.py index cedf15ed..bb02849e 100644 --- a/examples/run_hid_device.py +++ b/examples/run_hid_device.py @@ -92,8 +92,15 @@ HID_COUNTRY_CODE = 0x21 HID_VIRTUAL_CABLE = True HID_RECONNECT_INITIATE = True REPORT_DESCRIPTOR_TYPE = 0x22 - - +HID_LANGID_BASE_LANGUAGE = 0x0409 +HID_LANGID_BASE_BLUETOOTH_STRING_OFFSET = 0x100 +HID_BATTERY_POWER = True +HID_REMOTE_WAKE = True +HID_SUPERVISION_TIMEOUT = 0xC80 +HID_NORMALLY_CONNECTABLE = True +HID_BOOT_DEVICE = True +HID_SSR_HOST_MAX_LATENCY = 0x640 +HID_SSR_HOST_MIN_TIMEOUT = 0xC80 HID_REPORT_MAP = bytes( # pylint: disable=line-too-long [ @@ -216,15 +223,7 @@ HID_REPORT_MAP = bytes( 0xC0, # End Collection ] ) -HID_LANGID_BASE_LANGUAGE = 0x0409 -HID_LANGID_BASE_BLUETOOTH_STRING_OFFSET = 0x100 -HID_BATTERY_POWER = True -HID_REMOTE_WAKE = True -HID_SUPERVISION_TIMEOUT = 0xC80 -HID_NORMALLY_CONNECTABLE = True -HID_BOOT_DEVICE = True -HID_SSR_HOST_MAX_LATENCY = 0x640 -HID_SSR_HOST_MIN_TIMEOUT = 0xC80 + # Default protocol mode set to report protocol protocol_mode = Message.ProtocolMode.REPORT_PROTOCOL @@ -409,17 +408,34 @@ async def get_stream_reader(pipe) -> asyncio.StreamReader: await loop.connect_read_pipe(lambda: protocol, pipe) return reader +class DeviceData: + def __init__(self) -> None: + self.keyboardData=bytearray([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + self.mouseData=bytearray([0x00, 0x00, 0x00, 0x00]) + + def getKeyBoardData(self): + return self.keyboardData + + def getMouseData(self): + return self.mouseData + + def setKeyBoardData(self, data): + self.keyboardData=data + + def setMouseData(self, data): + self.mouseData=data -keyboardData=bytearray([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) -mouseData=bytearray([0x00, 0x00, 0x00, 0x00]) +#Device's live data - Mouse and Keyboard will be stored in this +deviceData=DeviceData() # ----------------------------------------------------------------------------- async def keyboard_device(hid_device, command): + if command == 'web': # Start a Websocket server to receive events from a web page async def serve(websocket, _path): - global keyboardData - global mouseData + global deviceData + #global mouseData while True: try: message = await websocket.recv() @@ -433,11 +449,11 @@ async def keyboard_device(hid_device, command): code = ord(key) if ord('a') <= code <= ord('z'): hid_code = 0x04 + code - ord('a') - keyboardData = bytearray([0x01, 0x00, 0x00, hid_code, 0x00, 0x00, 0x00, 0x00, 0x00]) - hid_device.send_data(keyboardData) + deviceData.setKeyBoardData(bytearray([0x01, 0x00, 0x00, hid_code, 0x00, 0x00, 0x00, 0x00, 0x00])) + hid_device.send_data(deviceData.getKeyBoardData()) elif message_type == 'keyup': - keyboardData = bytearray([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) - hid_device.send_data(keyboardData) + deviceData.setKeyBoardData(bytearray([0x01, 0x00, 0x00, hid_code, 0x00, 0x00, 0x00, 0x00, 0x00])) + hid_device.send_data(deviceData.getKeyBoardData()) elif message_type == "mousemove": x = parsed['x'] if x > 127: @@ -451,8 +467,8 @@ async def keyboard_device(hid_device, command): y = -127 x_cord = x.to_bytes(signed = True) y_cord = y.to_bytes(signed = True) - mouseData = bytearray([0x02, 0x00]) + x_cord + y_cord - hid_device.send_data(mouseData) + deviceData.setMouseData(bytearray([0x02, 0x00]) + x_cord + y_cord) + hid_device.send_data(deviceData.getKeyBoardData()) except websockets.exceptions.ConnectionClosedOK: pass @@ -467,12 +483,12 @@ async def keyboard_device(hid_device, command): # Keypress for the letter keycode = 0x04 + letter - 0x61 - keyboardData = bytearray([0x01, 0x00, 0x00, keycode, 0x00, 0x00, 0x00, 0x00, 0x00]) - hid_device.send_data(keyboardData) - + deviceData.setKeyBoardData(bytearray([0x01, 0x00, 0x00, keycode, 0x00, 0x00, 0x00, 0x00, 0x00])) + hid_device.send_data(deviceData.getKeyBoardData()) # Key release - keyboardData = bytearray([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) - hid_device.send_data(keyboardData) + deviceData.setKeyBoardData(bytearray([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + hid_device.send_data(deviceData.getKeyBoardData()) + # ----------------------------------------------------------------------------- @@ -508,10 +524,10 @@ async def main(): "buffer_size:" + str(buffer_size)) if report_type == Message.ReportType.INPUT_REPORT: if report_id == 1: - retValue.data = keyboardData + retValue.data = deviceData.getKeyBoardData() retValue.status = hid_device.GetSetReturn.SUCCESS elif report_id == 2: - retValue.data = mouseData + retValue.data = deviceData.getMouseData() retValue.status = hid_device.GetSetReturn.SUCCESS else: retValue.status = hid_device.GetSetReturn.REPORT_ID_NOT_FOUND From 98a1093ebfe6fb470dfa120440f61e73022c5876 Mon Sep 17 00:00:00 2001 From: Fahad Afroze Date: Thu, 23 Nov 2023 09:53:16 +0000 Subject: [PATCH 08/19] Add review comment changes 2 Also corrected sending mouseData --- examples/run_hid_device.py | 80 +++++++++++++++++--------------------- examples/run_hid_host.py | 14 ++++--- 2 files changed, 45 insertions(+), 49 deletions(-) diff --git a/examples/run_hid_device.py b/examples/run_hid_device.py index bb02849e..e01e1a57 100644 --- a/examples/run_hid_device.py +++ b/examples/run_hid_device.py @@ -78,30 +78,31 @@ SDP_HID_BOOT_DEVICE_ATTRIBUTE_ID = 0x020E SDP_HID_SSR_HOST_MAX_LATENCY_ATTRIBUTE_ID = 0x020F SDP_HID_SSR_HOST_MIN_TIMEOUT_ATTRIBUTE_ID = 0x0210 +# Refer to HID profile specification v1.1.1, "5.3 Service Discovery Protocol (SDP)" for details # HID SDP attribute values -LANGUAGE = 0x656e -ENCODING = 0x6a -PRIMARY_LANGUAGE_BASE_ID = 0x100 -VERSION_NUMBER = 0x0101 +LANGUAGE = 0x656e # 0x656E uint16 “en” (English) +ENCODING = 0x6a # 0x006A uint16 UTF-8 encoding +PRIMARY_LANGUAGE_BASE_ID = 0x100 # 0x0100 uint16 PrimaryLanguageBaseID +VERSION_NUMBER = 0x0101 # 0x0101 uint16 version number (v1.1) SERVICE_NAME = b'Bumble HID' SERVICE_DESCRIPTION = b'Bumble' PROVIDER_NAME = b'Bumble' -HID_PARSER_VERSION = 0x0111 -HID_DEVICE_SUBCLASS = 0xC0 -HID_COUNTRY_CODE = 0x21 -HID_VIRTUAL_CABLE = True -HID_RECONNECT_INITIATE = True -REPORT_DESCRIPTOR_TYPE = 0x22 -HID_LANGID_BASE_LANGUAGE = 0x0409 -HID_LANGID_BASE_BLUETOOTH_STRING_OFFSET = 0x100 -HID_BATTERY_POWER = True -HID_REMOTE_WAKE = True -HID_SUPERVISION_TIMEOUT = 0xC80 -HID_NORMALLY_CONNECTABLE = True -HID_BOOT_DEVICE = True -HID_SSR_HOST_MAX_LATENCY = 0x640 -HID_SSR_HOST_MIN_TIMEOUT = 0xC80 -HID_REPORT_MAP = bytes( +HID_PARSER_VERSION = 0x0111 # uint16 0x0111 +HID_DEVICE_SUBCLASS = 0xC0 # Combo keyboard/pointing device +HID_COUNTRY_CODE = 0x21 # 0x21 Uint8, USA +HID_VIRTUAL_CABLE = True # 0x01 Boolean +HID_RECONNECT_INITIATE = True # 0x01 Boolean +REPORT_DESCRIPTOR_TYPE = 0x22 # 0x22 Type = Report Descriptor +HID_LANGID_BASE_LANGUAGE = 0x0409 # 0x0409 Language = English (United States) +HID_LANGID_BASE_BLUETOOTH_STRING_OFFSET = 0x100 # 0x0100 Bluetooth String Offset +HID_BATTERY_POWER = True # 0x01 Boolean +HID_REMOTE_WAKE = True # 0x01 Boolean +HID_SUPERVISION_TIMEOUT = 0xC80 # uint16 0xC80 +HID_NORMALLY_CONNECTABLE = True # 0x01 Boolean +HID_BOOT_DEVICE = True # 0x01 Boolean +HID_SSR_HOST_MAX_LATENCY = 0x640 # uint16 0x640 +HID_SSR_HOST_MIN_TIMEOUT = 0xC80 # uint16 0xC80 +HID_REPORT_MAP = bytes( # Text String, 50 Octet Report Descriptor # pylint: disable=line-too-long [ 0x05, @@ -412,16 +413,16 @@ class DeviceData: def __init__(self) -> None: self.keyboardData=bytearray([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) self.mouseData=bytearray([0x00, 0x00, 0x00, 0x00]) - + def getKeyBoardData(self): return self.keyboardData - + def getMouseData(self): return self.mouseData - + def setKeyBoardData(self, data): self.keyboardData=data - + def setMouseData(self, data): self.mouseData=data @@ -468,26 +469,13 @@ async def keyboard_device(hid_device, command): x_cord = x.to_bytes(signed = True) y_cord = y.to_bytes(signed = True) deviceData.setMouseData(bytearray([0x02, 0x00]) + x_cord + y_cord) - hid_device.send_data(deviceData.getKeyBoardData()) + hid_device.send_data(deviceData.getMouseData()) except websockets.exceptions.ConnectionClosedOK: pass # pylint: disable-next=no-member await websockets.serve(serve, 'localhost', 8989) await asyncio.get_event_loop().create_future() - else: - message = bytes('hello', 'ascii') - while True: - for letter in message: - await asyncio.sleep(3.0) - - # Keypress for the letter - keycode = 0x04 + letter - 0x61 - deviceData.setKeyBoardData(bytearray([0x01, 0x00, 0x00, keycode, 0x00, 0x00, 0x00, 0x00, 0x00])) - hid_device.send_data(deviceData.getKeyBoardData()) - # Key release - deviceData.setKeyBoardData(bytearray([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) - hid_device.send_data(deviceData.getKeyBoardData()) @@ -540,23 +528,23 @@ async def main(): #testing, we will return single byte random data. retValue.data = bytearray([0x11]) retValue.status = hid_device.GetSetReturn.SUCCESS - + elif report_type == Message.ReportType.FEATURE_REPORT: #TBD - not requried for PTS testing retValue.status = hid_device.GetSetReturn.ERR_UNSUPPORTED_REQUEST - + else: retValue.status = hid_device.GetSetReturn.FAILURE return retValue - + def on_set_report_cb(report_id, report_type, data): retValue = hid_device.GetSetStatus() print("SET_REPORT report_id: " + str(report_id) +"report_type: "+ str(report_type)+ "data:" + str(data)) retValue.status = hid_device.GetSetReturn.SUCCESS return retValue - + def on_get_protocol_cb(): retValue = hid_device.GetSetStatus() @@ -588,7 +576,7 @@ async def main(): # Register for call backs hid_device.on('interrupt_data', on_hid_data_cb) - + hid_device.register_get_report_cb(on_get_report_cb) hid_device.register_set_report_cb(on_set_report_cb) hid_device.register_get_protocol_cb(on_get_protocol_cb) @@ -620,6 +608,7 @@ async def main(): print(" 7. Disconnect device") print(" 8. Delete Bonding") print(" 9. Re-connect to device") + print("10. Exit Application") print("\nEnter your choice : \n") choice = await reader.readline() @@ -687,6 +676,9 @@ async def main(): await connection.authenticate() await connection.encrypt() + elif choice == '10': + sys.exit("Application exit successful") + else: print("Invalid option selected.") @@ -698,7 +690,7 @@ async def main(): await menu() elif command == 'web': - # Run as a keyboard device + # Run as a keyboard and mouse device await keyboard_device(hid_device, command) else: diff --git a/examples/run_hid_host.py b/examples/run_hid_host.py index 89c11de5..391d1446 100644 --- a/examples/run_hid_host.py +++ b/examples/run_hid_host.py @@ -384,6 +384,7 @@ async def main(): print("13. Disconnect device") print("14. Delete Bonding") print("15. Re-connect to device") + print("16. Exit Application") print("\nEnter your choice : \n") choice = await reader.readline() @@ -415,27 +416,27 @@ async def main(): elif choice1 == '2': hid_host.get_report(1, 1, 0) - + elif choice1 == '3': hid_host.get_report(1, 5, 0) - + elif choice1 == '4': hid_host.get_report(2, 1, 0) elif choice1 == '5': hid_host.get_report(3, 5, 0) - + elif choice1 == '6': hid_host.get_report(1, 2, 3) elif choice1 == '7': hid_host.get_report(2, 3, 2) - + elif choice1 == '8': hid_host.get_report(3, 5, 3) else: print('Incorrect option selected') - + elif choice == '6': print(" 1. Report type 1 and Report id 0x01") print(" 2. Report type 2 and Report id 0x03") @@ -538,6 +539,9 @@ async def main(): await connection.authenticate() await connection.encrypt() + elif choice == '16': + sys.exit("Application exit successful") + else: print("Invalid option selected.") From 6ab41c466f56a854b95ee514373bbf1dbf0ea993 Mon Sep 17 00:00:00 2001 From: Fahad Afroze Date: Thu, 23 Nov 2023 12:27:56 +0000 Subject: [PATCH 09/19] Add review comment changes 3 --- examples/run_hid_device.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/run_hid_device.py b/examples/run_hid_device.py index e01e1a57..7585b6f0 100644 --- a/examples/run_hid_device.py +++ b/examples/run_hid_device.py @@ -87,21 +87,21 @@ VERSION_NUMBER = 0x0101 # 0x0101 uint16 version number (v1.1) SERVICE_NAME = b'Bumble HID' SERVICE_DESCRIPTION = b'Bumble' PROVIDER_NAME = b'Bumble' -HID_PARSER_VERSION = 0x0111 # uint16 0x0111 +HID_PARSER_VERSION = 0x0111 # uint16 0x0111 (v1.1.1) HID_DEVICE_SUBCLASS = 0xC0 # Combo keyboard/pointing device HID_COUNTRY_CODE = 0x21 # 0x21 Uint8, USA -HID_VIRTUAL_CABLE = True # 0x01 Boolean -HID_RECONNECT_INITIATE = True # 0x01 Boolean +HID_VIRTUAL_CABLE = True # Virtual cable enabled +HID_RECONNECT_INITIATE = True # Reconnect initiate enabled REPORT_DESCRIPTOR_TYPE = 0x22 # 0x22 Type = Report Descriptor HID_LANGID_BASE_LANGUAGE = 0x0409 # 0x0409 Language = English (United States) HID_LANGID_BASE_BLUETOOTH_STRING_OFFSET = 0x100 # 0x0100 Bluetooth String Offset -HID_BATTERY_POWER = True # 0x01 Boolean -HID_REMOTE_WAKE = True # 0x01 Boolean -HID_SUPERVISION_TIMEOUT = 0xC80 # uint16 0xC80 -HID_NORMALLY_CONNECTABLE = True # 0x01 Boolean -HID_BOOT_DEVICE = True # 0x01 Boolean -HID_SSR_HOST_MAX_LATENCY = 0x640 # uint16 0x640 -HID_SSR_HOST_MIN_TIMEOUT = 0xC80 # uint16 0xC80 +HID_BATTERY_POWER = True # Battery power enabled +HID_REMOTE_WAKE = True # Remote wake enabled +HID_SUPERVISION_TIMEOUT = 0xC80 # uint16 0xC80 (2s) +HID_NORMALLY_CONNECTABLE = True # Normally connectable enabled +HID_BOOT_DEVICE = True # Boot device support enabled +HID_SSR_HOST_MAX_LATENCY = 0x640 # uint16 0x640 (1s) +HID_SSR_HOST_MIN_TIMEOUT = 0xC80 # uint16 0xC80 (2s) HID_REPORT_MAP = bytes( # Text String, 50 Octet Report Descriptor # pylint: disable=line-too-long [ From 0578e84586482228578d85cd7c8b97a214de915b Mon Sep 17 00:00:00 2001 From: skarnataki Date: Thu, 23 Nov 2023 15:43:18 +0000 Subject: [PATCH 10/19] Menu and name change review comments fix --- bumble/hid.py | 32 ++++++++++++++++---------------- examples/run_hid_device.py | 32 ++++++++++++++++++++------------ examples/run_hid_host.py | 31 +++++++++++++++++-------------- 3 files changed, 53 insertions(+), 42 deletions(-) diff --git a/bumble/hid.py b/bumble/hid.py index 72a66287..066cdd2e 100644 --- a/bumble/hid.py +++ b/bumble/hid.py @@ -345,7 +345,7 @@ class HID(EventEmitter): def send_pdu_on_intr(self, msg: bytes) -> None: self.l2cap_intr_channel.send_pdu(msg) # type: ignore - def send_data(self, data: bytes) -> None: + def send_report_on_interrupt(self, data: bytes) -> None: if self.role == HID.Role.HOST: report_type = Message.ReportType.OUTPUT_REPORT else: @@ -375,12 +375,12 @@ class Device(HID): ERR_UNKNOWN = 0x03 SUCCESS = 0xff - + class GetSetStatus(): def __init__(self) -> None: self.status = 0 self.data=None - + def __init__(self, device: Device) -> None: super().__init__(device, HID.Role.DEVICE) self.get_report_cb = None @@ -399,7 +399,7 @@ class Device(HID): hid_message = bytes(msg) logger.debug(f'>>> HID CONTROL DATA: {hid_message.hex()}') self.send_pdu_on_ctrl(hid_message) - + def handle_get_report(self, pdu: bytes): ret = self.GetSetStatus() report_type=pdu[0] & 0x03 @@ -413,7 +413,7 @@ class Device(HID): if(self.get_report_cb != None): ret = self.get_report_cb(report_id, report_type, buffer_size) - + if(ret.status == self.GetSetReturn.FAILURE): self.send_handshake_message(Message.Handshake.ERR_UNKNOWN) elif(ret.status == self.GetSetReturn.SUCCESS): @@ -424,19 +424,19 @@ class Device(HID): self.send_control_data(report_type=report_type, data = data) else: self.send_handshake_message(Message.Handshake.ERR_INVALID_PARAMETER) - - elif(ret.status == self.GetSetReturn.REPORT_ID_NOT_FOUND): + + elif(ret.status == self.GetSetReturn.REPORT_ID_NOT_FOUND): self.send_handshake_message(Message.Handshake.ERR_INVALID_REPORT_ID) - elif(ret.status == self.GetSetReturn.ERR_UNSUPPORTED_REQUEST): + elif(ret.status == self.GetSetReturn.ERR_UNSUPPORTED_REQUEST): self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST) else: logger.debug("GetReport callback not registered !!") self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST) - + def register_get_report_cb(self,cb): self.get_report_cb=cb logger.debug("GetReport callback registered successfully") - + def handle_set_report(self, pdu: bytes): if(self.set_report_cb != None): report_type=pdu[0] & 0x03 @@ -448,13 +448,13 @@ class Device(HID): return else: logger.debug("SetReport callback not registered !!") - + self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST) def register_set_report_cb(self, cb): self.set_report_cb=cb logger.debug("SetReport callback registered successfully") - + def handle_get_protocol(self, pdu: bytes): ret = self.GetSetStatus() if(self.get_protocol_cb != None): @@ -470,7 +470,7 @@ class Device(HID): def register_get_protocol_cb(self, cb): self.get_protocol_cb=cb logger.debug("GetProtocol callback registered successfully") - + def handle_set_protocol(self, pdu: bytes): ret = self.GetSetStatus() if(self.set_protocol_cb != None): @@ -479,10 +479,10 @@ class Device(HID): return else: logger.debug("SetProtocol callback not registered !!") - + self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST) - - + + def register_set_protocol_cb(self, cb): self.set_protocol_cb=cb logger.debug("SetProtocol callback registered successfully") diff --git a/examples/run_hid_device.py b/examples/run_hid_device.py index 7585b6f0..512f74a1 100644 --- a/examples/run_hid_device.py +++ b/examples/run_hid_device.py @@ -451,10 +451,10 @@ async def keyboard_device(hid_device, command): if ord('a') <= code <= ord('z'): hid_code = 0x04 + code - ord('a') deviceData.setKeyBoardData(bytearray([0x01, 0x00, 0x00, hid_code, 0x00, 0x00, 0x00, 0x00, 0x00])) - hid_device.send_data(deviceData.getKeyBoardData()) + hid_device.send_report_on_interrupt(deviceData.getKeyBoardData()) elif message_type == 'keyup': deviceData.setKeyBoardData(bytearray([0x01, 0x00, 0x00, hid_code, 0x00, 0x00, 0x00, 0x00, 0x00])) - hid_device.send_data(deviceData.getKeyBoardData()) + hid_device.send_report_on_interrupt(deviceData.getKeyBoardData()) elif message_type == "mousemove": x = parsed['x'] if x > 127: @@ -469,7 +469,7 @@ async def keyboard_device(hid_device, command): x_cord = x.to_bytes(signed = True) y_cord = y.to_bytes(signed = True) deviceData.setMouseData(bytearray([0x02, 0x00]) + x_cord + y_cord) - hid_device.send_data(deviceData.getMouseData()) + hid_device.send_report_on_interrupt(deviceData.getMouseData()) except websockets.exceptions.ConnectionClosedOK: pass @@ -554,7 +554,7 @@ async def main(): def on_set_protocol_cb(protocol): retValue = hid_device.GetSetStatus() - #We do not support SET_PROTOCOL + #We do not support SET_PROTOCOL. print("SET_PROTOCOL report_id: " + str(protocol)) retValue.status=hid_device.GetSetReturn.ERR_UNSUPPORTED_REQUEST return retValue @@ -603,12 +603,12 @@ async def main(): print(" 2. Connect Interrupt Channel") print(" 3. Disconnect Control Channel") print(" 4. Disconnect Interrupt Channel") - print(" 5. Send Report") + print(" 5. Send Report on Interrupt Channel") print(" 6. Virtual Cable Unplug") print(" 7. Disconnect device") print(" 8. Delete Bonding") print(" 9. Re-connect to device") - print("10. Exit Application") + print("10. Exit ") print("\nEnter your choice : \n") choice = await reader.readline() @@ -629,21 +629,28 @@ async def main(): elif choice == '5': print(" 1. Report ID 0x01") print(" 2. Report ID 0x02") + print(" 3. Invalid Report ID") choice1 = await reader.readline() choice1 = choice1.decode('utf-8').strip() if choice1 == '1': data = bytearray([0x01, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00]) - hid_device.send_data(data) + hid_device.send_report_on_interrupt(data) data = bytearray([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) - hid_device.send_data(data) + hid_device.send_report_on_interrupt(data) elif choice1 == '2': data = bytearray([0x02, 0x00, 0x00, 0xf6]) - hid_device.send_data(data) + hid_device.send_report_on_interrupt(data) data = bytearray([0x02, 0x00, 0x00, 0x00]) - hid_device.send_data(data) + hid_device.send_report_on_interrupt(data) + + elif choice1 == '3': + data = bytearray([0x00, 0x00, 0x00, 0x00]) + hid_device.send_report_on_interrupt(data) + data = bytearray([0x00, 0x00, 0x00, 0x00]) + hid_device.send_report_on_interrupt(data) else: print('Incorrect option selected') @@ -668,7 +675,7 @@ async def main(): hid_host_bd_addr = str(hid_device.remote_device_bd_address) await device.keystore.delete(hid_host_bd_addr) except KeyError: - print('Device not found or Device already unpaired.') + print('Device NOT found or Device already unpaired.') elif choice == '9': hid_host_bd_addr = str(hid_device.remote_device_bd_address) @@ -677,7 +684,7 @@ async def main(): await connection.encrypt() elif choice == '10': - sys.exit("Application exit successful") + sys.exit("Exit successful") else: print("Invalid option selected.") @@ -698,6 +705,7 @@ async def main(): await keyboard_device(hid_device, 'web') else: + #default option is using keyboard.html (web) await keyboard_device(hid_device, 'web') await hci_source.wait_for_termination() diff --git a/examples/run_hid_host.py b/examples/run_hid_host.py index 391d1446..fe9e8116 100644 --- a/examples/run_hid_host.py +++ b/examples/run_hid_host.py @@ -377,14 +377,14 @@ async def main(): print(" 6. Set Report") print(" 7. Set Protocol Mode") print(" 8. Get Protocol Mode") - print(" 9. Send Report") + print(" 9. Send Report on Interrupt Channel") print("10. Suspend") print("11. Exit Suspend") print("12. Virtual Cable Unplug") print("13. Disconnect device") print("14. Delete Bonding") print("15. Re-connect to device") - print("16. Exit Application") + print("16. Exit") print("\nEnter your choice : \n") choice = await reader.readline() @@ -403,28 +403,31 @@ async def main(): await hid_host.disconnect_interrupt_channel() elif choice == '5': - print(" 1. Report ID 0x02 - Input, Mouse") - print(" 2. Report ID 0x03 - Input, Keyboard") - print(" 3. Report ID 0x05 - Input, Invalid ReportId") - print(" 4. Report ID 0x02 - Output") - print(" 5. Report ID 0x05 - Feature") + print(" 1. Input Report with ID 0x01") + print(" 2. Input Report with ID 0x02") + print(" 3. Input Report with ID 0x0F - Invalid ReportId") + print(" 4. Output Report with ID 0x02") + print(" 5. Feature Report with ID 0x05 - Unsupported Request") + print(" 6. Input Report with ID 0x02, BufferSize 3") + print(" 7. Output Report with ID 0x03, BufferSize 2") + print(" 8. Feature Report with ID 0x05, BufferSize 3") choice1 = await reader.readline() choice1 = choice1.decode('utf-8').strip() if choice1 == '1': - hid_host.get_report(1, 2, 0) + hid_host.get_report(1, 1, 0) elif choice1 == '2': - hid_host.get_report(1, 1, 0) + hid_host.get_report(1, 2, 0) elif choice1 == '3': hid_host.get_report(1, 5, 0) elif choice1 == '4': - hid_host.get_report(2, 1, 0) + hid_host.get_report(2, 2, 0) elif choice1 == '5': - hid_host.get_report(3, 5, 0) + hid_host.get_report(3, 15, 0) elif choice1 == '6': hid_host.get_report(1, 2, 3) @@ -490,11 +493,11 @@ async def main(): data = bytearray( [0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] ) - hid_host.send_data(data) + hid_host.send_report_on_interrupt(data) elif choice1 == '2': data = bytearray([0x03, 0x00, 0x0D, 0xFD, 0x00, 0x00]) - hid_host.send_data(data) + hid_host.send_report_on_interrupt(data) else: print('Incorrect option selected') @@ -540,7 +543,7 @@ async def main(): await connection.encrypt() elif choice == '16': - sys.exit("Application exit successful") + sys.exit("Exit successful") else: print("Invalid option selected.") From 0f29052adead9ad2fc43f41939842105682dd584 Mon Sep 17 00:00:00 2001 From: Fahad Afroze Date: Thu, 23 Nov 2023 17:46:55 +0000 Subject: [PATCH 11/19] Added mousemove changes Also modified keyboard data on keyup --- bumble/hid.py | 4 ++-- examples/run_hid_device.py | 25 +++++++++++-------------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/bumble/hid.py b/bumble/hid.py index 066cdd2e..8c36bdb2 100644 --- a/bumble/hid.py +++ b/bumble/hid.py @@ -224,7 +224,7 @@ class HID(EventEmitter): device.register_l2cap_server(HID_INTERRUPT_PSM, self.on_connection) device.on('connection', self.on_device_connection) - + async def connect_control_channel(self) -> None: # Create a new L2CAP connection - control channel try: @@ -450,7 +450,7 @@ class Device(HID): logger.debug("SetReport callback not registered !!") self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST) - + def register_set_report_cb(self, cb): self.set_report_cb=cb logger.debug("SetReport callback registered successfully") diff --git a/examples/run_hid_device.py b/examples/run_hid_device.py index 512f74a1..dd1ce575 100644 --- a/examples/run_hid_device.py +++ b/examples/run_hid_device.py @@ -94,7 +94,7 @@ HID_VIRTUAL_CABLE = True # Virtual cable enabled HID_RECONNECT_INITIATE = True # Reconnect initiate enabled REPORT_DESCRIPTOR_TYPE = 0x22 # 0x22 Type = Report Descriptor HID_LANGID_BASE_LANGUAGE = 0x0409 # 0x0409 Language = English (United States) -HID_LANGID_BASE_BLUETOOTH_STRING_OFFSET = 0x100 # 0x0100 Bluetooth String Offset +HID_LANGID_BASE_BLUETOOTH_STRING_OFFSET = 0x100 # 0x0100 Default HID_BATTERY_POWER = True # Battery power enabled HID_REMOTE_WAKE = True # Remote wake enabled HID_SUPERVISION_TIMEOUT = 0xC80 # uint16 0xC80 (2s) @@ -436,7 +436,6 @@ async def keyboard_device(hid_device, command): # Start a Websocket server to receive events from a web page async def serve(websocket, _path): global deviceData - #global mouseData while True: try: message = await websocket.recv() @@ -453,19 +452,17 @@ async def keyboard_device(hid_device, command): deviceData.setKeyBoardData(bytearray([0x01, 0x00, 0x00, hid_code, 0x00, 0x00, 0x00, 0x00, 0x00])) hid_device.send_report_on_interrupt(deviceData.getKeyBoardData()) elif message_type == 'keyup': - deviceData.setKeyBoardData(bytearray([0x01, 0x00, 0x00, hid_code, 0x00, 0x00, 0x00, 0x00, 0x00])) + deviceData.setKeyBoardData(bytearray([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) hid_device.send_report_on_interrupt(deviceData.getKeyBoardData()) elif message_type == "mousemove": + # logical min and max values + log_min = -127 + log_max = 127 x = parsed['x'] - if x > 127: - x = 127 - elif x < -127: - x = -127 y = parsed['y'] - if y > 127: - y = 127 - elif y < -127: - y = -127 + # limiting x and y values within logical max and min range + x = max(log_min, min(log_max, x)) + y = max(log_min, min(log_max, y)) x_cord = x.to_bytes(signed = True) y_cord = y.to_bytes(signed = True) deviceData.setMouseData(bytearray([0x02, 0x00]) + x_cord + y_cord) @@ -508,7 +505,7 @@ async def main(): def on_get_report_cb(report_id,report_type, buffer_size): retValue = hid_device.GetSetStatus() - print("GET_REPORT report_id: " + str(report_id) +"report_type: "+ str(report_type)+ + print("GET_REPORT report_id: " + str(report_id) +"report_type: "+ str(report_type)+ "buffer_size:" + str(buffer_size)) if report_type == Message.ReportType.INPUT_REPORT: if report_id == 1: @@ -524,7 +521,7 @@ async def main(): data_len = buffer_size -1 retValue.data = retValue.data[:data_len] elif report_type == Message.ReportType.OUTPUT_REPORT: - #This sample app has nothing to do with the report received, to enable PTS + #This sample app has nothing to do with the report received, to enable PTS #testing, we will return single byte random data. retValue.data = bytearray([0x11]) retValue.status = hid_device.GetSetReturn.SUCCESS @@ -540,7 +537,7 @@ async def main(): def on_set_report_cb(report_id, report_type, data): retValue = hid_device.GetSetStatus() - print("SET_REPORT report_id: " + str(report_id) +"report_type: "+ str(report_type)+ + print("SET_REPORT report_id: " + str(report_id) +"report_type: "+ str(report_type)+ "data:" + str(data)) retValue.status = hid_device.GetSetReturn.SUCCESS return retValue From d1033c018abdf32eabd3633adbc435580d5ce08e Mon Sep 17 00:00:00 2001 From: Fahad Afroze Date: Fri, 24 Nov 2023 05:42:31 +0000 Subject: [PATCH 12/19] Modified DeviceData class --- bumble/hid.py | 6 ++++-- examples/run_hid_device.py | 33 +++++++++++---------------------- 2 files changed, 15 insertions(+), 24 deletions(-) diff --git a/bumble/hid.py b/bumble/hid.py index 8c36bdb2..f50442a7 100644 --- a/bumble/hid.py +++ b/bumble/hid.py @@ -340,10 +340,12 @@ class HID(EventEmitter): self.emit("interrupt_data", pdu) def send_pdu_on_ctrl(self, msg: bytes) -> None: - self.l2cap_ctrl_channel.send_pdu(msg) # type: ignore + assert self.l2cap_ctrl_channel + self.l2cap_ctrl_channel.send_pdu(msg) def send_pdu_on_intr(self, msg: bytes) -> None: - self.l2cap_intr_channel.send_pdu(msg) # type: ignore + assert self.l2cap_intr_channel + self.l2cap_intr_channel.send_pdu(msg) def send_report_on_interrupt(self, data: bytes) -> None: if self.role == HID.Role.HOST: diff --git a/examples/run_hid_device.py b/examples/run_hid_device.py index dd1ce575..3287cb1f 100644 --- a/examples/run_hid_device.py +++ b/examples/run_hid_device.py @@ -409,22 +409,11 @@ async def get_stream_reader(pipe) -> asyncio.StreamReader: await loop.connect_read_pipe(lambda: protocol, pipe) return reader + class DeviceData: def __init__(self) -> None: - self.keyboardData=bytearray([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) - self.mouseData=bytearray([0x00, 0x00, 0x00, 0x00]) - - def getKeyBoardData(self): - return self.keyboardData - - def getMouseData(self): - return self.mouseData - - def setKeyBoardData(self, data): - self.keyboardData=data - - def setMouseData(self, data): - self.mouseData=data + self.keyboardData = bytearray([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + self.mouseData = bytearray([0x00, 0x00, 0x00, 0x00]) #Device's live data - Mouse and Keyboard will be stored in this deviceData=DeviceData() @@ -449,11 +438,11 @@ async def keyboard_device(hid_device, command): code = ord(key) if ord('a') <= code <= ord('z'): hid_code = 0x04 + code - ord('a') - deviceData.setKeyBoardData(bytearray([0x01, 0x00, 0x00, hid_code, 0x00, 0x00, 0x00, 0x00, 0x00])) - hid_device.send_report_on_interrupt(deviceData.getKeyBoardData()) + deviceData.keyboardData = bytearray([0x01, 0x00, 0x00, hid_code, 0x00, 0x00, 0x00, 0x00, 0x00]) + hid_device.send_report_on_interrupt(deviceData.keyboardData) elif message_type == 'keyup': - deviceData.setKeyBoardData(bytearray([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) - hid_device.send_report_on_interrupt(deviceData.getKeyBoardData()) + deviceData.keyboardData = bytearray([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + hid_device.send_report_on_interrupt(deviceData.keyboardData) elif message_type == "mousemove": # logical min and max values log_min = -127 @@ -465,8 +454,8 @@ async def keyboard_device(hid_device, command): y = max(log_min, min(log_max, y)) x_cord = x.to_bytes(signed = True) y_cord = y.to_bytes(signed = True) - deviceData.setMouseData(bytearray([0x02, 0x00]) + x_cord + y_cord) - hid_device.send_report_on_interrupt(deviceData.getMouseData()) + deviceData.mouseData = bytearray([0x02, 0x00]) + x_cord + y_cord + hid_device.send_report_on_interrupt(deviceData.mouseData) except websockets.exceptions.ConnectionClosedOK: pass @@ -509,10 +498,10 @@ async def main(): "buffer_size:" + str(buffer_size)) if report_type == Message.ReportType.INPUT_REPORT: if report_id == 1: - retValue.data = deviceData.getKeyBoardData() + retValue.data = deviceData.keyboardData retValue.status = hid_device.GetSetReturn.SUCCESS elif report_id == 2: - retValue.data = deviceData.getMouseData() + retValue.data = deviceData.mouseData retValue.status = hid_device.GetSetReturn.SUCCESS else: retValue.status = hid_device.GetSetReturn.REPORT_ID_NOT_FOUND From 9324237828104ea39ccffe72629790b549097802 Mon Sep 17 00:00:00 2001 From: skarnataki Date: Fri, 24 Nov 2023 06:11:52 +0000 Subject: [PATCH 13/19] send_data comment fix and lint error fix --- bumble/hid.py | 86 +++++++++++++++------------- examples/run_hid_device.py | 114 +++++++++++++++++++++---------------- examples/run_hid_host.py | 9 ++- 3 files changed, 113 insertions(+), 96 deletions(-) diff --git a/bumble/hid.py b/bumble/hid.py index f50442a7..059621c6 100644 --- a/bumble/hid.py +++ b/bumble/hid.py @@ -120,6 +120,7 @@ class SetReportMessage(Message): def __bytes__(self) -> bytes: return self.header(self.report_type) + self.data + @dataclass class SendControlData(Message): report_type: int @@ -131,6 +132,8 @@ class SendControlData(Message): packet_bytes.extend(self.data) return self.header(self.report_type) + packet_bytes + + @dataclass class GetProtocolMessage(Message): message_type = Message.MessageType.GET_PROTOCOL @@ -147,6 +150,7 @@ class SetProtocolMessage(Message): def __bytes__(self) -> bytes: return self.header(self.protocol_mode) + @dataclass class GetProtocolReplyMessage(Message): protocol_mode: int @@ -157,6 +161,7 @@ class GetProtocolReplyMessage(Message): packet_bytes.append(self.protocol_mode) return self.header(Message.ReportType.OTHER_REPORT) + packet_bytes + @dataclass class Suspend(Message): message_type = Message.MessageType.CONTROL @@ -180,7 +185,8 @@ class VirtualCableUnplug(Message): def __bytes__(self) -> bytes: return self.header(Message.ControlCommand.VIRTUAL_CABLE_UNPLUG) -#Device sends input report, host sends output report. + +# Device sends input report, host sends output report. @dataclass class SendData(Message): data: bytes @@ -190,6 +196,7 @@ class SendData(Message): def __bytes__(self) -> bytes: return self.header(self.report_type) + self.data + @dataclass class SendHandshakeMessage(Message): result_code: int @@ -208,7 +215,6 @@ class HID(EventEmitter): HOST = 0x00 DEVICE = 0x01 - def __init__(self, device: Device, role: int) -> None: super().__init__() self.device = device @@ -347,7 +353,7 @@ class HID(EventEmitter): assert self.l2cap_intr_channel self.l2cap_intr_channel.send_pdu(msg) - def send_report_on_interrupt(self, data: bytes) -> None: + def send_data(self, data: bytes) -> None: if self.role == HID.Role.HOST: report_type = Message.ReportType.OUTPUT_REPORT else: @@ -368,20 +374,18 @@ class HID(EventEmitter): # ----------------------------------------------------------------------------- - class Device(HID): class GetSetReturn(enum.IntEnum): FAILURE = 0x00 REPORT_ID_NOT_FOUND = 0x01 ERR_UNSUPPORTED_REQUEST = 0x02 ERR_UNKNOWN = 0x03 - SUCCESS = 0xff + SUCCESS = 0xFF - - class GetSetStatus(): + class GetSetStatus: def __init__(self) -> None: self.status = 0 - self.data=None + self.data = None def __init__(self, device: Device) -> None: super().__init__(device, HID.Role.DEVICE) @@ -396,56 +400,56 @@ class Device(HID): logger.debug(f'>>> HID HANDSHAKE MESSAGE, PDU: {hid_message.hex()}') self.send_pdu_on_ctrl(hid_message) - def send_control_data(self,report_type: int, data: bytes): - msg = SendControlData(report_type= report_type, data=data) + def send_control_data(self, report_type: int, data: bytes): + msg = SendControlData(report_type=report_type, data=data) hid_message = bytes(msg) logger.debug(f'>>> HID CONTROL DATA: {hid_message.hex()}') self.send_pdu_on_ctrl(hid_message) def handle_get_report(self, pdu: bytes): ret = self.GetSetStatus() - report_type=pdu[0] & 0x03 + report_type = pdu[0] & 0x03 buffer_flag = (pdu[0] & 0x08) >> 3 report_id = pdu[1] logger.debug("buffer_flag: " + str(buffer_flag)) - if(buffer_flag == 1): + if buffer_flag == 1: buffer_size = (pdu[3] << 8) | pdu[2] else: buffer_size = 0 - if(self.get_report_cb != None): + if self.get_report_cb != None: ret = self.get_report_cb(report_id, report_type, buffer_size) - if(ret.status == self.GetSetReturn.FAILURE): + if ret.status == self.GetSetReturn.FAILURE: self.send_handshake_message(Message.Handshake.ERR_UNKNOWN) - elif(ret.status == self.GetSetReturn.SUCCESS): - data = bytearray() - data.append(report_id) - data.extend(ret.data) - if(len(data) None: diff --git a/examples/run_hid_device.py b/examples/run_hid_device.py index 3287cb1f..e88ea602 100644 --- a/examples/run_hid_device.py +++ b/examples/run_hid_device.py @@ -60,7 +60,7 @@ from bumble.utils import AsyncRunner SDP_HID_SERVICE_NAME_ATTRIBUTE_ID = 0x0100 SDP_HID_SERVICE_DESCRIPTION_ATTRIBUTE_ID = 0x0101 SDP_HID_PROVIDER_NAME_ATTRIBUTE_ID = 0x0102 -SDP_HID_DEVICE_RELEASE_NUMBER_ATTRIBUTE_ID = 0x0200 # [DEPRECATED] +SDP_HID_DEVICE_RELEASE_NUMBER_ATTRIBUTE_ID = 0x0200 # [DEPRECATED] SDP_HID_PARSER_VERSION_ATTRIBUTE_ID = 0x0201 SDP_HID_DEVICE_SUBCLASS_ATTRIBUTE_ID = 0x0202 SDP_HID_COUNTRY_CODE_ATTRIBUTE_ID = 0x0203 @@ -68,20 +68,20 @@ SDP_HID_VIRTUAL_CABLE_ATTRIBUTE_ID = 0x0204 SDP_HID_RECONNECT_INITIATE_ATTRIBUTE_ID = 0x0205 SDP_HID_DESCRIPTOR_LIST_ATTRIBUTE_ID = 0x0206 SDP_HID_LANGID_BASE_LIST_ATTRIBUTE_ID = 0x0207 -SDP_HID_SDP_DISABLE_ATTRIBUTE_ID = 0x0208 # [DEPRECATED] +SDP_HID_SDP_DISABLE_ATTRIBUTE_ID = 0x0208 # [DEPRECATED] SDP_HID_BATTERY_POWER_ATTRIBUTE_ID = 0x0209 SDP_HID_REMOTE_WAKE_ATTRIBUTE_ID = 0x020A -SDP_HID_PROFILE_VERSION_ATTRIBUTE_ID = 0x020B # DEPRECATED] +SDP_HID_PROFILE_VERSION_ATTRIBUTE_ID = 0x020B # DEPRECATED] SDP_HID_SUPERVISION_TIMEOUT_ATTRIBUTE_ID = 0x020C -SDP_HID_NORMALLY_CONNECTABLE_ATTRIBUTE_ID = 0x020D +SDP_HID_NORMALLY_CONNECTABLE_ATTRIBUTE_ID = 0x020D SDP_HID_BOOT_DEVICE_ATTRIBUTE_ID = 0x020E SDP_HID_SSR_HOST_MAX_LATENCY_ATTRIBUTE_ID = 0x020F SDP_HID_SSR_HOST_MIN_TIMEOUT_ATTRIBUTE_ID = 0x0210 # Refer to HID profile specification v1.1.1, "5.3 Service Discovery Protocol (SDP)" for details # HID SDP attribute values -LANGUAGE = 0x656e # 0x656E uint16 “en” (English) -ENCODING = 0x6a # 0x006A uint16 UTF-8 encoding +LANGUAGE = 0x656E # 0x656E uint16 “en” (English) +ENCODING = 0x6A # 0x006A uint16 UTF-8 encoding PRIMARY_LANGUAGE_BASE_ID = 0x100 # 0x0100 uint16 PrimaryLanguageBaseID VERSION_NUMBER = 0x0101 # 0x0101 uint16 version number (v1.1) SERVICE_NAME = b'Bumble HID' @@ -222,7 +222,7 @@ HID_REPORT_MAP = bytes( # Text String, 50 Octet Report Descriptor 0x06, # . Input (Data,Var,Rel,No Wrap,Linear,Preferred State,No Null Position) 0xC0, # . End Collection 0xC0, # End Collection - ] + ] ) @@ -298,7 +298,9 @@ def sdp_records(): DataElement.sequence( [ DataElement.uuid(BT_L2CAP_PROTOCOL_ID), - DataElement.unsigned_integer_16(HID_INTERRUPT_PSM), + DataElement.unsigned_integer_16( + HID_INTERRUPT_PSM + ), ] ), DataElement.sequence( @@ -362,8 +364,12 @@ def sdp_records(): [ DataElement.sequence( [ - DataElement.unsigned_integer_16(HID_LANGID_BASE_LANGUAGE), - DataElement.unsigned_integer_16(HID_LANGID_BASE_BLUETOOTH_STRING_OFFSET), + DataElement.unsigned_integer_16( + HID_LANGID_BASE_LANGUAGE + ), + DataElement.unsigned_integer_16( + HID_LANGID_BASE_BLUETOOTH_STRING_OFFSET + ), ] ), ] @@ -415,8 +421,8 @@ class DeviceData: self.keyboardData = bytearray([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) self.mouseData = bytearray([0x00, 0x00, 0x00, 0x00]) -#Device's live data - Mouse and Keyboard will be stored in this -deviceData=DeviceData() +# Device's live data - Mouse and Keyboard will be stored in this +deviceData = DeviceData() # ----------------------------------------------------------------------------- async def keyboard_device(hid_device, command): @@ -425,6 +431,7 @@ async def keyboard_device(hid_device, command): # Start a Websocket server to receive events from a web page async def serve(websocket, _path): global deviceData + # global mouseData while True: try: message = await websocket.recv() @@ -438,24 +445,25 @@ async def keyboard_device(hid_device, command): code = ord(key) if ord('a') <= code <= ord('z'): hid_code = 0x04 + code - ord('a') - deviceData.keyboardData = bytearray([0x01, 0x00, 0x00, hid_code, 0x00, 0x00, 0x00, 0x00, 0x00]) - hid_device.send_report_on_interrupt(deviceData.keyboardData) + deviceData.setKeyBoardData(bytearray([0x01, 0x00, 0x00, hid_code, 0x00, 0x00, 0x00, 0x00, 0x00])) + hid_device.send_report_on_interrupt(deviceData.getKeyBoardData()) elif message_type == 'keyup': - deviceData.keyboardData = bytearray([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) - hid_device.send_report_on_interrupt(deviceData.keyboardData) + deviceData.setKeyBoardData(bytearray([0x01, 0x00, 0x00, hid_code, 0x00, 0x00, 0x00, 0x00, 0x00])) + hid_device.send_report_on_interrupt(deviceData.getKeyBoardData()) elif message_type == "mousemove": # logical min and max values log_min = -127 log_max = 127 x = parsed['x'] y = parsed['y'] - # limiting x and y values within logical max and min range - x = max(log_min, min(log_max, x)) - y = max(log_min, min(log_max, y)) + if y > 127: + y = 127 + elif y < -127: + y = -127 x_cord = x.to_bytes(signed = True) y_cord = y.to_bytes(signed = True) - deviceData.mouseData = bytearray([0x02, 0x00]) + x_cord + y_cord - hid_device.send_report_on_interrupt(deviceData.mouseData) + deviceData.setMouseData(bytearray([0x02, 0x00]) + x_cord + y_cord) + hid_device.send_report_on_interrupt(deviceData.getMouseData()) except websockets.exceptions.ConnectionClosedOK: pass @@ -464,7 +472,6 @@ async def keyboard_device(hid_device, command): await asyncio.get_event_loop().create_future() - # ----------------------------------------------------------------------------- async def main(): if len(sys.argv) < 3: @@ -484,7 +491,7 @@ async def main(): hid_host_bd_addr = str(hid_device.remote_device_bd_address) await hid_device.disconnect_interrupt_channel() await hid_device.disconnect_control_channel() - await device.keystore.delete(hid_host_bd_addr) #type: ignore + await device.keystore.delete(hid_host_bd_addr) # type: ignore connection = hid_device.connection if connection is not None: await connection.disconnect() @@ -492,9 +499,9 @@ async def main(): def on_hid_data_cb(pdu): print(f'Received Data, PDU: {pdu.hex()}') - def on_get_report_cb(report_id,report_type, buffer_size): + def on_get_report_cb(report_id, report_type, buffer_size): retValue = hid_device.GetSetStatus() - print("GET_REPORT report_id: " + str(report_id) +"report_type: "+ str(report_type)+ + print("GET_REPORT report_id: " + str(report_id) +"report_type: "+ str(report_type)+ "buffer_size:" + str(buffer_size)) if report_type == Message.ReportType.INPUT_REPORT: if report_id == 1: @@ -506,17 +513,17 @@ async def main(): else: retValue.status = hid_device.GetSetReturn.REPORT_ID_NOT_FOUND - if(buffer_size): - data_len = buffer_size -1 + if buffer_size: + data_len = buffer_size - 1 retValue.data = retValue.data[:data_len] elif report_type == Message.ReportType.OUTPUT_REPORT: - #This sample app has nothing to do with the report received, to enable PTS + #This sample app has nothing to do with the report received, to enable PTS #testing, we will return single byte random data. retValue.data = bytearray([0x11]) retValue.status = hid_device.GetSetReturn.SUCCESS elif report_type == Message.ReportType.FEATURE_REPORT: - #TBD - not requried for PTS testing + # TBD - not requried for PTS testing retValue.status = hid_device.GetSetReturn.ERR_UNSUPPORTED_REQUEST else: @@ -526,23 +533,22 @@ async def main(): def on_set_report_cb(report_id, report_type, data): retValue = hid_device.GetSetStatus() - print("SET_REPORT report_id: " + str(report_id) +"report_type: "+ str(report_type)+ + print("SET_REPORT report_id: " + str(report_id) +"report_type: "+ str(report_type)+ "data:" + str(data)) retValue.status = hid_device.GetSetReturn.SUCCESS return retValue - def on_get_protocol_cb(): retValue = hid_device.GetSetStatus() - retValue.data=protocol_mode.to_bytes() - retValue.status=hid_device.GetSetReturn.SUCCESS + retValue.data = protocol_mode.to_bytes() + retValue.status = hid_device.GetSetReturn.SUCCESS return retValue def on_set_protocol_cb(protocol): retValue = hid_device.GetSetStatus() - #We do not support SET_PROTOCOL. + # We do not support SET_PROTOCOL. print("SET_PROTOCOL report_id: " + str(protocol)) - retValue.status=hid_device.GetSetReturn.ERR_UNSUPPORTED_REQUEST + retValue.status = hid_device.GetSetReturn.ERR_UNSUPPORTED_REQUEST return retValue def on_virtual_cable_unplug_cb(): @@ -584,7 +590,9 @@ async def main(): async def menu(): reader = await get_stream_reader(sys.stdin) while True: - print("\n************************ HID Device Menu *****************************\n") + print( + "\n************************ HID Device Menu *****************************\n" + ) print(" 1. Connect Control Channel") print(" 2. Connect Interrupt Channel") print(" 3. Disconnect Control Channel") @@ -621,22 +629,26 @@ async def main(): choice1 = choice1.decode('utf-8').strip() if choice1 == '1': - data = bytearray([0x01, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00]) - hid_device.send_report_on_interrupt(data) - data = bytearray([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) - hid_device.send_report_on_interrupt(data) + data = bytearray( + [0x01, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00] + ) + hid_device.send_data(data) + data = bytearray( + [0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] + ) + hid_device.send_data(data) elif choice1 == '2': - data = bytearray([0x02, 0x00, 0x00, 0xf6]) - hid_device.send_report_on_interrupt(data) - data = bytearray([0x02, 0x00, 0x00, 0x00]) - hid_device.send_report_on_interrupt(data) + data = bytearray([0x02, 0x00, 0x00, 0xF6]) + hid_device.send_data(data) + data = bytearray([0x02, 0x00, 0x00, 0x00]) + hid_device.send_data(data) elif choice1 == '3': - data = bytearray([0x00, 0x00, 0x00, 0x00]) - hid_device.send_report_on_interrupt(data) - data = bytearray([0x00, 0x00, 0x00, 0x00]) - hid_device.send_report_on_interrupt(data) + data = bytearray([0x00, 0x00, 0x00, 0x00]) + hid_device.send_data(data) + data = bytearray([0x00, 0x00, 0x00, 0x00]) + hid_device.send_data(data) else: print('Incorrect option selected') @@ -665,7 +677,9 @@ async def main(): elif choice == '9': hid_host_bd_addr = str(hid_device.remote_device_bd_address) - connection = await device.connect(hid_host_bd_addr, transport=BT_BR_EDR_TRANSPORT) + connection = await device.connect( + hid_host_bd_addr, transport=BT_BR_EDR_TRANSPORT + ) await connection.authenticate() await connection.encrypt() @@ -691,7 +705,7 @@ async def main(): await keyboard_device(hid_device, 'web') else: - #default option is using keyboard.html (web) + # default option is using keyboard.html (web) await keyboard_device(hid_device, 'web') await hci_source.wait_for_termination() diff --git a/examples/run_hid_host.py b/examples/run_hid_host.py index fe9e8116..7519b4e1 100644 --- a/examples/run_hid_host.py +++ b/examples/run_hid_host.py @@ -308,8 +308,8 @@ async def main(): if (report_length <= 1) or (report_id == 0): return - #Parse report over interrupt channel - if (report_type == Message.ReportType.INPUT_REPORT): + # Parse report over interrupt channel + if report_type == Message.ReportType.INPUT_REPORT: ReportParser.parse_input_report(pdu[1:]) # type: ignore async def handle_virtual_cable_unplug(): @@ -362,7 +362,6 @@ async def main(): await get_hid_device_sdp_record(connection) - async def menu(): reader = await get_stream_reader(sys.stdin) while True: @@ -493,11 +492,11 @@ async def main(): data = bytearray( [0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] ) - hid_host.send_report_on_interrupt(data) + hid_host.send_data(data) elif choice1 == '2': data = bytearray([0x03, 0x00, 0x0D, 0xFD, 0x00, 0x00]) - hid_host.send_report_on_interrupt(data) + hid_host.send_data(data) else: print('Incorrect option selected') From f47b9178ad62519d1a6df9c9124ae4d8b6c0c105 Mon Sep 17 00:00:00 2001 From: Fahad Afroze Date: Mon, 27 Nov 2023 11:55:35 +0000 Subject: [PATCH 14/19] Added GET_REPORT and SET_REPORT changes Added changes to handle invalid cases --- bumble/hid.py | 16 ++++++++--- examples/run_hid_device.py | 55 +++++++++++++++++++++++--------------- 2 files changed, 45 insertions(+), 26 deletions(-) diff --git a/bumble/hid.py b/bumble/hid.py index 059621c6..c9205102 100644 --- a/bumble/hid.py +++ b/bumble/hid.py @@ -380,6 +380,7 @@ class Device(HID): REPORT_ID_NOT_FOUND = 0x01 ERR_UNSUPPORTED_REQUEST = 0x02 ERR_UNKNOWN = 0x03 + ERR_INVALID_PARAMETER = 0x04 SUCCESS = 0xFF class GetSetStatus: @@ -433,6 +434,8 @@ class Device(HID): elif ret.status == self.GetSetReturn.REPORT_ID_NOT_FOUND: self.send_handshake_message(Message.Handshake.ERR_INVALID_REPORT_ID) + elif ret.status == self.GetSetReturn.ERR_INVALID_PARAMETER: + self.send_handshake_message(Message.Handshake.ERR_INVALID_PARAMETER) elif ret.status == self.GetSetReturn.ERR_UNSUPPORTED_REQUEST: self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST) else: @@ -448,14 +451,19 @@ class Device(HID): report_type = pdu[0] & 0x03 report_id = pdu[1] report_data = pdu[2:] - ret = self.set_report_cb(report_id, report_type, report_data) + report_size = len(pdu[1:]) + ret = self.set_report_cb(report_id, report_type, report_size, report_data) if ret.status == self.GetSetReturn.SUCCESS: self.send_handshake_message(Message.Handshake.SUCCESSFUL) - return + elif ret.status == self.GetSetReturn.ERR_INVALID_PARAMETER: + self.send_handshake_message(Message.Handshake.ERR_INVALID_PARAMETER) + elif ret.status == self.GetSetReturn.REPORT_ID_NOT_FOUND: + self.send_handshake_message(Message.Handshake.ERR_INVALID_REPORT_ID) + else: + self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST) else: logger.debug("SetReport callback not registered !!") - - self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST) + self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST) def register_set_report_cb(self, cb): self.set_report_cb = cb diff --git a/examples/run_hid_device.py b/examples/run_hid_device.py index e88ea602..3016bdf0 100644 --- a/examples/run_hid_device.py +++ b/examples/run_hid_device.py @@ -418,8 +418,8 @@ async def get_stream_reader(pipe) -> asyncio.StreamReader: class DeviceData: def __init__(self) -> None: - self.keyboardData = bytearray([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) - self.mouseData = bytearray([0x00, 0x00, 0x00, 0x00]) + self.keyboardData = bytearray([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + self.mouseData = bytearray([0x02, 0x00, 0x00, 0x00]) # Device's live data - Mouse and Keyboard will be stored in this deviceData = DeviceData() @@ -431,7 +431,6 @@ async def keyboard_device(hid_device, command): # Start a Websocket server to receive events from a web page async def serve(websocket, _path): global deviceData - # global mouseData while True: try: message = await websocket.recv() @@ -445,25 +444,24 @@ async def keyboard_device(hid_device, command): code = ord(key) if ord('a') <= code <= ord('z'): hid_code = 0x04 + code - ord('a') - deviceData.setKeyBoardData(bytearray([0x01, 0x00, 0x00, hid_code, 0x00, 0x00, 0x00, 0x00, 0x00])) - hid_device.send_report_on_interrupt(deviceData.getKeyBoardData()) + deviceData.keyboardData = bytearray([0x01, 0x00, 0x00, hid_code, 0x00, 0x00, 0x00, 0x00, 0x00]) + hid_device.send_data(deviceData.keyboardData) elif message_type == 'keyup': - deviceData.setKeyBoardData(bytearray([0x01, 0x00, 0x00, hid_code, 0x00, 0x00, 0x00, 0x00, 0x00])) - hid_device.send_report_on_interrupt(deviceData.getKeyBoardData()) + deviceData.keyboardData = bytearray([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + hid_device.send_data(deviceData.keyboardData) elif message_type == "mousemove": # logical min and max values log_min = -127 log_max = 127 x = parsed['x'] y = parsed['y'] - if y > 127: - y = 127 - elif y < -127: - y = -127 + # limiting x and y values within logical max and min range + x = max(log_min, min(log_max, x)) + y = max(log_min, min(log_max, y)) x_cord = x.to_bytes(signed = True) y_cord = y.to_bytes(signed = True) - deviceData.setMouseData(bytearray([0x02, 0x00]) + x_cord + y_cord) - hid_device.send_report_on_interrupt(deviceData.getMouseData()) + deviceData.mouseData = bytearray([0x02, 0x00]) + x_cord + y_cord + hid_device.send_data(deviceData.mouseData) except websockets.exceptions.ConnectionClosedOK: pass @@ -505,10 +503,10 @@ async def main(): "buffer_size:" + str(buffer_size)) if report_type == Message.ReportType.INPUT_REPORT: if report_id == 1: - retValue.data = deviceData.keyboardData + retValue.data = deviceData.keyboardData[1:] retValue.status = hid_device.GetSetReturn.SUCCESS elif report_id == 2: - retValue.data = deviceData.mouseData + retValue.data = deviceData.mouseData[1:] retValue.status = hid_device.GetSetReturn.SUCCESS else: retValue.status = hid_device.GetSetReturn.REPORT_ID_NOT_FOUND @@ -521,21 +519,34 @@ async def main(): #testing, we will return single byte random data. retValue.data = bytearray([0x11]) retValue.status = hid_device.GetSetReturn.SUCCESS - elif report_type == Message.ReportType.FEATURE_REPORT: - # TBD - not requried for PTS testing - retValue.status = hid_device.GetSetReturn.ERR_UNSUPPORTED_REQUEST - + retValue.status = hid_device.GetSetReturn.ERR_INVALID_PARAMETER + elif report_type == Message.ReportType.OTHER_REPORT: + if report_id == 3: + retValue.status = hid_device.GetSetReturn.REPORT_ID_NOT_FOUND else: retValue.status = hid_device.GetSetReturn.FAILURE return retValue - def on_set_report_cb(report_id, report_type, data): + def on_set_report_cb(report_id, report_type, report_size, data): retValue = hid_device.GetSetStatus() print("SET_REPORT report_id: " + str(report_id) +"report_type: "+ str(report_type)+ - "data:" + str(data)) - retValue.status = hid_device.GetSetReturn.SUCCESS + "report_size " + str(report_size) + "data:" + str(data)) + if report_type == Message.ReportType.FEATURE_REPORT: + retValue.status = hid_device.GetSetReturn.ERR_INVALID_PARAMETER + elif report_type == Message.ReportType.INPUT_REPORT: + if report_id == 1 and report_size != len(deviceData.keyboardData): + retValue.status = hid_device.GetSetReturn.ERR_INVALID_PARAMETER + elif report_id == 2 and report_size != len(deviceData.mouseData): + retValue.status = hid_device.GetSetReturn.ERR_INVALID_PARAMETER + elif report_id == 3: + retValue.status = hid_device.GetSetReturn.REPORT_ID_NOT_FOUND + else: + retValue.status = hid_device.GetSetReturn.SUCCESS + else: + retValue.status = hid_device.GetSetReturn.SUCCESS + return retValue def on_get_protocol_cb(): From 07f71fc895d1b082cfd123d144940cf09c9d296e Mon Sep 17 00:00:00 2001 From: skarnataki Date: Mon, 27 Nov 2023 13:04:54 +0000 Subject: [PATCH 15/19] Project format and lint error fix. Redefination if Device class needs to be discussed --- bumble/hid.py | 38 +++++++++++++++---------- examples/run_hid_device.py | 57 +++++++++++++++++++++++++++++--------- 2 files changed, 68 insertions(+), 27 deletions(-) diff --git a/bumble/hid.py b/bumble/hid.py index c9205102..0dd87cd1 100644 --- a/bumble/hid.py +++ b/bumble/hid.py @@ -274,8 +274,10 @@ class HID(EventEmitter): await channel.disconnect() def on_device_connection(self, connection: Connection) -> None: - self.connection = connection - self.remote_device_bd_address = connection.peer_address + self.connection = connection # type: ignore[assignment] + self.remote_device_bd_address = ( + connection.peer_address + ) # type: ignore[assignment] connection.on('disconnection', self.on_disconnection) def on_connection(self, l2cap_channel: l2cap.ClassicChannel) -> None: @@ -312,16 +314,16 @@ class HID(EventEmitter): self.emit('handshake', Message.Handshake(param)) elif message_type == Message.MessageType.GET_REPORT: logger.debug('<<< HID GET REPORT') - self.handle_get_report(pdu) + self.handle_get_report(pdu) # type: ignore[attr-defined] elif message_type == Message.MessageType.SET_REPORT: logger.debug('<<< HID SET REPORT') - self.handle_set_report(pdu) + self.handle_set_report(pdu) # type: ignore[attr-defined] elif message_type == Message.MessageType.GET_PROTOCOL: logger.debug('<<< HID GET PROTOCOL') - self.handle_get_protocol(pdu) + self.handle_get_protocol(pdu) # type: ignore[attr-defined] elif message_type == Message.MessageType.SET_PROTOCOL: logger.debug('<<< HID SET PROTOCOL') - self.handle_set_protocol(pdu) + self.handle_set_protocol(pdu) # type: ignore[attr-defined] elif message_type == Message.MessageType.DATA: logger.debug('<<< HID CONTROL DATA') self.emit('control_data', pdu) @@ -339,7 +341,9 @@ class HID(EventEmitter): logger.debug('<<< HID CONTROL OPERATION UNSUPPORTED') else: logger.debug('<<< HID MESSAGE TYPE UNSUPPORTED') - self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST) + self.send_handshake_message( + Message.Handshake.ERR_UNSUPPORTED_REQUEST + ) # type: ignore[attr-defined] def on_intr_pdu(self, pdu: bytes) -> None: logger.debug(f'<<< HID INTERRUPT PDU: {pdu.hex()}') @@ -374,7 +378,7 @@ class HID(EventEmitter): # ----------------------------------------------------------------------------- -class Device(HID): +class Device(HID): # type: ignore[no-redef] class GetSetReturn(enum.IntEnum): FAILURE = 0x00 REPORT_ID_NOT_FOUND = 0x01 @@ -385,8 +389,8 @@ class Device(HID): class GetSetStatus: def __init__(self) -> None: + self.data: bytes self.status = 0 - self.data = None def __init__(self, device: Device) -> None: super().__init__(device, HID.Role.DEVICE) @@ -419,7 +423,9 @@ class Device(HID): buffer_size = 0 if self.get_report_cb != None: - ret = self.get_report_cb(report_id, report_type, buffer_size) + ret = self.get_report_cb( + report_id, report_type, buffer_size + ) # type: ignore if ret.status == self.GetSetReturn.FAILURE: self.send_handshake_message(Message.Handshake.ERR_UNKNOWN) @@ -427,7 +433,9 @@ class Device(HID): data = bytearray() data.append(report_id) data.extend(ret.data) - if len(data) < self.l2cap_ctrl_channel.mtu: + if ( + len(data) < self.l2cap_ctrl_channel.mtu + ): # type: ignore[union-attr] self.send_control_data(report_type=report_type, data=data) else: self.send_handshake_message(Message.Handshake.ERR_INVALID_PARAMETER) @@ -452,7 +460,9 @@ class Device(HID): report_id = pdu[1] report_data = pdu[2:] report_size = len(pdu[1:]) - ret = self.set_report_cb(report_id, report_type, report_size, report_data) + ret = self.set_report_cb( + report_id, report_type, report_size, report_data + ) # type: ignore if ret.status == self.GetSetReturn.SUCCESS: self.send_handshake_message(Message.Handshake.SUCCESSFUL) elif ret.status == self.GetSetReturn.ERR_INVALID_PARAMETER: @@ -472,7 +482,7 @@ class Device(HID): def handle_get_protocol(self, pdu: bytes): ret = self.GetSetStatus() if self.get_protocol_cb != None: - ret = self.get_protocol_cb() + ret = self.get_protocol_cb() # type: ignore if ret.status == self.GetSetReturn.SUCCESS: self.send_control_data(Message.ReportType.OTHER_REPORT, ret.data) return @@ -488,7 +498,7 @@ class Device(HID): def handle_set_protocol(self, pdu: bytes): ret = self.GetSetStatus() if self.set_protocol_cb != None: - ret = self.set_protocol_cb(pdu[0] & 0x01) + ret = self.set_protocol_cb(pdu[0] & 0x01) # type: ignore if ret.status == self.GetSetReturn.SUCCESS: return else: diff --git a/examples/run_hid_device.py b/examples/run_hid_device.py index 3016bdf0..e0ac281d 100644 --- a/examples/run_hid_device.py +++ b/examples/run_hid_device.py @@ -418,9 +418,12 @@ async def get_stream_reader(pipe) -> asyncio.StreamReader: class DeviceData: def __init__(self) -> None: - self.keyboardData = bytearray([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + self.keyboardData = bytearray( + [0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] + ) self.mouseData = bytearray([0x02, 0x00, 0x00, 0x00]) + # Device's live data - Mouse and Keyboard will be stored in this deviceData = DeviceData() @@ -444,10 +447,24 @@ async def keyboard_device(hid_device, command): code = ord(key) if ord('a') <= code <= ord('z'): hid_code = 0x04 + code - ord('a') - deviceData.keyboardData = bytearray([0x01, 0x00, 0x00, hid_code, 0x00, 0x00, 0x00, 0x00, 0x00]) + deviceData.keyboardData = bytearray( + [ + 0x01, + 0x00, + 0x00, + hid_code, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + ] + ) hid_device.send_data(deviceData.keyboardData) elif message_type == 'keyup': - deviceData.keyboardData = bytearray([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + deviceData.keyboardData = bytearray( + [0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] + ) hid_device.send_data(deviceData.keyboardData) elif message_type == "mousemove": # logical min and max values @@ -458,8 +475,8 @@ async def keyboard_device(hid_device, command): # limiting x and y values within logical max and min range x = max(log_min, min(log_max, x)) y = max(log_min, min(log_max, y)) - x_cord = x.to_bytes(signed = True) - y_cord = y.to_bytes(signed = True) + x_cord = x.to_bytes(signed=True) + y_cord = y.to_bytes(signed=True) deviceData.mouseData = bytearray([0x02, 0x00]) + x_cord + y_cord hid_device.send_data(deviceData.mouseData) except websockets.exceptions.ConnectionClosedOK: @@ -499,8 +516,14 @@ async def main(): def on_get_report_cb(report_id, report_type, buffer_size): retValue = hid_device.GetSetStatus() - print("GET_REPORT report_id: " + str(report_id) +"report_type: "+ str(report_type)+ - "buffer_size:" + str(buffer_size)) + print( + "GET_REPORT report_id: " + + str(report_id) + + "report_type: " + + str(report_type) + + "buffer_size:" + + str(buffer_size) + ) if report_type == Message.ReportType.INPUT_REPORT: if report_id == 1: retValue.data = deviceData.keyboardData[1:] @@ -515,8 +538,8 @@ async def main(): data_len = buffer_size - 1 retValue.data = retValue.data[:data_len] elif report_type == Message.ReportType.OUTPUT_REPORT: - #This sample app has nothing to do with the report received, to enable PTS - #testing, we will return single byte random data. + # This sample app has nothing to do with the report received, to enable PTS + # testing, we will return single byte random data. retValue.data = bytearray([0x11]) retValue.status = hid_device.GetSetReturn.SUCCESS elif report_type == Message.ReportType.FEATURE_REPORT: @@ -531,15 +554,23 @@ async def main(): def on_set_report_cb(report_id, report_type, report_size, data): retValue = hid_device.GetSetStatus() - print("SET_REPORT report_id: " + str(report_id) +"report_type: "+ str(report_type)+ - "report_size " + str(report_size) + "data:" + str(data)) + print( + "SET_REPORT report_id: " + + str(report_id) + + "report_type: " + + str(report_type) + + "report_size " + + str(report_size) + + "data:" + + str(data) + ) if report_type == Message.ReportType.FEATURE_REPORT: retValue.status = hid_device.GetSetReturn.ERR_INVALID_PARAMETER elif report_type == Message.ReportType.INPUT_REPORT: if report_id == 1 and report_size != len(deviceData.keyboardData): - retValue.status = hid_device.GetSetReturn.ERR_INVALID_PARAMETER + retValue.status = hid_device.GetSetReturn.ERR_INVALID_PARAMETER elif report_id == 2 and report_size != len(deviceData.mouseData): - retValue.status = hid_device.GetSetReturn.ERR_INVALID_PARAMETER + retValue.status = hid_device.GetSetReturn.ERR_INVALID_PARAMETER elif report_id == 3: retValue.status = hid_device.GetSetReturn.REPORT_ID_NOT_FOUND else: From 403a13e4c6a4c2fb13eb37b95c986a9c947924f9 Mon Sep 17 00:00:00 2001 From: skarnataki Date: Tue, 28 Nov 2023 13:42:25 +0000 Subject: [PATCH 16/19] Review comment fix HID device --- bumble/hid.py | 160 ++++++++++++++++++++++---------------------------- 1 file changed, 69 insertions(+), 91 deletions(-) diff --git a/bumble/hid.py b/bumble/hid.py index 0dd87cd1..daf0d700 100644 --- a/bumble/hid.py +++ b/bumble/hid.py @@ -19,17 +19,15 @@ from __future__ import annotations from dataclasses import dataclass import logging import enum +import struct from pyee import EventEmitter from typing import Optional, TYPE_CHECKING -from bumble import l2cap +from bumble import l2cap, device from bumble.colors import color from bumble.core import InvalidStateError, ProtocolError -if TYPE_CHECKING: - from bumble.device import Device, Connection - # ----------------------------------------------------------------------------- # Logging @@ -105,10 +103,11 @@ class GetReportMessage(Message): if self.buffer_size == 0: return self.header(self.report_type) + packet_bytes else: - packet_bytes.extend( - [(self.buffer_size & 0xFF), ((self.buffer_size >> 8) & 0xFF)] + return ( + self.header(0x08 | self.report_type) + + packet_bytes + + struct.pack(" bytes: - packet_bytes = bytearray() - - packet_bytes.extend(self.data) - return self.header(self.report_type) + packet_bytes + return self.header(self.report_type) + self.data @dataclass @@ -151,17 +147,6 @@ class SetProtocolMessage(Message): return self.header(self.protocol_mode) -@dataclass -class GetProtocolReplyMessage(Message): - protocol_mode: int - message_type = Message.MessageType.DATA - - def __bytes__(self) -> bytes: - packet_bytes = bytearray() - packet_bytes.append(self.protocol_mode) - return self.header(Message.ReportType.OTHER_REPORT) + packet_bytes - - @dataclass class Suspend(Message): message_type = Message.MessageType.CONTROL @@ -215,7 +200,7 @@ class HID(EventEmitter): HOST = 0x00 DEVICE = 0x01 - def __init__(self, device: Device, role: int) -> None: + def __init__(self, device: device.Device, role: Role) -> None: super().__init__() self.device = device self.connection = None @@ -273,11 +258,11 @@ class HID(EventEmitter): self.l2cap_ctrl_channel = None await channel.disconnect() - def on_device_connection(self, connection: Connection) -> None: + def on_device_connection(self, connection: device.Connection) -> None: self.connection = connection # type: ignore[assignment] self.remote_device_bd_address = ( - connection.peer_address - ) # type: ignore[assignment] + connection.peer_address # type: ignore[assignment] + ) connection.on('disconnection', self.on_disconnection) def on_connection(self, l2cap_channel: l2cap.ClassicChannel) -> None: @@ -341,9 +326,9 @@ class HID(EventEmitter): logger.debug('<<< HID CONTROL OPERATION UNSUPPORTED') else: logger.debug('<<< HID MESSAGE TYPE UNSUPPORTED') - self.send_handshake_message( + self.send_handshake_message( # type: ignore[attr-defined] Message.Handshake.ERR_UNSUPPORTED_REQUEST - ) # type: ignore[attr-defined] + ) def on_intr_pdu(self, pdu: bytes) -> None: logger.debug(f'<<< HID INTERRUPT PDU: {pdu.hex()}') @@ -378,7 +363,7 @@ class HID(EventEmitter): # ----------------------------------------------------------------------------- -class Device(HID): # type: ignore[no-redef] +class Device(HID): class GetSetReturn(enum.IntEnum): FAILURE = 0x00 REPORT_ID_NOT_FOUND = 0x01 @@ -389,10 +374,10 @@ class Device(HID): # type: ignore[no-redef] class GetSetStatus: def __init__(self) -> None: - self.data: bytes + self.data = bytearray() self.status = 0 - def __init__(self, device: Device) -> None: + def __init__(self, device: device.Device) -> None: super().__init__(device, HID.Role.DEVICE) self.get_report_cb = None self.set_report_cb = None @@ -412,7 +397,10 @@ class Device(HID): # type: ignore[no-redef] self.send_pdu_on_ctrl(hid_message) def handle_get_report(self, pdu: bytes): - ret = self.GetSetStatus() + if self.get_report_cb is None: + logger.debug("GetReport callback not registered !!") + self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST) + return report_type = pdu[0] & 0x03 buffer_flag = (pdu[0] & 0x08) >> 3 report_id = pdu[1] @@ -422,32 +410,23 @@ class Device(HID): # type: ignore[no-redef] else: buffer_size = 0 - if self.get_report_cb != None: - ret = self.get_report_cb( - report_id, report_type, buffer_size - ) # type: ignore + ret = self.get_report_cb(report_id, report_type, buffer_size) - if ret.status == self.GetSetReturn.FAILURE: - self.send_handshake_message(Message.Handshake.ERR_UNKNOWN) - elif ret.status == self.GetSetReturn.SUCCESS: - data = bytearray() - data.append(report_id) - data.extend(ret.data) - if ( - len(data) < self.l2cap_ctrl_channel.mtu - ): # type: ignore[union-attr] - self.send_control_data(report_type=report_type, data=data) - else: - self.send_handshake_message(Message.Handshake.ERR_INVALID_PARAMETER) - - elif ret.status == self.GetSetReturn.REPORT_ID_NOT_FOUND: - self.send_handshake_message(Message.Handshake.ERR_INVALID_REPORT_ID) - elif ret.status == self.GetSetReturn.ERR_INVALID_PARAMETER: + if ret.status == self.GetSetReturn.FAILURE: + self.send_handshake_message(Message.Handshake.ERR_UNKNOWN) + elif ret.status == self.GetSetReturn.SUCCESS: + data = bytearray() + data.append(report_id) + data.extend(ret.data) + if len(data) < self.l2cap_ctrl_channel.mtu: + self.send_control_data(report_type=report_type, data=data) + else: self.send_handshake_message(Message.Handshake.ERR_INVALID_PARAMETER) - elif ret.status == self.GetSetReturn.ERR_UNSUPPORTED_REQUEST: - self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST) - else: - logger.debug("GetReport callback not registered !!") + elif ret.status == self.GetSetReturn.REPORT_ID_NOT_FOUND: + self.send_handshake_message(Message.Handshake.ERR_INVALID_REPORT_ID) + elif ret.status == self.GetSetReturn.ERR_INVALID_PARAMETER: + self.send_handshake_message(Message.Handshake.ERR_INVALID_PARAMETER) + elif ret.status == self.GetSetReturn.ERR_UNSUPPORTED_REQUEST: self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST) def register_get_report_cb(self, cb): @@ -455,56 +434,55 @@ class Device(HID): # type: ignore[no-redef] logger.debug("GetReport callback registered successfully") def handle_set_report(self, pdu: bytes): - if self.set_report_cb != None: - report_type = pdu[0] & 0x03 - report_id = pdu[1] - report_data = pdu[2:] - report_size = len(pdu[1:]) - ret = self.set_report_cb( - report_id, report_type, report_size, report_data - ) # type: ignore - if ret.status == self.GetSetReturn.SUCCESS: - self.send_handshake_message(Message.Handshake.SUCCESSFUL) - elif ret.status == self.GetSetReturn.ERR_INVALID_PARAMETER: - self.send_handshake_message(Message.Handshake.ERR_INVALID_PARAMETER) - elif ret.status == self.GetSetReturn.REPORT_ID_NOT_FOUND: - self.send_handshake_message(Message.Handshake.ERR_INVALID_REPORT_ID) - else: - self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST) - else: + if self.set_report_cb is None: logger.debug("SetReport callback not registered !!") self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST) + return + report_type = pdu[0] & 0x03 + report_id = pdu[1] + report_data = pdu[2:] + report_size = len(pdu[1:]) + ret = self.set_report_cb( + report_id, report_type, report_size, report_data + ) # type: ignore + if ret.status == self.GetSetReturn.SUCCESS: + self.send_handshake_message(Message.Handshake.SUCCESSFUL) + elif ret.status == self.GetSetReturn.ERR_INVALID_PARAMETER: + self.send_handshake_message(Message.Handshake.ERR_INVALID_PARAMETER) + elif ret.status == self.GetSetReturn.REPORT_ID_NOT_FOUND: + self.send_handshake_message(Message.Handshake.ERR_INVALID_REPORT_ID) + else: + self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST) def register_set_report_cb(self, cb): self.set_report_cb = cb logger.debug("SetReport callback registered successfully") def handle_get_protocol(self, pdu: bytes): - ret = self.GetSetStatus() - if self.get_protocol_cb != None: - ret = self.get_protocol_cb() # type: ignore - if ret.status == self.GetSetReturn.SUCCESS: - self.send_control_data(Message.ReportType.OTHER_REPORT, ret.data) - return - else: + if self.get_protocol_cb is None: logger.debug("GetProtocol callback not registered !!") - - self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST) + self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST) + return + ret = self.get_protocol_cb() + if ret.status == self.GetSetReturn.SUCCESS: + self.send_control_data(Message.ReportType.OTHER_REPORT, ret.data) + else: + self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST) def register_get_protocol_cb(self, cb): self.get_protocol_cb = cb logger.debug("GetProtocol callback registered successfully") def handle_set_protocol(self, pdu: bytes): - ret = self.GetSetStatus() - if self.set_protocol_cb != None: - ret = self.set_protocol_cb(pdu[0] & 0x01) # type: ignore - if ret.status == self.GetSetReturn.SUCCESS: - return - else: + if self.set_protocol_cb is None: logger.debug("SetProtocol callback not registered !!") - - self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST) + self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST) + return + ret = self.set_protocol_cb(pdu[0] & 0x01) + if ret.status == self.GetSetReturn.SUCCESS: + self.send_handshake_message(Message.Handshake.SUCCESSFUL) + else: + self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST) def register_set_protocol_cb(self, cb): self.set_protocol_cb = cb @@ -513,7 +491,7 @@ class Device(HID): # type: ignore[no-redef] # ----------------------------------------------------------------------------- class Host(HID): - def __init__(self, device: Device) -> None: + def __init__(self, device: device.Device) -> None: super().__init__(device, HID.Role.HOST) def get_report(self, report_type: int, report_id: int, buffer_size: int) -> None: From 5e3ecb74e4f1da5aa2b500e7f5256d7afed5b8ba Mon Sep 17 00:00:00 2001 From: skarnataki Date: Tue, 5 Dec 2023 13:41:26 +0000 Subject: [PATCH 17/19] Review comment fix -2 --- bumble/hid.py | 74 ++++++++++---------- examples/run_hid_device.py | 139 +++++++++++++++++-------------------- 2 files changed, 104 insertions(+), 109 deletions(-) diff --git a/bumble/hid.py b/bumble/hid.py index daf0d700..347c2187 100644 --- a/bumble/hid.py +++ b/bumble/hid.py @@ -22,7 +22,7 @@ import enum import struct from pyee import EventEmitter -from typing import Optional, TYPE_CHECKING +from typing import Optional, Callable, TYPE_CHECKING from bumble import l2cap, device from bumble.colors import color @@ -193,8 +193,9 @@ class SendHandshakeMessage(Message): # ----------------------------------------------------------------------------- class HID(EventEmitter): - l2cap_ctrl_channel: Optional[l2cap.ClassicChannel] - l2cap_intr_channel: Optional[l2cap.ClassicChannel] + l2cap_ctrl_channel: Optional[l2cap.ClassicChannel] = None + l2cap_intr_channel: Optional[l2cap.ClassicChannel] = None + connection: Optional[device.Connection] = None class Role(enum.IntEnum): HOST = 0x00 @@ -203,19 +204,29 @@ class HID(EventEmitter): def __init__(self, device: device.Device, role: Role) -> None: super().__init__() self.device = device - self.connection = None - self.remote_device_bd_address = None self.role = role - self.l2cap_ctrl_channel = None - self.l2cap_intr_channel = None - # Register ourselves with the L2CAP channel manager - device.register_l2cap_server(HID_CONTROL_PSM, self.on_connection) - device.register_l2cap_server(HID_INTERRUPT_PSM, self.on_connection) + device.register_l2cap_server(HID_CONTROL_PSM, self.on_l2cap_connection) + device.register_l2cap_server(HID_INTERRUPT_PSM, self.on_l2cap_connection) device.on('connection', self.on_device_connection) + def handle_get_report(self, pdu: bytes): + return + + def handle_set_report(self, pdu: bytes): + return + + def handle_get_protocol(self, pdu: bytes): + return + + def handle_set_protocol(self, pdu: bytes): + return + + def send_handshake_message(self, result_code: int): + return + async def connect_control_channel(self) -> None: # Create a new L2CAP connection - control channel try: @@ -259,20 +270,17 @@ class HID(EventEmitter): await channel.disconnect() def on_device_connection(self, connection: device.Connection) -> None: - self.connection = connection # type: ignore[assignment] - self.remote_device_bd_address = ( - connection.peer_address # type: ignore[assignment] - ) - connection.on('disconnection', self.on_disconnection) + self.connection = connection + connection.on('disconnection', self.on_device_disconnection) - def on_connection(self, l2cap_channel: l2cap.ClassicChannel) -> None: + def on_device_disconnection(self, reason: int) -> None: + self.connection = None + + def on_l2cap_connection(self, l2cap_channel: l2cap.ClassicChannel) -> None: logger.debug(f'+++ New L2CAP connection: {l2cap_channel}') l2cap_channel.on('open', lambda: self.on_l2cap_channel_open(l2cap_channel)) l2cap_channel.on('close', lambda: self.on_l2cap_channel_close(l2cap_channel)) - def on_disconnection(self, reason: int) -> None: - self.connection = None - def on_l2cap_channel_open(self, l2cap_channel: l2cap.ClassicChannel) -> None: if l2cap_channel.psm == HID_CONTROL_PSM: self.l2cap_ctrl_channel = l2cap_channel @@ -299,16 +307,16 @@ class HID(EventEmitter): self.emit('handshake', Message.Handshake(param)) elif message_type == Message.MessageType.GET_REPORT: logger.debug('<<< HID GET REPORT') - self.handle_get_report(pdu) # type: ignore[attr-defined] + self.handle_get_report(pdu) elif message_type == Message.MessageType.SET_REPORT: logger.debug('<<< HID SET REPORT') - self.handle_set_report(pdu) # type: ignore[attr-defined] + self.handle_set_report(pdu) elif message_type == Message.MessageType.GET_PROTOCOL: logger.debug('<<< HID GET PROTOCOL') - self.handle_get_protocol(pdu) # type: ignore[attr-defined] + self.handle_get_protocol(pdu) elif message_type == Message.MessageType.SET_PROTOCOL: logger.debug('<<< HID SET PROTOCOL') - self.handle_set_protocol(pdu) # type: ignore[attr-defined] + self.handle_set_protocol(pdu) elif message_type == Message.MessageType.DATA: logger.debug('<<< HID CONTROL DATA') self.emit('control_data', pdu) @@ -326,9 +334,7 @@ class HID(EventEmitter): logger.debug('<<< HID CONTROL OPERATION UNSUPPORTED') else: logger.debug('<<< HID MESSAGE TYPE UNSUPPORTED') - self.send_handshake_message( # type: ignore[attr-defined] - Message.Handshake.ERR_UNSUPPORTED_REQUEST - ) + self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST) def on_intr_pdu(self, pdu: bytes) -> None: logger.debug(f'<<< HID INTERRUPT PDU: {pdu.hex()}') @@ -379,10 +385,10 @@ class Device(HID): def __init__(self, device: device.Device) -> None: super().__init__(device, HID.Role.DEVICE) - self.get_report_cb = None - self.set_report_cb = None - self.get_protocol_cb = None - self.set_protocol_cb = None + get_report_cb: Optional[Callable[[int, int, int], None]] = None + set_report_cb: Optional[Callable[[int, int, int, bytes], None]] = None + get_protocol_cb: Optional[Callable[[], None]] = None + set_protocol_cb: Optional[Callable[[int, bytes], None]] = None def send_handshake_message(self, result_code: int) -> None: msg = SendHandshakeMessage(result_code) @@ -418,7 +424,7 @@ class Device(HID): data = bytearray() data.append(report_id) data.extend(ret.data) - if len(data) < self.l2cap_ctrl_channel.mtu: + if len(data) < self.l2cap_ctrl_channel.mtu: # type: ignore[union-attr] self.send_control_data(report_type=report_type, data=data) else: self.send_handshake_message(Message.Handshake.ERR_INVALID_PARAMETER) @@ -441,10 +447,8 @@ class Device(HID): report_type = pdu[0] & 0x03 report_id = pdu[1] report_data = pdu[2:] - report_size = len(pdu[1:]) - ret = self.set_report_cb( - report_id, report_type, report_size, report_data - ) # type: ignore + report_size = len(report_data) + 1 + ret = self.set_report_cb(report_id, report_type, report_size, report_data) if ret.status == self.GetSetReturn.SUCCESS: self.send_handshake_message(Message.Handshake.SUCCESSFUL) elif ret.status == self.GetSetReturn.ERR_INVALID_PARAMETER: diff --git a/examples/run_hid_device.py b/examples/run_hid_device.py index e0ac281d..0a5b1d0d 100644 --- a/examples/run_hid_device.py +++ b/examples/run_hid_device.py @@ -428,63 +428,62 @@ class DeviceData: deviceData = DeviceData() # ----------------------------------------------------------------------------- -async def keyboard_device(hid_device, command): +async def keyboard_device(hid_device): - if command == 'web': - # Start a Websocket server to receive events from a web page - async def serve(websocket, _path): - global deviceData - while True: - try: - message = await websocket.recv() - print('Received: ', str(message)) - parsed = json.loads(message) - message_type = parsed['type'] - if message_type == 'keydown': - # Only deal with keys a to z for now - key = parsed['key'] - if len(key) == 1: - code = ord(key) - if ord('a') <= code <= ord('z'): - hid_code = 0x04 + code - ord('a') - deviceData.keyboardData = bytearray( - [ - 0x01, - 0x00, - 0x00, - hid_code, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - ] - ) - hid_device.send_data(deviceData.keyboardData) - elif message_type == 'keyup': - deviceData.keyboardData = bytearray( - [0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] - ) - hid_device.send_data(deviceData.keyboardData) - elif message_type == "mousemove": - # logical min and max values - log_min = -127 - log_max = 127 - x = parsed['x'] - y = parsed['y'] - # limiting x and y values within logical max and min range - x = max(log_min, min(log_max, x)) - y = max(log_min, min(log_max, y)) - x_cord = x.to_bytes(signed=True) - y_cord = y.to_bytes(signed=True) - deviceData.mouseData = bytearray([0x02, 0x00]) + x_cord + y_cord - hid_device.send_data(deviceData.mouseData) - except websockets.exceptions.ConnectionClosedOK: - pass + # Start a Websocket server to receive events from a web page + async def serve(websocket, _path): + global deviceData + while True: + try: + message = await websocket.recv() + print('Received: ', str(message)) + parsed = json.loads(message) + message_type = parsed['type'] + if message_type == 'keydown': + # Only deal with keys a to z for now + key = parsed['key'] + if len(key) == 1: + code = ord(key) + if ord('a') <= code <= ord('z'): + hid_code = 0x04 + code - ord('a') + deviceData.keyboardData = bytearray( + [ + 0x01, + 0x00, + 0x00, + hid_code, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + ] + ) + hid_device.send_data(deviceData.keyboardData) + elif message_type == 'keyup': + deviceData.keyboardData = bytearray( + [0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] + ) + hid_device.send_data(deviceData.keyboardData) + elif message_type == "mousemove": + # logical min and max values + log_min = -127 + log_max = 127 + x = parsed['x'] + y = parsed['y'] + # limiting x and y values within logical max and min range + x = max(log_min, min(log_max, x)) + y = max(log_min, min(log_max, y)) + x_cord = x.to_bytes(signed=True) + y_cord = y.to_bytes(signed=True) + deviceData.mouseData = bytearray([0x02, 0x00]) + x_cord + y_cord + hid_device.send_data(deviceData.mouseData) + except websockets.exceptions.ConnectionClosedOK: + pass - # pylint: disable-next=no-member - await websockets.serve(serve, 'localhost', 8989) - await asyncio.get_event_loop().create_future() + # pylint: disable-next=no-member + await websockets.serve(serve, 'localhost', 8989) + await asyncio.get_event_loop().create_future() # ----------------------------------------------------------------------------- @@ -511,10 +510,10 @@ async def main(): if connection is not None: await connection.disconnect() - def on_hid_data_cb(pdu): + def on_hid_data_cb(pdu: bytes): print(f'Received Data, PDU: {pdu.hex()}') - def on_get_report_cb(report_id, report_type, buffer_size): + def on_get_report_cb(report_id: int, report_type: int, buffer_size: int): retValue = hid_device.GetSetStatus() print( "GET_REPORT report_id: " @@ -552,7 +551,9 @@ async def main(): return retValue - def on_set_report_cb(report_id, report_type, report_size, data): + def on_set_report_cb( + report_id: int, report_type: int, report_size: int, data: bytes + ): retValue = hid_device.GetSetStatus() print( "SET_REPORT report_id: " @@ -586,7 +587,7 @@ async def main(): retValue.status = hid_device.GetSetReturn.SUCCESS return retValue - def on_set_protocol_cb(protocol): + def on_set_protocol_cb(protocol: int): retValue = hid_device.GetSetStatus() # We do not support SET_PROTOCOL. print("SET_PROTOCOL report_id: " + str(protocol)) @@ -731,24 +732,14 @@ async def main(): else: print("Invalid option selected.") - if len(sys.argv) > 3: + if (len(sys.argv) > 3) and (command == 'test-mode'): + # Test mode for PTS/Unit testing command = sys.argv[3] - - if command == 'test-mode': - # Enabling menu for testing - await menu() - - elif command == 'web': - # Run as a keyboard and mouse device - await keyboard_device(hid_device, command) - - else: - print("Command incorrect. Switching to default: web") - await keyboard_device(hid_device, 'web') - + await menu() else: # default option is using keyboard.html (web) - await keyboard_device(hid_device, 'web') + print("Command incorrect. Switching to default") + await keyboard_device(hid_device) await hci_source.wait_for_termination() From 9da2e32ad721b65610ba2302112e302b3bbc36d0 Mon Sep 17 00:00:00 2001 From: skarnataki Date: Fri, 15 Dec 2023 09:42:57 +0000 Subject: [PATCH 18/19] Review comment Fix 3 - rename json file and usage of Optional in parameters --- bumble/hid.py | 30 ++++++++++++------- examples/{classic3.json => hid_keyboard.json} | 0 examples/run_hid_device.py | 9 +++--- 3 files changed, 23 insertions(+), 16 deletions(-) rename examples/{classic3.json => hid_keyboard.json} (100%) diff --git a/bumble/hid.py b/bumble/hid.py index 347c2187..cc522da3 100644 --- a/bumble/hid.py +++ b/bumble/hid.py @@ -27,6 +27,7 @@ from typing import Optional, Callable, TYPE_CHECKING from bumble import l2cap, device from bumble.colors import color from bumble.core import InvalidStateError, ProtocolError +from .hci import Address # ----------------------------------------------------------------------------- @@ -203,6 +204,7 @@ class HID(EventEmitter): def __init__(self, device: device.Device, role: Role) -> None: super().__init__() + self.remote_device_bd_address: Optional[Address] = None self.device = device self.role = role @@ -213,19 +215,19 @@ class HID(EventEmitter): device.on('connection', self.on_device_connection) def handle_get_report(self, pdu: bytes): - return + pass def handle_set_report(self, pdu: bytes): - return + pass def handle_get_protocol(self, pdu: bytes): - return + pass def handle_set_protocol(self, pdu: bytes): - return + pass def send_handshake_message(self, result_code: int): - return + pass async def connect_control_channel(self) -> None: # Create a new L2CAP connection - control channel @@ -271,6 +273,7 @@ class HID(EventEmitter): def on_device_connection(self, connection: device.Connection) -> None: self.connection = connection + self.remote_device_bd_address = connection.peer_address connection.on('disconnection', self.on_device_disconnection) def on_device_disconnection(self, reason: int) -> None: @@ -388,7 +391,7 @@ class Device(HID): get_report_cb: Optional[Callable[[int, int, int], None]] = None set_report_cb: Optional[Callable[[int, int, int, bytes], None]] = None get_protocol_cb: Optional[Callable[[], None]] = None - set_protocol_cb: Optional[Callable[[int, bytes], None]] = None + set_protocol_cb: Optional[Callable[[int], None]] = None def send_handshake_message(self, result_code: int) -> None: msg = SendHandshakeMessage(result_code) @@ -417,7 +420,7 @@ class Device(HID): buffer_size = 0 ret = self.get_report_cb(report_id, report_type, buffer_size) - + assert ret is not None if ret.status == self.GetSetReturn.FAILURE: self.send_handshake_message(Message.Handshake.ERR_UNKNOWN) elif ret.status == self.GetSetReturn.SUCCESS: @@ -435,7 +438,7 @@ class Device(HID): elif ret.status == self.GetSetReturn.ERR_UNSUPPORTED_REQUEST: self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST) - def register_get_report_cb(self, cb): + def register_get_report_cb(self, cb: Callable[[int, int, int], None]) -> None: self.get_report_cb = cb logger.debug("GetReport callback registered successfully") @@ -449,6 +452,7 @@ class Device(HID): report_data = pdu[2:] report_size = len(report_data) + 1 ret = self.set_report_cb(report_id, report_type, report_size, report_data) + assert ret is not None if ret.status == self.GetSetReturn.SUCCESS: self.send_handshake_message(Message.Handshake.SUCCESSFUL) elif ret.status == self.GetSetReturn.ERR_INVALID_PARAMETER: @@ -458,7 +462,9 @@ class Device(HID): else: self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST) - def register_set_report_cb(self, cb): + def register_set_report_cb( + self, cb: Callable[[int, int, int, bytes], None] + ) -> None: self.set_report_cb = cb logger.debug("SetReport callback registered successfully") @@ -468,12 +474,13 @@ class Device(HID): self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST) return ret = self.get_protocol_cb() + assert ret is not None if ret.status == self.GetSetReturn.SUCCESS: self.send_control_data(Message.ReportType.OTHER_REPORT, ret.data) else: self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST) - def register_get_protocol_cb(self, cb): + def register_get_protocol_cb(self, cb: Callable[[], None]) -> None: self.get_protocol_cb = cb logger.debug("GetProtocol callback registered successfully") @@ -483,12 +490,13 @@ class Device(HID): self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST) return ret = self.set_protocol_cb(pdu[0] & 0x01) + assert ret is not None if ret.status == self.GetSetReturn.SUCCESS: self.send_handshake_message(Message.Handshake.SUCCESSFUL) else: self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST) - def register_set_protocol_cb(self, cb): + def register_set_protocol_cb(self, cb: Callable[[int], None]) -> None: self.set_protocol_cb = cb logger.debug("SetProtocol callback registered successfully") diff --git a/examples/classic3.json b/examples/hid_keyboard.json similarity index 100% rename from examples/classic3.json rename to examples/hid_keyboard.json diff --git a/examples/run_hid_device.py b/examples/run_hid_device.py index 0a5b1d0d..b71e38be 100644 --- a/examples/run_hid_device.py +++ b/examples/run_hid_device.py @@ -496,8 +496,8 @@ async def main(): ' web (run a keyboard with keypress input from a web page, ' 'see keyboard.html' ) - print('example: python run_hid_device.py classic3.json usb:0 web') - print('example: python run_hid_device.py classic3.json usb:0 test-mode') + print('example: python run_hid_device.py hid_keyboard.json usb:0 web') + print('example: python run_hid_device.py hid_keyboard.json usb:0 test-mode') return @@ -732,13 +732,12 @@ async def main(): else: print("Invalid option selected.") - if (len(sys.argv) > 3) and (command == 'test-mode'): + if (len(sys.argv) > 3) and (sys.argv[3] == 'test-mode'): # Test mode for PTS/Unit testing - command = sys.argv[3] await menu() else: # default option is using keyboard.html (web) - print("Command incorrect. Switching to default") + print("Executing in Web mode") await keyboard_device(hid_device) await hci_source.wait_for_termination() From 0b314bd7f77712f32ebcd38b5bf73ad4d3266028 Mon Sep 17 00:00:00 2001 From: skarnataki Date: Mon, 18 Dec 2023 13:36:25 +0000 Subject: [PATCH 19/19] Updated absctract class and method for on_ctrl_pdu in hid.py --- bumble/hid.py | 115 +++++++++++++++++++++++++++----------------------- 1 file changed, 62 insertions(+), 53 deletions(-) diff --git a/bumble/hid.py b/bumble/hid.py index cc522da3..c0fb0535 100644 --- a/bumble/hid.py +++ b/bumble/hid.py @@ -21,8 +21,10 @@ import logging import enum import struct +from abc import ABC, abstractmethod from pyee import EventEmitter from typing import Optional, Callable, TYPE_CHECKING +from typing_extensions import override from bumble import l2cap, device from bumble.colors import color @@ -193,7 +195,7 @@ class SendHandshakeMessage(Message): # ----------------------------------------------------------------------------- -class HID(EventEmitter): +class HID(ABC, EventEmitter): l2cap_ctrl_channel: Optional[l2cap.ClassicChannel] = None l2cap_intr_channel: Optional[l2cap.ClassicChannel] = None connection: Optional[device.Connection] = None @@ -214,21 +216,6 @@ class HID(EventEmitter): device.on('connection', self.on_device_connection) - def handle_get_report(self, pdu: bytes): - pass - - def handle_set_report(self, pdu: bytes): - pass - - def handle_get_protocol(self, pdu: bytes): - pass - - def handle_set_protocol(self, pdu: bytes): - pass - - def send_handshake_message(self, result_code: int): - pass - async def connect_control_channel(self) -> None: # Create a new L2CAP connection - control channel try: @@ -300,44 +287,9 @@ class HID(EventEmitter): self.l2cap_intr_channel = None logger.debug(f'$$$ L2CAP channel close: {l2cap_channel}') + @abstractmethod def on_ctrl_pdu(self, pdu: bytes) -> None: - logger.debug(f'<<< HID CONTROL PDU: {pdu.hex()}') - param = pdu[0] & 0x0F - message_type = pdu[0] >> 4 - - if message_type == Message.MessageType.HANDSHAKE: - logger.debug(f'<<< HID HANDSHAKE: {Message.Handshake(param).name}') - self.emit('handshake', Message.Handshake(param)) - elif message_type == Message.MessageType.GET_REPORT: - logger.debug('<<< HID GET REPORT') - self.handle_get_report(pdu) - elif message_type == Message.MessageType.SET_REPORT: - logger.debug('<<< HID SET REPORT') - self.handle_set_report(pdu) - elif message_type == Message.MessageType.GET_PROTOCOL: - logger.debug('<<< HID GET PROTOCOL') - self.handle_get_protocol(pdu) - elif message_type == Message.MessageType.SET_PROTOCOL: - logger.debug('<<< HID SET PROTOCOL') - self.handle_set_protocol(pdu) - elif message_type == Message.MessageType.DATA: - logger.debug('<<< HID CONTROL DATA') - self.emit('control_data', pdu) - elif message_type == Message.MessageType.CONTROL: - if param == Message.ControlCommand.SUSPEND: - logger.debug('<<< HID SUSPEND') - self.emit('suspend') - elif param == Message.ControlCommand.EXIT_SUSPEND: - logger.debug('<<< HID EXIT SUSPEND') - self.emit('exit_suspend') - elif param == Message.ControlCommand.VIRTUAL_CABLE_UNPLUG: - logger.debug('<<< HID VIRTUAL CABLE UNPLUG') - self.emit('virtual_cable_unplug') - else: - logger.debug('<<< HID CONTROL OPERATION UNSUPPORTED') - else: - logger.debug('<<< HID MESSAGE TYPE UNSUPPORTED') - self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST) + pass def on_intr_pdu(self, pdu: bytes) -> None: logger.debug(f'<<< HID INTERRUPT PDU: {pdu.hex()}') @@ -393,6 +345,43 @@ class Device(HID): get_protocol_cb: Optional[Callable[[], None]] = None set_protocol_cb: Optional[Callable[[int], None]] = None + @override + def on_ctrl_pdu(self, pdu: bytes) -> None: + logger.debug(f'<<< HID CONTROL PDU: {pdu.hex()}') + param = pdu[0] & 0x0F + message_type = pdu[0] >> 4 + + if message_type == Message.MessageType.GET_REPORT: + logger.debug('<<< HID GET REPORT') + self.handle_get_report(pdu) + elif message_type == Message.MessageType.SET_REPORT: + logger.debug('<<< HID SET REPORT') + self.handle_set_report(pdu) + elif message_type == Message.MessageType.GET_PROTOCOL: + logger.debug('<<< HID GET PROTOCOL') + self.handle_get_protocol(pdu) + elif message_type == Message.MessageType.SET_PROTOCOL: + logger.debug('<<< HID SET PROTOCOL') + self.handle_set_protocol(pdu) + elif message_type == Message.MessageType.DATA: + logger.debug('<<< HID CONTROL DATA') + self.emit('control_data', pdu) + elif message_type == Message.MessageType.CONTROL: + if param == Message.ControlCommand.SUSPEND: + logger.debug('<<< HID SUSPEND') + self.emit('suspend') + elif param == Message.ControlCommand.EXIT_SUSPEND: + logger.debug('<<< HID EXIT SUSPEND') + self.emit('exit_suspend') + elif param == Message.ControlCommand.VIRTUAL_CABLE_UNPLUG: + logger.debug('<<< HID VIRTUAL CABLE UNPLUG') + self.emit('virtual_cable_unplug') + else: + logger.debug('<<< HID CONTROL OPERATION UNSUPPORTED') + else: + logger.debug('<<< HID MESSAGE TYPE UNSUPPORTED') + self.send_handshake_message(Message.Handshake.ERR_UNSUPPORTED_REQUEST) + def send_handshake_message(self, result_code: int) -> None: msg = SendHandshakeMessage(result_code) hid_message = bytes(msg) @@ -543,3 +532,23 @@ class Host(HID): hid_message = bytes(msg) logger.debug(f'>>> HID CONTROL EXIT SUSPEND, PDU:{hid_message.hex()}') self.send_pdu_on_ctrl(hid_message) + + @override + def on_ctrl_pdu(self, pdu: bytes) -> None: + logger.debug(f'<<< HID CONTROL PDU: {pdu.hex()}') + param = pdu[0] & 0x0F + message_type = pdu[0] >> 4 + if message_type == Message.MessageType.HANDSHAKE: + logger.debug(f'<<< HID HANDSHAKE: {Message.Handshake(param).name}') + self.emit('handshake', Message.Handshake(param)) + elif message_type == Message.MessageType.DATA: + logger.debug('<<< HID CONTROL DATA') + self.emit('control_data', pdu) + elif message_type == Message.MessageType.CONTROL: + if param == Message.ControlCommand.VIRTUAL_CABLE_UNPLUG: + logger.debug('<<< HID VIRTUAL CABLE UNPLUG') + self.emit('virtual_cable_unplug') + else: + logger.debug('<<< HID CONTROL OPERATION UNSUPPORTED') + else: + logger.debug('<<< HID MESSAGE TYPE UNSUPPORTED')