# Copyright 2021-2023 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 dataclasses import struct from dataclasses import field from bumble import hci # ----------------------------------------------------------------------------- # Constants # ----------------------------------------------------------------------------- # Android Vendor Specific Commands and Events. # Only a subset of the commands are implemented here currently. # # pylint: disable-next=line-too-long # See https://source.android.com/docs/core/connect/bluetooth/hci_requirements#chip-capabilities-and-configuration HCI_LE_GET_VENDOR_CAPABILITIES_COMMAND = hci.hci_vendor_command_op_code(0x153) HCI_LE_APCF_COMMAND = hci.hci_vendor_command_op_code(0x157) HCI_GET_CONTROLLER_ACTIVITY_ENERGY_INFO_COMMAND = hci.hci_vendor_command_op_code(0x159) HCI_A2DP_HARDWARE_OFFLOAD_COMMAND = hci.hci_vendor_command_op_code(0x15D) HCI_BLUETOOTH_QUALITY_REPORT_COMMAND = hci.hci_vendor_command_op_code(0x15E) HCI_DYNAMIC_AUDIO_BUFFER_COMMAND = hci.hci_vendor_command_op_code(0x15F) HCI_BLUETOOTH_QUALITY_REPORT_EVENT = 0x58 hci.HCI_Command.register_commands(globals()) # ----------------------------------------------------------------------------- @dataclasses.dataclass class HCI_LE_Get_Vendor_Capabilities_ReturnParameters(hci.HCI_StatusReturnParameters): max_advt_instances: int = field(metadata=hci.metadata(1), default=0) offloaded_resolution_of_private_address: int = field( metadata=hci.metadata(1), default=0 ) total_scan_results_storage: int = field(metadata=hci.metadata(2), default=0) max_irk_list_sz: int = field(metadata=hci.metadata(1), default=0) filtering_support: int = field(metadata=hci.metadata(1), default=0) max_filter: int = field(metadata=hci.metadata(1), default=0) activity_energy_info_support: int = field(metadata=hci.metadata(1), default=0) version_supported: int = field(metadata=hci.metadata(2), default=0) total_num_of_advt_tracked: int = field(metadata=hci.metadata(2), default=0) extended_scan_support: int = field(metadata=hci.metadata(1), default=0) debug_logging_supported: int = field(metadata=hci.metadata(1), default=0) le_address_generation_offloading_support: int = field( metadata=hci.metadata(1), default=0 ) a2dp_source_offload_capability_mask: int = field( metadata=hci.metadata(4), default=0 ) bluetooth_quality_report_support: int = field(metadata=hci.metadata(1), default=0) dynamic_audio_buffer_support: int = field(metadata=hci.metadata(4), default=0) @hci.HCI_SyncCommand.sync_command(HCI_LE_Get_Vendor_Capabilities_ReturnParameters) @dataclasses.dataclass class HCI_LE_Get_Vendor_Capabilities_Command( hci.HCI_SyncCommand[HCI_LE_Get_Vendor_Capabilities_ReturnParameters] ): # pylint: disable=line-too-long ''' See https://source.android.com/docs/core/connect/bluetooth/hci_requirements#vendor-specific-capabilities ''' @classmethod def parse_return_parameters(cls, parameters): # There are many versions of this data structure, so we need to parse until # there are no more bytes to parse, and leave un-signaled parameters set to # 0 return_parameters = HCI_LE_Get_Vendor_Capabilities_ReturnParameters( hci.HCI_ErrorCode.SUCCESS ) try: offset = 0 for field in cls.return_parameters_class.fields: field_name, field_type = field field_value, field_size = hci.HCI_Object.parse_field( parameters, offset, field_type ) setattr(return_parameters, field_name, field_value) offset += field_size except struct.error: pass return return_parameters # ----------------------------------------------------------------------------- # APCF Subcommands class LeApcfOpcode(hci.SpecableEnum): ENABLE = 0x00 SET_FILTERING_PARAMETERS = 0x01 BROADCASTER_ADDRESS = 0x02 SERVICE_UUID = 0x03 SERVICE_SOLICITATION_UUID = 0x04 LOCAL_NAME = 0x05 MANUFACTURER_DATA = 0x06 SERVICE_DATA = 0x07 TRANSPORT_DISCOVERY_SERVICE = 0x08 AD_TYPE_FILTER = 0x09 READ_EXTENDED_FEATURES = 0xFF @dataclasses.dataclass class HCI_LE_APCF_ReturnParameters(hci.HCI_StatusReturnParameters): opcode: int = field(metadata=LeApcfOpcode.type_metadata(1)) payload: bytes = field(metadata=hci.metadata('*')) @hci.HCI_SyncCommand.sync_command(HCI_LE_APCF_ReturnParameters) @dataclasses.dataclass class HCI_LE_APCF_Command(hci.HCI_SyncCommand[HCI_LE_APCF_ReturnParameters]): # pylint: disable=line-too-long ''' See https://source.android.com/docs/core/connect/bluetooth/hci_requirements#le_apcf_command NOTE: the subcommand-specific payloads are left as opaque byte arrays in this implementation. A future enhancement may define subcommand-specific data structures. ''' opcode: int = dataclasses.field(metadata=LeApcfOpcode.type_metadata(1)) payload: bytes = dataclasses.field(metadata=hci.metadata("*")) # ----------------------------------------------------------------------------- @dataclasses.dataclass class HCI_Get_Controller_Activity_Energy_Info_ReturnParameters( hci.HCI_StatusReturnParameters ): total_tx_time_ms: int = field(metadata=hci.metadata(4)) total_rx_time_ms: int = field(metadata=hci.metadata(4)) total_idle_time_ms: int = field(metadata=hci.metadata(4)) total_energy_used: int = field(metadata=hci.metadata(4)) @hci.HCI_SyncCommand.sync_command( HCI_Get_Controller_Activity_Energy_Info_ReturnParameters ) @dataclasses.dataclass class HCI_Get_Controller_Activity_Energy_Info_Command( hci.HCI_SyncCommand[HCI_Get_Controller_Activity_Energy_Info_ReturnParameters] ): # pylint: disable=line-too-long ''' See https://source.android.com/docs/core/connect/bluetooth/hci_requirements#le_get_controller_activity_energy_info ''' # ----------------------------------------------------------------------------- # A2DP Hardware Offload Subcommands class A2dpHardwareOffloadOpcode(hci.SpecableEnum): START_A2DP_OFFLOAD = 0x01 STOP_A2DP_OFFLOAD = 0x02 @dataclasses.dataclass class HCI_A2DP_Hardware_Offload_ReturnParameters(hci.HCI_StatusReturnParameters): opcode: int = dataclasses.field(metadata=A2dpHardwareOffloadOpcode.type_metadata(1)) payload: bytes = dataclasses.field(metadata=hci.metadata("*")) @hci.HCI_SyncCommand.sync_command(HCI_A2DP_Hardware_Offload_ReturnParameters) @dataclasses.dataclass class HCI_A2DP_Hardware_Offload_Command( hci.HCI_SyncCommand[HCI_A2DP_Hardware_Offload_ReturnParameters] ): # pylint: disable=line-too-long ''' See https://source.android.com/docs/core/connect/bluetooth/hci_requirements#a2dp-hardware-offload-support NOTE: the subcommand-specific payloads are left as opaque byte arrays in this implementation. A future enhancement may define subcommand-specific data structures. ''' opcode: int = dataclasses.field(metadata=A2dpHardwareOffloadOpcode.type_metadata(1)) payload: bytes = dataclasses.field(metadata=hci.metadata("*")) # ----------------------------------------------------------------------------- # Dynamic Audio Buffer Subcommands class DynamicAudioBufferOpcode(hci.SpecableEnum): GET_AUDIO_BUFFER_TIME_CAPABILITY = 0x01 @dataclasses.dataclass class HCI_Dynamic_Audio_Buffer_ReturnParameters(hci.HCI_StatusReturnParameters): opcode: int = dataclasses.field(metadata=DynamicAudioBufferOpcode.type_metadata(1)) payload: bytes = dataclasses.field(metadata=hci.metadata("*")) @hci.HCI_SyncCommand.sync_command(HCI_Dynamic_Audio_Buffer_ReturnParameters) @dataclasses.dataclass class HCI_Dynamic_Audio_Buffer_Command( hci.HCI_SyncCommand[HCI_Dynamic_Audio_Buffer_ReturnParameters] ): # pylint: disable=line-too-long ''' See https://source.android.com/docs/core/connect/bluetooth/hci_requirements#dynamic-audio-buffer-command NOTE: the subcommand-specific payloads are left as opaque byte arrays in this implementation. A future enhancement may define subcommand-specific data structures. ''' opcode: int = dataclasses.field(metadata=DynamicAudioBufferOpcode.type_metadata(1)) payload: bytes = dataclasses.field(metadata=hci.metadata("*")) # ----------------------------------------------------------------------------- class HCI_Android_Vendor_Event(hci.HCI_Extended_Event): event_code: int = hci.HCI_VENDOR_EVENT subevent_classes: dict[int, type[hci.HCI_Extended_Event]] = {} @classmethod def subclass_from_parameters( cls, parameters: bytes ) -> hci.HCI_Extended_Event | None: subevent_code = parameters[0] if subevent_code == HCI_BLUETOOTH_QUALITY_REPORT_EVENT: quality_report_id = parameters[1] if quality_report_id in (0x01, 0x02, 0x03, 0x04, 0x07, 0x08, 0x09): return HCI_Bluetooth_Quality_Report_Event.from_parameters(parameters) return None HCI_Android_Vendor_Event.register_subevents(globals()) hci.HCI_Event.add_vendor_factory(HCI_Android_Vendor_Event.subclass_from_parameters) # ----------------------------------------------------------------------------- @hci.HCI_Extended_Event.event @dataclasses.dataclass class HCI_Bluetooth_Quality_Report_Event(HCI_Android_Vendor_Event): # pylint: disable=line-too-long ''' See https://source.android.com/docs/core/connect/bluetooth/hci_requirements#bluetooth-quality-report-sub-event ''' quality_report_id: int = field(metadata=hci.metadata(1)) packet_types: int = field(metadata=hci.metadata(1)) connection_handle: int = field(metadata=hci.metadata(2)) connection_role: int = field(metadata=hci.Role.type_metadata(1)) tx_power_level: int = field(metadata=hci.metadata(-1)) rssi: int = field(metadata=hci.metadata(-1)) snr: int = field(metadata=hci.metadata(1)) unused_afh_channel_count: int = field(metadata=hci.metadata(1)) afh_select_unideal_channel_count: int = field(metadata=hci.metadata(1)) lsto: int = field(metadata=hci.metadata(2)) connection_piconet_clock: int = field(metadata=hci.metadata(4)) retransmission_count: int = field(metadata=hci.metadata(4)) no_rx_count: int = field(metadata=hci.metadata(4)) nak_count: int = field(metadata=hci.metadata(4)) last_tx_ack_timestamp: int = field(metadata=hci.metadata(4)) flow_off_count: int = field(metadata=hci.metadata(4)) last_flow_on_timestamp: int = field(metadata=hci.metadata(4)) buffer_overflow_bytes: int = field(metadata=hci.metadata(4)) buffer_underflow_bytes: int = field(metadata=hci.metadata(4)) bdaddr: hci.Address = field(metadata=hci.metadata(hci.Address.parse_address)) cal_failed_item_count: int = field(metadata=hci.metadata(1)) tx_total_packets: int = field(metadata=hci.metadata(4)) tx_unacked_packets: int = field(metadata=hci.metadata(4)) tx_flushed_packets: int = field(metadata=hci.metadata(4)) tx_last_subevent_packets: int = field(metadata=hci.metadata(4)) crc_error_packets: int = field(metadata=hci.metadata(4)) rx_duplicate_packets: int = field(metadata=hci.metadata(4)) rx_unreceived_packets: int = field(metadata=hci.metadata(4)) vendor_specific_parameters: bytes = field(metadata=hci.metadata('*'))