#!/usr/bin/env python3 import os import re import sys import time import struct import subprocess from bluetooth._bluetooth import SOL_HCI from bluetooth._bluetooth import MSG_WAITALL from bluetooth._bluetooth import hci_open_dev from bluetooth._bluetooth import hci_close_dev from bluetooth._bluetooth import HCI_FILTER from bluetooth._bluetooth import hci_filter_new from bluetooth._bluetooth import hci_filter_clear from bluetooth._bluetooth import hci_filter_set_ptype from bluetooth._bluetooth import hci_filter_set_event from bluetooth._bluetooth import hci_filter_set_opcode from bluetooth._bluetooth import cmd_opcode_pack from bluetooth._bluetooth import hci_send_cmd from bluetooth._bluetooth import OGF_LINK_CTL from bluetooth._bluetooth import OCF_INQUIRY_CANCEL from bluetooth._bluetooth import OCF_EXIT_PERIODIC_INQUIRY from bluetooth._bluetooth import OCF_CREATE_CONN from bluetooth._bluetooth import OCF_DISCONNECT from bluetooth._bluetooth import OCF_READ_REMOTE_FEATURES from bluetooth._bluetooth import OCF_READ_REMOTE_VERSION OCF_READ_REMOTE_EXT_FEATURES = 0x001C OCF_CREATE_CONN_CANCEL = 0x0008 from bluetooth._bluetooth import OGF_HOST_CTL from bluetooth._bluetooth import OCF_RESET from bluetooth._bluetooth import OCF_SET_EVENT_FLT from bluetooth._bluetooth import OCF_WRITE_SCAN_ENABLE from bluetooth._bluetooth import OCF_READ_LOCAL_NAME from bluetooth._bluetooth import OCF_WRITE_INQUIRY_MODE from bluetooth._bluetooth import OCF_WRITE_AUTH_ENABLE from bluetooth._bluetooth import OCF_READ_CLASS_OF_DEV from bluetooth._bluetooth import OCF_READ_PAGE_TIMEOUT from bluetooth._bluetooth import OCF_WRITE_PAGE_TIMEOUT OCF_READ_EXT_PAGE_TIMEOUT = 0x007E OCF_WRITE_EXT_PAGE_TIMEOUT = 0x007F from bluetooth._bluetooth import OGF_INFO_PARAM from bluetooth._bluetooth import OCF_READ_BD_ADDR OGF_LE_CTL = 0x08 OCF_LE_SET_ADVERTISING_PARAMETERS = 0x0006 OCF_LE_SET_ADVERTISING_DATA = 0x0008 OCF_LE_SET_SCAN_RESPONSE_DATA = 0x0009 OCF_LE_SET_ADVERTISING_ENABLE = 0x000A OCF_LE_SET_SCAN_ENABLE = 0x000C # EVT_*_SIZE indicates Parameter_Total_Length of the HCI event packet from bluetooth._bluetooth import HCI_EVENT_PKT from bluetooth._bluetooth import HCI_MAX_EVENT_SIZE # bluez set this to 260, but the max is 257 from bluetooth._bluetooth import EVT_CMD_COMPLETE # The size of event parameters of HCI_Command_Complete event is variable, so # EVT_CMD_COMPLETE_SIZE only defines the fixed part, Num_HCI_Command_Packets and # Command_Opcode (total 3 bytes). from bluetooth._bluetooth import EVT_CMD_COMPLETE_SIZE from bluetooth._bluetooth import EVT_INQUIRY_COMPLETE from bluetooth._bluetooth import EVT_CONN_COMPLETE from bluetooth._bluetooth import EVT_CONN_COMPLETE_SIZE from bluetooth._bluetooth import EVT_DISCONN_COMPLETE from bluetooth._bluetooth import EVT_DISCONN_COMPLETE_SIZE from bluetooth._bluetooth import EVT_CMD_STATUS from bluetooth._bluetooth import EVT_CMD_STATUS_SIZE from bluetooth._bluetooth import EVT_READ_REMOTE_FEATURES_COMPLETE from bluetooth._bluetooth import EVT_READ_REMOTE_FEATURES_COMPLETE_SIZE from bluetooth._bluetooth import EVT_READ_REMOTE_VERSION_COMPLETE from bluetooth._bluetooth import EVT_READ_REMOTE_VERSION_COMPLETE_SIZE EVT_READ_REMOTE_EXT_FEATURES_COMPLETE = 0x23 EVT_READ_REMOTE_EXT_FEATURES_COMPLETE_SIZE = 13 EVT_LE_META = 0x3E SUBEVT_LE_ADVERTISING_REPORT = 0x02 # HCI_LE_Scan_Request_Received event 在 Bluetooth 5.0 中加入 SUBEVT_LE_SCAN_REQUEST_RECEIVED = 0x13 SUBEVT_LE_SCAN_REQUEST_RECEIVED_SIZE = 9 BD_ADDR_LEN = 6 LINK_CTRL_CMD_OGF = 0x01 HCI_CTRL_BASEBAND_CMD_OGF = 0x03 # // bluetooth/hci.h # struct hci_filter { # uint32_t type_mask; # uint32_t event_mask[2]; # uint16_t opcode; # }; HCI_FILTER_SIZE = 14 # 4 + 4*2 + 2 from .ui import WARNING class HCI: def __init__(self, iface='hci0'): self.devid = HCI.hcix2devid(iface) if self.devid == -1: raise ValueError @classmethod def hcix2devid(cls, hcix:str) -> int: devid = re.findall('(0)|([1-9]+)', hcix, flags=0) if len(devid) == 1 and ((devid[0][0] == '') ^ (devid[0][1] == '')): devid = int(devid[0][0]) if devid[0][0] != '' else int(devid[0][1]) else: devid = -1 return devid ################### Link Control Commands ################################# def inquiry_cancel(self) -> dict: ''' Return -- { 'Num_HCI_Command_Packets': int, 'Command_Opcode': int, 'Status': int } ''' dd = hci_open_dev(self.devid) flt = hci_filter_new() hci_filter_set_ptype(flt, HCI_EVENT_PKT) hci_filter_set_event(flt, EVT_CMD_COMPLETE) hci_filter_set_opcode( flt, cmd_opcode_pack(OGF_LINK_CTL, OCF_INQUIRY_CANCEL)) dd.setsockopt(SOL_HCI, HCI_FILTER, flt) hci_send_cmd(dd, OGF_LINK_CTL, OCF_INQUIRY_CANCEL) event_params = dd.recv(3+EVT_CMD_COMPLETE_SIZE+1)[3:] num_hci_cmd_pkts, cmd_opcode, status = struct.unpack('<BHB', event_params) event_params = { 'Num_HCI_Command_Packets': num_hci_cmd_pkts, 'Command_Opcode': cmd_opcode, 'Status': status } hci_close_dev(dd.fileno()) return event_params def exit_periodic_inquiry_mode(self) -> dict: ''' Return -- { 'Num_HCI_Command_Packets': int, 'Command_Opcode': int, 'Status': int } ''' dd = hci_open_dev(self.devid) flt = hci_filter_new() hci_filter_set_ptype(flt, HCI_EVENT_PKT) hci_filter_set_event(flt, EVT_CMD_COMPLETE) hci_filter_set_opcode( flt, cmd_opcode_pack(OGF_LINK_CTL, OCF_EXIT_PERIODIC_INQUIRY)) dd.setsockopt(SOL_HCI, HCI_FILTER, flt) hci_send_cmd(dd, OGF_LINK_CTL, OCF_EXIT_PERIODIC_INQUIRY) event_params = dd.recv(3+EVT_CMD_COMPLETE_SIZE+1)[3:] num_hci_cmd_pkts, cmd_opcode, status = struct.unpack('<BHB', event_params) event_params = { 'Num_HCI_Command_Packets': num_hci_cmd_pkts, 'Command_Opcode': cmd_opcode, 'Status': status } hci_close_dev(dd.fileno()) return status def create_connection(self, cmd_params:dict): ''' cmd_params -- { 'BD_ADDR': str, 'Packet_Type': int, 'Page_Scan_Repetition_Mode': , 'Reserved': , 'Clock_Offset': , 'Allow_Role_Switch': } ''' dd = hci_open_dev(self.devid) bin_cmd_params = bytes.fromhex( cmd_params['BD_ADDR'].replace(':', ''))[::-1] + \ cmd_params['Packet_Type'].to_bytes(2, 'little') + \ cmd_params['Page_Scan_Repetition_Mode'].to_bytes(1, 'little') + \ cmd_params['Reserved'].to_bytes(1, 'little') + \ cmd_params['Clock_Offset'].to_bytes(2, 'little') + \ cmd_params['Allow_Role_Switch'].to_bytes(1, 'little') flt = hci_filter_new() hci_filter_set_ptype(flt, HCI_EVENT_PKT) hci_filter_set_event(flt, EVT_CONN_COMPLETE) dd.setsockopt(SOL_HCI, HCI_FILTER, flt) hci_send_cmd(dd, OGF_LINK_CTL, OCF_CREATE_CONN, bin_cmd_params) while True: event_params = dd.recv(3+EVT_CONN_COMPLETE_SIZE)[3:] status, conn_handle, bd_addr, link_type, encrypt_enabled = \ struct.unpack('<BH6sBB', event_params) event_params = { 'Status': status, 'Connection_Handle': conn_handle, 'BD_ADDR': ':'.join(['%02x'%b for b in bd_addr[::-1]]), 'Link_Type': link_type, 'Encryption_Enabled': encrypt_enabled, } if event_params['BD_ADDR'].lower() == cmd_params['BD_ADDR'].lower(): break else: print(WARNING, 'Another HCI_Connection_Complete event detected', event_params) hci_close_dev(dd.fileno()) return event_params def disconnect(self, cmd_params:dict) -> dict: ''' cmd_params -- { 'Connection_Handle': int, 2 bytes, 'Reason': int, 1 bytes } ''' dd = hci_open_dev(self.devid) flt = hci_filter_new() hci_filter_clear(flt) hci_filter_set_ptype(flt, HCI_EVENT_PKT) hci_filter_set_event(flt, EVT_DISCONN_COMPLETE) dd.setsockopt(SOL_HCI, HCI_FILTER, flt) bin_cmd_params = cmd_params['Connection_Handle'].to_bytes(2, 'little') + \ cmd_params['Reason'].to_bytes(1, 'little') hci_send_cmd(dd, OGF_LINK_CTL, OCF_DISCONNECT, bin_cmd_params) # Receive and exclude HCI packet type (1 B) event_params = dd.recv(3+EVT_DISCONN_COMPLETE_SIZE)[3:] status, conn_handle, reason, = struct.unpack( '<BHB', event_params) event_params = { 'Status': status, 'Connection_Handle': conn_handle, 'Reason': reason } hci_close_dev(dd.fileno()) return event_params def create_connection_cancel(self, cmd_params:dict) -> dict: ''' cmd_params -- { 'BD_ADDR': str } ''' dd = hci_open_dev(self.devid) bin_cmd_params = bytes.fromhex(cmd_params['BD_ADDR'].replace(':', '')[::-1]) flt = hci_filter_new() hci_filter_set_ptype(flt, HCI_EVENT_PKT) hci_filter_set_event(flt, EVT_CMD_COMPLETE) hci_filter_set_opcode( flt, cmd_opcode_pack(OGF_LINK_CTL, OCF_CREATE_CONN_CANCEL)) dd.setsockopt(SOL_HCI, HCI_FILTER, flt) hci_send_cmd(dd, OGF_LINK_CTL, OCF_CREATE_CONN_CANCEL, bin_cmd_params) event_params = dd.recv(3+EVT_CMD_COMPLETE_SIZE+7)[3:] num_hci_cmd_pkts, cmd_opcode, status, \ bdaddr = struct.unpack('<BHB6s', event_params) hci_close_dev(dd.fileno()) return event_params def read_remote_supported_features(self, cmd_params:dict) -> dict: ''' cmd_params -- {'Connection_Handle': 0x0000 to 0x0EFF} ''' dd = hci_open_dev(self.devid) bin_cmd_params = cmd_params['Connection_Handle'].to_bytes(2, 'little') flt = hci_filter_new() hci_filter_set_ptype(flt, HCI_EVENT_PKT) hci_filter_set_event(flt, EVT_READ_REMOTE_FEATURES_COMPLETE) dd.setsockopt(SOL_HCI, HCI_FILTER, flt) hci_send_cmd(dd, OGF_LINK_CTL, OCF_READ_REMOTE_FEATURES, bin_cmd_params) while True: event_params = dd.recv(3+EVT_READ_REMOTE_FEATURES_COMPLETE_SIZE)[3:] status, conn_handle, lmp_features = struct.unpack('<BH8s', event_params) event_params = { 'Status': status, 'Connection_Handle': conn_handle, 'LMP_Features': lmp_features } if event_params['Connection_Handle'] == cmd_params['Connection_Handle']: break hci_close_dev(dd.fileno()) return event_params def read_remote_extended_features(self, cmd_params:dict) -> dict: ''' cmd_params -- { 'Connection_Handle': 0x0000 to 0x0EFF, 'Page_Number': int } ''' dd = hci_open_dev(self.devid) bin_cmd_params = cmd_params[ 'Connection_Handle'].to_bytes(2, 'little') + \ cmd_params['Page_Number'].to_bytes(1, 'little') flt = hci_filter_new() hci_filter_set_ptype(flt, HCI_EVENT_PKT) hci_filter_set_event(flt, EVT_READ_REMOTE_EXT_FEATURES_COMPLETE) dd.setsockopt(SOL_HCI, HCI_FILTER, flt) hci_send_cmd(dd, OGF_LINK_CTL, OCF_READ_REMOTE_EXT_FEATURES, bin_cmd_params) while True: event_params = dd.recv(3 + \ EVT_READ_REMOTE_EXT_FEATURES_COMPLETE_SIZE)[3:] status, conn_handle, page_num, max_page_num, ext_lmp_features = \ struct.unpack('<BHBB8s', event_params) event_params = { 'Status': status, 'Connection_Handle': conn_handle, 'Page_Number': page_num, 'Maximum_Page_Number': max_page_num, 'Extended_LMP_Features': ext_lmp_features } if event_params['Connection_Handle'] == cmd_params['Connection_Handle'] and \ event_params['Page_Number'] == cmd_params['Page_Number']: break hci_close_dev(dd.fileno()) return event_params def read_remote_version_information(self, cmd_params:dict) -> dict: ''' cmd_params -- { 'Connection_Handle': 0x0000 } ''' dd = hci_open_dev(self.devid) bin_cmd_params = cmd_params['Connection_Handle'].to_bytes(2, 'little') flt = hci_filter_new() hci_filter_set_ptype(flt, HCI_EVENT_PKT) hci_filter_set_event(flt, EVT_READ_REMOTE_VERSION_COMPLETE) dd.setsockopt(SOL_HCI, HCI_FILTER, flt) hci_send_cmd(dd, OGF_LINK_CTL, OCF_READ_REMOTE_VERSION, bin_cmd_params) while True: event_params = dd.recv(3 + \ EVT_READ_REMOTE_VERSION_COMPLETE_SIZE)[3:] status, conn_handle, ver, manufacturer_name, subver = \ struct.unpack('<BHBHH', event_params) event_params = { 'Status': status, 'Connection_Handle': conn_handle, 'Version': ver, 'Manufacturer_Name': manufacturer_name, 'Subversion': subver } if event_params['Connection_Handle'] == cmd_params['Connection_Handle']: break hci_close_dev(dd.fileno()) return event_params ################## Controller & Baseband Commands ######################### def reset(self) -> dict: dd = hci_open_dev(self.devid) flt = hci_filter_new() hci_filter_set_ptype(flt, HCI_EVENT_PKT) hci_filter_set_event(flt, EVT_CMD_COMPLETE) hci_filter_set_opcode(flt, cmd_opcode_pack(OGF_HOST_CTL, OCF_RESET)) dd.setsockopt(SOL_HCI, HCI_FILTER, flt) hci_send_cmd(dd, OGF_HOST_CTL, OCF_RESET) event_params = dd.recv(3+EVT_CMD_COMPLETE_SIZE+1)[3:] num_hci_cmd_pkts, cmd_opcode, status = struct.unpack('<BHB', event_params) event_params = { 'Num_HCI_Command_Packets': num_hci_cmd_pkts, 'Command_Opcode': cmd_opcode, 'Status': status } hci_close_dev(dd.fileno()) return event_params def write_scan_enable(self, cmd_params={'Scan_Enable': 0x00}) -> dict: dd = hci_open_dev(self.devid) bin_cmd_params = cmd_params['Scan_Enable'].to_bytes(1, 'little') flt = hci_filter_new() hci_filter_set_ptype(flt, HCI_EVENT_PKT) hci_filter_set_event(flt, EVT_CMD_COMPLETE) hci_filter_set_opcode( flt, cmd_opcode_pack(OGF_HOST_CTL, OCF_WRITE_SCAN_ENABLE)) dd.setsockopt(SOL_HCI, HCI_FILTER, flt) hci_send_cmd(dd, OGF_HOST_CTL, OCF_WRITE_SCAN_ENABLE, bin_cmd_params) event_params = dd.recv(3+EVT_CMD_COMPLETE_SIZE+1)[3:] num_hci_cmd_pkts, cmd_opcode, status = struct.unpack('<BHB', event_params) event_params = { 'Num_HCI_Command_Packets': num_hci_cmd_pkts, 'Command_Opcode': cmd_opcode, 'Status': status } hci_close_dev(dd.fileno()) return event_params def set_event_filter(self, cmd_params:dict) -> dict: '''A little complicated. see the core specification BLUETOOTH CORE SPECIFICATION Version 5.2 | Vol 4, Part E page 2078. Only support Filter_Type = 0x00 now. ''' dd = hci_open_dev(self.devid) bin_cmd_params = cmd_params['Filter_Type'].to_bytes(1, 'little') flt = hci_filter_new() hci_filter_set_ptype(flt, HCI_EVENT_PKT) hci_filter_set_event(flt, EVT_CMD_COMPLETE) hci_filter_set_opcode(flt, cmd_opcode_pack(OGF_HOST_CTL, OCF_SET_EVENT_FLT)) dd.setsockopt(SOL_HCI, HCI_FILTER, flt) hci_send_cmd(dd, OGF_HOST_CTL, OCF_SET_EVENT_FLT, bin_cmd_params) event_params = dd.recv(3+EVT_CMD_COMPLETE_SIZE+1)[3:] num_hci_cmd_pkts, cmd_opcode, status = struct.unpack('<BHB', event_params) event_params = { 'Num_HCI_Command_Packets': num_hci_cmd_pkts, 'Command_Opcode': cmd_opcode, 'Status': status } hci_close_dev(dd.fileno()) return event_params def read_local_name(self) -> dict: dd = hci_open_dev(self.devid) flt = hci_filter_new() hci_filter_set_ptype(flt, HCI_EVENT_PKT) hci_filter_set_event(flt, EVT_CMD_COMPLETE) hci_filter_set_opcode( flt, cmd_opcode_pack(OGF_HOST_CTL, OCF_READ_LOCAL_NAME)) dd.setsockopt(SOL_HCI, HCI_FILTER, flt) hci_send_cmd(dd, OGF_HOST_CTL, OCF_READ_LOCAL_NAME) event_params = dd.recv(3+EVT_CMD_COMPLETE_SIZE+249)[3:] num_hci_cmd_pkts, cmd_opcode, status, local_name = struct.unpack('<BHB248s', event_params) event_params = { 'Num_HCI_Command_Packets': num_hci_cmd_pkts, 'Command_Opcode': cmd_opcode, 'Status': status, 'Local_Name': local_name.decode() } hci_close_dev(dd.fileno()) return event_params def write_inquiry_mode(self, cmd_params={'Inquiry_Mode': 0x00}) -> dict: dd = hci_open_dev(self.devid) bin_cmd_params = cmd_params['Inquiry_Mode'].to_bytes(1, 'little') flt = hci_filter_new() hci_filter_set_ptype(flt, HCI_EVENT_PKT) hci_filter_set_event(flt, EVT_CMD_COMPLETE) hci_filter_set_opcode( flt, cmd_opcode_pack(OGF_HOST_CTL, OCF_WRITE_INQUIRY_MODE)) dd.setsockopt(SOL_HCI, HCI_FILTER, flt) hci_send_cmd(dd, OGF_HOST_CTL, OCF_WRITE_INQUIRY_MODE, bin_cmd_params) event_params = dd.recv(3+EVT_CMD_COMPLETE_SIZE+1)[3:] num_hci_cmd_pkts, cmd_opcode, status = struct.unpack('<BHB', event_params) event_params = { 'Num_HCI_Command_Packets': num_hci_cmd_pkts, 'Command_Opcode': cmd_opcode, 'Status': status } hci_close_dev(dd.fileno()) return event_params def read_page_timeout(self) -> dict: dd = hci_open_dev(self.devid) flt = hci_filter_new() hci_filter_set_ptype(flt, HCI_EVENT_PKT) hci_filter_set_event(flt, EVT_CMD_COMPLETE) hci_filter_set_opcode( flt, cmd_opcode_pack(OGF_HOST_CTL, OCF_READ_PAGE_TIMEOUT)) dd.setsockopt(SOL_HCI, HCI_FILTER, flt) hci_send_cmd(dd, OGF_HOST_CTL, OCF_READ_PAGE_TIMEOUT) event_params = dd.recv(3+EVT_CMD_COMPLETE_SIZE+3)[3:] num_hci_cmd_pkts, cmd_opcode, status, page_timeout = struct.unpack('<BHBH', event_params) event_params = { 'Num_HCI_Command_Packets': num_hci_cmd_pkts, 'Command_Opcode': cmd_opcode, 'Status': status, 'Page_Timeout': page_timeout } hci_close_dev(dd.fileno()) return event_params def write_page_timeout(self, cmd_params={'Page_Timeout': 0x2000}) -> dict: dd = hci_open_dev(self.devid) bin_cmd_params = cmd_params['Page_Timeout'].to_bytes(2, 'little') flt = hci_filter_new() hci_filter_set_ptype(flt, HCI_EVENT_PKT) hci_filter_set_event(flt, EVT_CMD_COMPLETE) hci_filter_set_opcode( flt, cmd_opcode_pack(OGF_HOST_CTL, OCF_WRITE_PAGE_TIMEOUT)) dd.setsockopt(SOL_HCI, HCI_FILTER, flt) hci_send_cmd(dd, OGF_HOST_CTL, OCF_WRITE_PAGE_TIMEOUT, bin_cmd_params) event_params = dd.recv(3+EVT_CMD_COMPLETE_SIZE+3)[3:] num_hci_cmd_pkts, cmd_opcode, status = struct.unpack('<BHB', event_params) event_params = { 'Num_HCI_Command_Packets': num_hci_cmd_pkts, 'Command_Opcode': cmd_opcode, 'Status': status } hci_close_dev(dd.fileno()) return event_params def write_authentication_enable(self, cmd_params={'Authentication_Enable': 0x00}) -> dict: dd = hci_open_dev(self.devid) bin_cmd_params = cmd_params['Authentication_Enable'].to_bytes(1, 'little') flt = hci_filter_new() hci_filter_set_ptype(flt, HCI_EVENT_PKT) hci_filter_set_event(flt, EVT_CMD_COMPLETE) hci_filter_set_opcode( flt, cmd_opcode_pack(OGF_HOST_CTL, OCF_WRITE_AUTH_ENABLE)) dd.setsockopt(SOL_HCI, HCI_FILTER, flt) hci_send_cmd(dd, OGF_HOST_CTL, OCF_WRITE_AUTH_ENABLE, bin_cmd_params) event_params = dd.recv(3+EVT_CMD_COMPLETE_SIZE+3)[3:] num_hci_cmd_pkts, cmd_opcode, status = struct.unpack('<BHB', event_params) event_params = { 'Num_HCI_Command_Packets': num_hci_cmd_pkts, 'Command_Opcode': cmd_opcode, 'Status': status } hci_close_dev(dd.fileno()) return event_params def read_class_of_device(self) -> dict: dd = hci_open_dev(self.devid) flt = hci_filter_new() hci_filter_set_ptype(flt, HCI_EVENT_PKT) hci_filter_set_event(flt, EVT_CMD_COMPLETE) hci_filter_set_opcode( flt, cmd_opcode_pack(OGF_HOST_CTL, OCF_READ_CLASS_OF_DEV)) dd.setsockopt(SOL_HCI, HCI_FILTER, flt) hci_send_cmd(dd, OGF_HOST_CTL, OCF_READ_CLASS_OF_DEV) event_params = dd.recv(3+HCI_MAX_EVENT_SIZE)[3:] num_hci_cmd_pkts, cmd_opcode, status, cod = struct.unpack('<BHB3s', event_params) event_params = { 'Num_HCI_Command_Packets': num_hci_cmd_pkts, 'Command_Opcode': cmd_opcode, 'Status': status, 'Class_Of_Device': cod[::-1] } hci_close_dev(dd.fileno()) return event_params def read_extended_page_timeout(self): dd = hci_open_dev(self.devid) flt = hci_filter_new() hci_filter_set_ptype(flt, HCI_EVENT_PKT) hci_filter_set_event(flt, EVT_CMD_COMPLETE) hci_filter_set_opcode( flt, cmd_opcode_pack(OGF_HOST_CTL, OCF_READ_EXT_PAGE_TIMEOUT)) dd.setsockopt(SOL_HCI, HCI_FILTER, flt) hci_send_cmd(dd, OGF_HOST_CTL, OCF_READ_EXT_PAGE_TIMEOUT) event_params = dd.recv(3+EVT_CMD_COMPLETE_SIZE+3)[3:] num_hci_cmd_pkts, cmd_opcode, status, \ ext_page_timeout = struct.unpack('<BHBH', event_params) event_params = { 'Num_HCI_Command_Packets': num_hci_cmd_pkts, 'Command_Opcode': cmd_opcode, 'Status': status, 'Extended_Page_Timeout': ext_page_timeout } hci_close_dev(dd.fileno()) return event_params #################### Informational Parameters ############################### def read_bd_addr(self) -> dict: r"""'Return BD_ADDR string "XX:XX:XX:XX:XX:XX'""" dd = hci_open_dev(self.devid) flt = hci_filter_new() hci_filter_set_ptype(flt, HCI_EVENT_PKT) hci_filter_set_event(flt, EVT_CMD_COMPLETE) hci_filter_set_opcode( flt, cmd_opcode_pack(OGF_INFO_PARAM, OCF_READ_BD_ADDR)) dd.setsockopt(SOL_HCI, HCI_FILTER, flt) hci_send_cmd(dd, OGF_INFO_PARAM, OCF_READ_BD_ADDR) event_params = dd.recv(HCI_MAX_EVENT_SIZE)[3:] num_hci_cmd_pkts, cmd_opcode, status, bd_addr = struct.unpack("<BHB6s", event_params) bd_addr = ["%02X"%b for b in bd_addr] bd_addr.reverse() event_params = { 'Num_HCI_Command_Packets': num_hci_cmd_pkts, 'Command_Opcode': cmd_opcode, 'Status': status, 'BD_ADDR': ':'.join(bd_addr) } hci_close_dev(dd.fileno()) return event_params #################### LE Controller Commands ############################### def le_set_advertising_parameters(self, cmd_params={ 'Advertising_Interval_Min': 0x0800, 'Advertising_Interval_Max': 0x0800, 'Advertising_Type': 0x00, # ADV_IND 'Own_Address_Type': 0x00, # Public Device Address 'Peer_Address_Type': 0x00, # Public Device Address 'Peer_Address': bytes(6), 'Advertising_Channel_Map': 0x07, # 37, 38, 39 'Advertising_Filter_Policy': 0x00 # Process scan and connection requests from all devices }) -> dict: dd = hci_open_dev(self.devid) bin_cmd_params = cmd_params['Advertising_Interval_Min'].to_bytes(2, 'little') + \ cmd_params['Advertising_Interval_Max'].to_bytes(2, 'little') + \ cmd_params['Advertising_Type'].to_bytes(1, 'little') + \ cmd_params['Own_Address_Type'].to_bytes(1, 'little') + \ cmd_params['Peer_Address_Type'].to_bytes(1, 'little') + \ cmd_params['Peer_Address'][::-1] + \ cmd_params['Advertising_Channel_Map'].to_bytes(1, 'little') + \ cmd_params['Advertising_Filter_Policy'].to_bytes(1, 'little') flt = hci_filter_new() hci_filter_set_ptype(flt, HCI_EVENT_PKT) hci_filter_set_event(flt, EVT_CMD_COMPLETE) hci_filter_set_opcode( flt, cmd_opcode_pack(OGF_LE_CTL, OCF_LE_SET_ADVERTISING_PARAMETERS)) dd.setsockopt(SOL_HCI, HCI_FILTER, flt) hci_send_cmd(dd, OGF_LE_CTL, OCF_LE_SET_ADVERTISING_PARAMETERS, bin_cmd_params) event_params = dd.recv(3+EVT_CMD_COMPLETE_SIZE+1)[3:] num_hci_cmd_pkts, cmd_opcode, status = struct.unpack('<BHB', event_params) event_params = { 'Num_HCI_Command_Packets': num_hci_cmd_pkts, 'Command_Opcode': cmd_opcode, 'Status': status } hci_close_dev(dd.fileno()) return event_params def le_set_advertising_data(self, cmd_params={ 'Advertising_Data_Length': 0x1f, 'Advertising_Data': bytes(0x1f)}) -> dict: dd = hci_open_dev(self.devid) bin_cmd_params = cmd_params['Advertising_Data_Length'].to_bytes(1, 'little') + \ cmd_params['Advertising_Data'] flt = hci_filter_new() hci_filter_set_ptype(flt, HCI_EVENT_PKT) hci_filter_set_event(flt, EVT_CMD_COMPLETE) hci_filter_set_opcode( flt, cmd_opcode_pack(OGF_LE_CTL, OCF_LE_SET_ADVERTISING_DATA)) dd.setsockopt(SOL_HCI, HCI_FILTER, flt) hci_send_cmd(dd, OGF_LE_CTL, OCF_LE_SET_ADVERTISING_DATA, bin_cmd_params) event_params = dd.recv(3+EVT_CMD_COMPLETE_SIZE+1)[3:] num_hci_cmd_pkts, cmd_opcode, status = struct.unpack('<BHB', event_params) event_params = { 'Num_HCI_Command_Packets': num_hci_cmd_pkts, 'Command_Opcode': cmd_opcode, 'Status': status } hci_close_dev(dd.fileno()) return event_params def le_set_scan_response_data(self, cmd_params={ 'Scan_Response_Data_Length': 0x1f, 'Scan_Response_Data': bytes(0x1f)}) -> dict: dd = hci_open_dev(self.devid) bin_cmd_params = cmd_params['Scan_Response_Data_Length'].to_bytes(1, 'little') + \ cmd_params['Scan_Response_Data'] flt = hci_filter_new() hci_filter_set_ptype(flt, HCI_EVENT_PKT) hci_filter_set_event(flt, EVT_CMD_COMPLETE) hci_filter_set_opcode( flt, cmd_opcode_pack(OGF_LE_CTL, OCF_LE_SET_SCAN_RESPONSE_DATA)) dd.setsockopt(SOL_HCI, HCI_FILTER, flt) hci_send_cmd(dd, OGF_LE_CTL, OCF_LE_SET_SCAN_RESPONSE_DATA, bin_cmd_params) event_params = dd.recv(3+EVT_CMD_COMPLETE_SIZE+1)[3:] num_hci_cmd_pkts, cmd_opcode, status = struct.unpack('<BHB', event_params) event_params = { 'Num_HCI_Command_Packets': num_hci_cmd_pkts, 'Command_Opcode': cmd_opcode, 'Status': status } hci_close_dev(dd.fileno()) return event_params def le_set_advertising_enable(self, cmd_params={'Advertising_Enable': 0x00}) -> dict: dd = hci_open_dev(self.devid) bin_cmd_params = cmd_params['Advertising_Enable'].to_bytes(1, 'little') flt = hci_filter_new() hci_filter_set_ptype(flt, HCI_EVENT_PKT) hci_filter_set_event(flt, EVT_CMD_COMPLETE) hci_filter_set_opcode( flt, cmd_opcode_pack(OGF_LE_CTL, OCF_LE_SET_ADVERTISING_ENABLE)) dd.setsockopt(SOL_HCI, HCI_FILTER, flt) hci_send_cmd(dd, OGF_LE_CTL, OCF_LE_SET_ADVERTISING_ENABLE, bin_cmd_params) event_params = dd.recv(3+EVT_CMD_COMPLETE_SIZE+1)[3:] num_hci_cmd_pkts, cmd_opcode, status = struct.unpack('<BHB', event_params) event_params = { 'Num_HCI_Command_Packets': num_hci_cmd_pkts, 'Command_Opcode': cmd_opcode, 'Status': status } hci_close_dev(dd.fileno()) return event_params def le_set_scan_enable(self, cmd_params:dict): ''' cmd_params -- { 'LE_Scan_Enable': int 0 or 1, 'Filter_Duplicates': int 0 or 1 } ''' dd = hci_open_dev(self.devid) bin_cmd_params = cmd_params['LE_Scan_Enable'].to_bytes(1, 'little') + \ cmd_params['Filter_Duplicates'].to_bytes(1, 'little') flt = hci_filter_new() hci_filter_set_ptype(flt, HCI_EVENT_PKT) hci_filter_set_event(flt, EVT_CMD_COMPLETE) hci_filter_set_opcode( flt, cmd_opcode_pack(OGF_LE_CTL, OCF_LE_SET_SCAN_ENABLE)) dd.setsockopt(SOL_HCI, HCI_FILTER, flt) hci_send_cmd(dd, OGF_LE_CTL, OCF_LE_SET_SCAN_ENABLE, bin_cmd_params) event_params = dd.recv(3+EVT_CMD_COMPLETE_SIZE+1)[3:] num_hci_cmd_pkts, cmd_opcode, status = struct.unpack('<BHB', event_params) event_params = { 'Num_HCI_Command_Packets': num_hci_cmd_pkts, 'Command_Opcode': cmd_opcode, 'Status': status } if event_params['Status'] != 0x00: raise RuntimeError( 'Status of HCI_LE_Set_Scan_Enable command: 0x%02x'%event_params['Status']) if not cmd_params['LE_Scan_Enable']: return event_params flt = hci_filter_new() hci_filter_set_ptype(flt, HCI_EVENT_PKT) hci_filter_set_event(flt, EVT_LE_META) dd.setsockopt(SOL_HCI, HCI_FILTER, flt) while True: event_params = dd.recv(3+HCI_MAX_EVENT_SIZE)[3:] if event_params[0] != SUBEVT_LE_ADVERTISING_REPORT: continue num_reports = event_params[1] if num_reports == 1: event_type, addr_type, addr = struct.unpack('<BB6s', event_params[2:10]) print('Event_Type:', event_type) print('Address_Type:', addr_type) print('Address:', addr) hci_close_dev(dd.fileno()) return event_params def le_create_connection(self, cmd_params:dict) -> dict: ''' cmd_params -- { 'LE_Scan_Interval': , 'LE_Scan_Window': , 'Initiator_Filter_Policy': , 'Peer_Address_Type': , 'Peer_Address': , 'Own_Address_Type': , 'Connection_Interval_Min': , 'Connection_Interval_Max': , 'Connection_Latency': , 'Supervision_Timeout': , 'Min_CE_Length': , 'Max_CE_Lengt': } ''' pass def hci_write_local_name(params:bytes, iface='hci0'): ogf = HCI_CTRL_BASEBAND_CMD_OGF ocf = 0x0013 params = ' '.join([hex(b) for b in params]) hcitool_cmd = gen_hcitool_cmd(ogf, ocf, params, iface) print(subprocess.getoutput(hcitool_cmd)) def hci_link_Key_request_reply(bd_addr:str, link_key:str, iface='hci0'): '''BLUETOOTH CORE SPECIFICATION Version 5.1 | Vol 2, Part E page 825, 7.1.10 Link Key Request Reply command''' ogf = LINK_CTRL_CMD_OGF ocf = 0x000B # HCI command parameter using litten-endian bd_addr = ' '.join(['0x' + e for e in bd_addr.split(':')[::-1]]) print(bd_addr) link_key = ' '.join([hex(b) for b in bytes.fromhex(link_key)]) print(link_key) params = bd_addr + ' ' + link_key hcitool_cmd = gen_hcitool_cmd(ogf, ocf, params, iface) print(subprocess.getoutput(hcitool_cmd)) def hci_read_stored_link_key(bd_addr='00:00:00:00:00:00', read_all_flag=0x01, iface='hci0'): '''BLUETOOTH CORE SPECIFICATION Version 5.1 | Vol 2, Part E page 966, 7.3.8 Read Stored Link Key command''' ogf = HCI_CTRL_BASEBAND_CMD_OGF ocf = 0x000D bd_addr = ' '.join(['0x' + e for e in bd_addr.split(':')[::-1]]) read_all_flag = hex(read_all_flag) params = ' '.join([bd_addr, read_all_flag]) hcitool_cmd = gen_hcitool_cmd(ogf, ocf, params, iface) print(subprocess.getoutput(hcitool_cmd)) def hci_write_stored_link_key(bd_addrs: list, link_keys:list, iface='hci0'): '''BLUETOOTH CORE SPECIFICATION Version 5.1 | Vol 2, Part E page 968, 7.3.9 Write Stored Link Key command.''' ogf = HCI_CTRL_BASEBAND_CMD_OGF ocf = 0x0011 if (len(bd_addrs) != len(link_keys)): print("[ERROR] BD_ADDRs and Link Keys is not one-to-one correspondence.") return False num_keys_to_write = len(link_keys) temp = '' for bd_addr in bd_addrs: temp += ' '.join( ['0x' + e for e in bd_addr.split(':')[::-1]] ) + ' ' bd_addrs = temp print(bd_addrs) temp = '' for link_key in link_keys: temp += ' '.join( [hex(b) for b in bytes.fromhex(link_key)] ) + ' ' link_keys = temp print(link_keys) params = hex(num_keys_to_write) + ' ' + bd_addrs + ' ' \ + link_keys hcitool_cmd = gen_hcitool_cmd(ogf, ocf, params, iface) print(subprocess.getoutput(hcitool_cmd)) def hci_delete_stored_link_key(bd_addr='00:00:00:00:00:00', del_all_flag=0x01, iface='hci0'): "BLUETOOTH CORE SPECIFICATION Version 5.1 | Vol 2, Part E page 970, 7.3.10 Delete Stored Link Key command" ogf = HCI_CTRL_BASEBAND_CMD_OGF ocf = 0x0012 bd_addr = ' '.join(['0x' + e for e in bd_addr.split(':')[::-1]]) del_all_flag = hex(del_all_flag) params = ' '.join([bd_addr, del_all_flag]) hcitool_cmd = gen_hcitool_cmd(ogf, ocf, params) print(subprocess.getoutput(hcitool_cmd)) def hci_write_simple_pairing_mode(simple_pairing_mode=0x00, iface='hci0'): '''7.3.59 Write Simple Pairing Mode command''' ogf = HCI_CTRL_BASEBAND_CMD_OGF ocf = 0x0056 simple_pairing_mode = hex(simple_pairing_mode) params = simple_pairing_mode hcitool_cmd = gen_hcitool_cmd(ogf, ocf, params) print(subprocess.getoutput(hcitool_cmd)) def gen_hcitool_cmd(ogf:int, ocf:int, params:str, iface='hci0') -> str: '''构造执行任意 HCI command 的 hcitool 命令。''' cmd_args = ['hcitool', '-i', iface, 'cmd', hex(ogf), hex(ocf), params] cmd = ' '.join(cmd_args) #print('[DEBUG]', cmd) return cmd class __Test: @classmethod def scan_undiscoverable_dev(cls): import multiprocessing #hci_read_page_timeout() #hci_write_page_timeout(0x0200) # 0x0500 较稳定 #hci_read_page_timeout() # p1 = multiprocessing.Process(target=job,args=(1,2)) # range(, ) #hci_create_connection('3C:28:6D:E0:58:F7') for i in range(0x000000, 0x100000): addr = '3c:28:6d:'+':'.join('%02x'%b for b in i.to_bytes(3, 'big', signed=False)) print(addr) #print('HCI connect', addr) #status, bdaddr = hci_create_connection(addr) #hci_create_connection(addr) time.sleep(0.5) # if status == 0: # print(status, bdaddr) @classmethod def pp_le_scanner_addr(cls): hci = HCI('hci0') hci.le_set_advertising_parameters() bytes.fromhex('020106020aeb0303ff00')+bytes(0x1f-10) hci.le_set_advertising_data({ 'Advertising_Data_Length': 0x12, 'Advertising_Data': bytes.fromhex('020106020aeb0303ff000709424c45435446')+bytes(0x1f-0x12) }) hci.le_set_scan_response_data({ 'Scan_Response_Data_Length': 0x0a, 'Scan_Response_Data': bytes.fromhex('020106020aeb0303ff00')+bytes(0x1f-0x0a) }) hci.le_set_advertising_enable({'Advertising_Enable': 0x01}) dd = hci_open_dev(0) flt = hci_filter_new() hci_filter_set_ptype(flt, HCI_EVENT_PKT) hci_filter_set_event(flt, EVT_LE_META) dd.setsockopt(SOL_HCI, HCI_FILTER, flt) while True: event_params = dd.recv(3+SUBEVT_LE_SCAN_REQUEST_RECEIVED_SIZE)[3:] if event_params[0] != SUBEVT_LE_SCAN_REQUEST_RECEIVED: continue subevt_code, adv_handle, scanner_addr_type, scanner_addr = struct.unpack('<BBB6s', event_params) event_params = { 'Subevent_Code': subevt_code, 'Advertising_Handle': adv_handle, 'Scanner_Address_Type': scanner_addr_type, 'Scanner_Address': scanner_addr } print(event_params) hci_close_dev(dd.fileno()) if __name__ == '__main__': # print(EVT_READ_REMOTE_VERSION_COMPLETE) # print(EVT_READ_REMOTE_VERSION_COMPLETE_SIZE) # __Test.scan_undiscoverable_dev() __Test.pp_le_scanner_addr()