forked from auracaster/bumble_mirror
more HCI commands
This commit is contained in:
@@ -570,16 +570,16 @@ class DeviceListener(Device.Listener, Connection.Listener):
|
|||||||
def on_connection_data_length_change(self):
|
def on_connection_data_length_change(self):
|
||||||
self.app.append_to_output(f'connection data length change: {self.app.connected_peer.connection.data_length}')
|
self.app.append_to_output(f'connection data length change: {self.app.connected_peer.connection.data_length}')
|
||||||
|
|
||||||
def on_advertisement(self, address, ad_data, rssi, connectable):
|
def on_advertisement(self, advertisement):
|
||||||
entry_key = f'{address}/{address.address_type}'
|
entry_key = f'{advertisement.address}/{advertisement.address.address_type}'
|
||||||
entry = self.scan_results.get(entry_key)
|
entry = self.scan_results.get(entry_key)
|
||||||
if entry:
|
if entry:
|
||||||
entry.ad_data = ad_data
|
entry.ad_data = advertisement.data
|
||||||
entry.rssi = rssi
|
entry.rssi = advertisement.rssi
|
||||||
entry.connectable = connectable
|
entry.connectable = advertisement.connectable
|
||||||
else:
|
else:
|
||||||
self.app.add_known_address(str(address))
|
self.app.add_known_address(str(advertisement.address))
|
||||||
self.scan_results[entry_key] = ScanResult(address, address.address_type, ad_data, rssi, connectable)
|
self.scan_results[entry_key] = ScanResult(advertisement.address, advertisement.address.address_type, advertisement.data, advertisement.rssi, advertisement.is_connectable)
|
||||||
|
|
||||||
self.app.show_scan_results(self.scan_results)
|
self.app.show_scan_results(self.scan_results)
|
||||||
|
|
||||||
|
|||||||
14
apps/scan.py
14
apps/scan.py
@@ -78,14 +78,14 @@ class AdvertisementPrinter:
|
|||||||
separator = '\n '
|
separator = '\n '
|
||||||
print(f'>>> {color(address, address_color)} [{color(address_type_string, type_color)}]{address_qualifier}{resolution_qualifier}:{separator}RSSI:{rssi:4} {rssi_bar}{separator}{ad_data.to_string(separator)}\n')
|
print(f'>>> {color(address, address_color)} [{color(address_type_string, type_color)}]{address_qualifier}{resolution_qualifier}:{separator}RSSI:{rssi:4} {rssi_bar}{separator}{ad_data.to_string(separator)}\n')
|
||||||
|
|
||||||
def on_advertisement(self, address, ad_data, rssi, connectable):
|
def on_advertisement(self, advertisement):
|
||||||
address_color = 'yellow' if connectable else 'red'
|
address_color = 'yellow' if advertisement.is_connectable else 'red'
|
||||||
self.print_advertisement(address, address_color, ad_data, rssi)
|
self.print_advertisement(advertisement.address, address_color, advertisement.data, advertisement.rssi)
|
||||||
|
|
||||||
def on_advertising_report(self, address, ad_data, rssi, event_type):
|
def on_advertising_report(self, report):
|
||||||
print(f'{color("EVENT", "green")}: {HCI_LE_Advertising_Report_Event.event_type_name(event_type)}')
|
print(f'{color("EVENT", "green")}: {HCI_LE_Advertising_Report_Event.event_type_name(report.event_type)}')
|
||||||
ad_data = AdvertisingData.from_bytes(ad_data)
|
data = AdvertisingData.from_bytes(report.data)
|
||||||
self.print_advertisement(address, 'yellow', ad_data, rssi)
|
self.print_advertisement(report.address, 'yellow', data, report.rssi)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|||||||
163
bumble/device.py
163
bumble/device.py
@@ -61,29 +61,134 @@ DEVICE_MAX_SCAN_WINDOW = 10240
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class Advertisement:
|
||||||
|
TX_POWER_NOT_AVAILABLE = HCI_LE_Extended_Advertising_Report_Event.TX_POWER_INFORMATION_NOT_AVAILABLE
|
||||||
|
RSSI_NOT_AVAILABLE = HCI_LE_Extended_Advertising_Report_Event.RSSI_NOT_AVAILABLE
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_advertising_report(cls, report):
|
||||||
|
if isinstance(report, HCI_LE_Advertising_Report_Event.Report):
|
||||||
|
return LegacyAdvertisement.from_advertising_report(report)
|
||||||
|
elif isinstance(report, HCI_LE_Extended_Advertising_Report_Event.Report):
|
||||||
|
return ExtendedAdvertisement.from_advertising_report(report)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
address,
|
||||||
|
rssi = HCI_LE_Extended_Advertising_Report_Event.RSSI_NOT_AVAILABLE,
|
||||||
|
is_legacy = False,
|
||||||
|
is_anonymous = False,
|
||||||
|
is_connectable = False,
|
||||||
|
is_directed = False,
|
||||||
|
is_scannable = False,
|
||||||
|
is_scan_response = False,
|
||||||
|
primary_phy = 0,
|
||||||
|
secondary_phy = 0,
|
||||||
|
tx_power = HCI_LE_Extended_Advertising_Report_Event.TX_POWER_INFORMATION_NOT_AVAILABLE,
|
||||||
|
sid = 0,
|
||||||
|
data = b''
|
||||||
|
):
|
||||||
|
self.address = address
|
||||||
|
self.rssi = rssi
|
||||||
|
self.is_legacy = is_legacy
|
||||||
|
self.is_anonymous = is_anonymous
|
||||||
|
self.is_connectable = is_connectable
|
||||||
|
self.is_directed = is_directed
|
||||||
|
self.is_scannable = is_scannable
|
||||||
|
self.is_scan_response = is_scan_response
|
||||||
|
self.primary_phy = primary_phy
|
||||||
|
self.secondary_phy = secondary_phy
|
||||||
|
self.tx_power = tx_power
|
||||||
|
self.sid = sid
|
||||||
|
self.data = AdvertisingData.from_bytes(data)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class LegacyAdvertisement(Advertisement):
|
||||||
|
@classmethod
|
||||||
|
def from_advertising_report(cls, report):
|
||||||
|
return cls(
|
||||||
|
address = report.address,
|
||||||
|
rssi = report.rssi,
|
||||||
|
is_legacy = True,
|
||||||
|
is_connectable = report.event_type in {
|
||||||
|
HCI_LE_Advertising_Report_Event.ADV_IND,
|
||||||
|
HCI_LE_Advertising_Report_Event.ADV_DIRECT_IND
|
||||||
|
},
|
||||||
|
is_directed = report.event_type == HCI_LE_Advertising_Report_Event.ADV_DIRECT_IND,
|
||||||
|
is_scannable = report.event_type in {
|
||||||
|
HCI_LE_Advertising_Report_Event.ADV_IND,
|
||||||
|
HCI_LE_Advertising_Report_Event.ADV_SCAN_IND
|
||||||
|
},
|
||||||
|
is_scan_response = report.event_type == HCI_LE_Advertising_Report_Event.SCAN_RSP,
|
||||||
|
data = report.data
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class ExtendedAdvertisement(Advertisement):
|
||||||
|
@classmethod
|
||||||
|
def from_advertising_report(cls, report):
|
||||||
|
return cls(
|
||||||
|
address = report.address,
|
||||||
|
rssi = report.rssi,
|
||||||
|
is_legacy = report.event_type & (1 << HCI_LE_Extended_Advertising_Report_Event.LEGACY_ADVERTISING_PDU_USED) != 0,
|
||||||
|
is_anonymous = report.address.address_type == HCI_LE_Extended_Advertising_Report_Event.ANONYMOUS_ADDRESS_TYPE,
|
||||||
|
is_connectable = report.event_type & (1 << HCI_LE_Extended_Advertising_Report_Event.CONNECTABLE_ADVERTISING) != 0,
|
||||||
|
is_directed = report.event_type & (1 << HCI_LE_Extended_Advertising_Report_Event.DIRECTED_ADVERTISING) != 0,
|
||||||
|
is_scannable = report.event_type & (1 << HCI_LE_Extended_Advertising_Report_Event.SCANNABLE_ADVERTISING) != 0,
|
||||||
|
is_scan_response = report.event_type & (1 << HCI_LE_Extended_Advertising_Report_Event.SCAN_RESPONSE) != 0,
|
||||||
|
primary_phy = report.primary_phy,
|
||||||
|
secondary_phy = report.secondary_phy,
|
||||||
|
tx_power = report.tx_power,
|
||||||
|
sid = report.advertising_sid,
|
||||||
|
data = report.data
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class AdvertisementDataAccumulator:
|
class AdvertisementDataAccumulator:
|
||||||
def __init__(self):
|
def __init__(self, passive=False):
|
||||||
self.advertising_data = AdvertisingData()
|
self.passive = passive
|
||||||
self.last_advertisement_type = None
|
self.last_event_type = None
|
||||||
self.connectable = False
|
self.advertisement = None
|
||||||
self.flushable = False
|
self.data = b''
|
||||||
|
|
||||||
def update(self, data, advertisement_type):
|
def update(self, report):
|
||||||
if advertisement_type == HCI_LE_Advertising_Report_Event.SCAN_RSP:
|
if isinstance(report, HCI_LE_Advertising_Report_Event.Report):
|
||||||
if self.last_advertisement_type != HCI_LE_Advertising_Report_Event.SCAN_RSP:
|
if report.event_type == HCI_LE_Advertising_Report_Event.SCAN_RSP:
|
||||||
self.advertising_data.append(data)
|
if self.last_event_type in {
|
||||||
self.flushable = True
|
HCI_LE_Advertising_Report_Event.ADV_IND,
|
||||||
|
HCI_LE_Advertising_Report_Event.ADV_SCAN_IND
|
||||||
|
}:
|
||||||
|
# This is the response to a scannable advertisement
|
||||||
|
self.advertisement = Advertisement.from_advertising_report(report)
|
||||||
|
self.advertisement.data = AdvertisingData.from_bytes(self.data + report.data)
|
||||||
|
self.advertisement.is_connectable = (self.last_event_type == HCI_LE_Advertising_Report_Event.ADV_IND)
|
||||||
else:
|
else:
|
||||||
self.advertising_data = AdvertisingData.from_bytes(data)
|
# Unexpected scan response
|
||||||
self.flushable = self.last_advertisement_type != HCI_LE_Advertising_Report_Event.SCAN_RSP
|
self.advertisement = None
|
||||||
|
|
||||||
if advertisement_type == HCI_LE_Advertising_Report_Event.ADV_IND or advertisement_type == HCI_LE_Advertising_Report_Event.ADV_DIRECT_IND:
|
# Reset the data
|
||||||
self.connectable = True
|
self.data = b''
|
||||||
elif advertisement_type == HCI_LE_Advertising_Report_Event.ADV_SCAN_IND or advertisement_type == HCI_LE_Advertising_Report_Event.ADV_NONCONN_IND:
|
else:
|
||||||
self.connectable = False
|
self.data = report.data
|
||||||
|
|
||||||
self.last_advertisement_type = advertisement_type
|
if self.passive or report.event_type in {
|
||||||
|
HCI_LE_Advertising_Report_Event.ADV_DIRECT_IND,
|
||||||
|
HCI_LE_Advertising_Report_Event.ADV_NONCONN_IND
|
||||||
|
} or self.last_event_type not in {
|
||||||
|
None,
|
||||||
|
HCI_LE_Advertising_Report_Event.SCAN_RSP
|
||||||
|
}:
|
||||||
|
# Don't wait for a scan response
|
||||||
|
self.advertisement = Advertisement.from_advertising_report(report)
|
||||||
|
else:
|
||||||
|
# Wait for a scan response
|
||||||
|
self.advertisement = None
|
||||||
|
|
||||||
|
self.last_event_type = report.event_type
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -403,7 +508,7 @@ class Device(CompositeEventEmitter):
|
|||||||
|
|
||||||
@composite_listener
|
@composite_listener
|
||||||
class Listener:
|
class Listener:
|
||||||
def on_advertisement(self, address, data, rssi, advertisement_type):
|
def on_advertisement(self, advertisement):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def on_inquiry_result(self, address, class_of_device, data, rssi):
|
def on_inquiry_result(self, address, class_of_device, data, rssi):
|
||||||
@@ -454,6 +559,7 @@ class Device(CompositeEventEmitter):
|
|||||||
[l2cap.L2CAP_Information_Request.EXTENDED_FEATURE_FIXED_CHANNELS])
|
[l2cap.L2CAP_Information_Request.EXTENDED_FEATURE_FIXED_CHANNELS])
|
||||||
self.advertisement_data = {}
|
self.advertisement_data = {}
|
||||||
self.scanning = False
|
self.scanning = False
|
||||||
|
self.scanning_is_passive = False
|
||||||
self.discovering = False
|
self.discovering = False
|
||||||
self.connecting = False
|
self.connecting = False
|
||||||
self.disconnecting = False
|
self.disconnecting = False
|
||||||
@@ -758,6 +864,7 @@ class Device(CompositeEventEmitter):
|
|||||||
))
|
))
|
||||||
|
|
||||||
self.scanning = True
|
self.scanning = True
|
||||||
|
self.scanning_is_passive = not active
|
||||||
|
|
||||||
async def stop_scanning(self):
|
async def stop_scanning(self):
|
||||||
await self.send_command(HCI_LE_Set_Scan_Enable_Command(
|
await self.send_command(HCI_LE_Set_Scan_Enable_Command(
|
||||||
@@ -771,19 +878,13 @@ class Device(CompositeEventEmitter):
|
|||||||
return self.scanning
|
return self.scanning
|
||||||
|
|
||||||
@host_event_handler
|
@host_event_handler
|
||||||
def on_advertising_report(self, address, data, rssi, advertisement_type):
|
def on_advertising_report(self, report):
|
||||||
if not (accumulator := self.advertisement_data.get(address)):
|
if not (accumulator := self.advertisement_data.get(report.address)):
|
||||||
accumulator = AdvertisementDataAccumulator()
|
accumulator = AdvertisementDataAccumulator(passive=self.scanning_is_passive)
|
||||||
self.advertisement_data[address] = accumulator
|
self.advertisement_data[report.address] = accumulator
|
||||||
accumulator.update(data, advertisement_type)
|
accumulator.update(report)
|
||||||
if accumulator.flushable:
|
if accumulator.advertisement is not None:
|
||||||
self.emit(
|
self.emit('advertisement', accumulator.advertisement)
|
||||||
'advertisement',
|
|
||||||
address,
|
|
||||||
accumulator.advertising_data,
|
|
||||||
rssi,
|
|
||||||
accumulator.connectable
|
|
||||||
)
|
|
||||||
|
|
||||||
async def start_discovery(self):
|
async def start_discovery(self):
|
||||||
await self.host.send_command(HCI_Write_Inquiry_Mode_Command(inquiry_mode=HCI_EXTENDED_INQUIRY_MODE))
|
await self.host.send_command(HCI_Write_Inquiry_Mode_Command(inquiry_mode=HCI_EXTENDED_INQUIRY_MODE))
|
||||||
|
|||||||
412
bumble/hci.py
412
bumble/hci.py
@@ -1373,11 +1373,14 @@ class HCI_Error(ProtocolError):
|
|||||||
super().__init__(error_code, 'hci', HCI_Constant.error_name(error_code))
|
super().__init__(error_code, 'hci', HCI_Constant.error_name(error_code))
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
class HCI_StatusError(ProtocolError):
|
class HCI_StatusError(ProtocolError):
|
||||||
def __init__(self, response):
|
def __init__(self, response):
|
||||||
super().__init__(response.status,
|
super().__init__(
|
||||||
|
response.status,
|
||||||
error_namespace=HCI_Command.command_name(response.command_opcode),
|
error_namespace=HCI_Command.command_name(response.command_opcode),
|
||||||
error_name=HCI_Constant.status_name(response.status))
|
error_name=HCI_Constant.status_name(response.status)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -1402,7 +1405,7 @@ class HCI_Object:
|
|||||||
def dict_from_bytes(data, offset, fields):
|
def dict_from_bytes(data, offset, fields):
|
||||||
result = collections.OrderedDict()
|
result = collections.OrderedDict()
|
||||||
for (field_name, field_type) in fields:
|
for (field_name, field_type) in fields:
|
||||||
# The field_type may be a dictionnary with a mapper, parser, and/or size
|
# The field_type may be a dictionary with a mapper, parser, and/or size
|
||||||
if type(field_type) is dict:
|
if type(field_type) is dict:
|
||||||
if 'size' in field_type:
|
if 'size' in field_type:
|
||||||
field_type = field_type['size']
|
field_type = field_type['size']
|
||||||
@@ -1523,9 +1526,9 @@ class HCI_Object:
|
|||||||
|
|
||||||
return bytes(result)
|
return bytes(result)
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def from_bytes(data, offset, fields):
|
def from_bytes(cls, data, offset, fields):
|
||||||
return HCI_Object(fields, **HCI_Object.dict_from_bytes(data, offset, fields))
|
return cls(fields, **cls.dict_from_bytes(data, offset, fields))
|
||||||
|
|
||||||
def to_bytes(self):
|
def to_bytes(self):
|
||||||
return HCI_Object.dict_to_bytes(self.__dict__, self.fields)
|
return HCI_Object.dict_to_bytes(self.__dict__, self.fields)
|
||||||
@@ -3024,6 +3027,24 @@ class HCI_LE_Remote_Connection_Parameter_Request_Negative_Reply_Command(HCI_Comm
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
@HCI_Command.command(
|
||||||
|
fields=[
|
||||||
|
('connection_handle', 2),
|
||||||
|
('tx_octets', 2),
|
||||||
|
('tx_time', 2),
|
||||||
|
],
|
||||||
|
return_parameters_fields=[
|
||||||
|
('status', STATUS_SPEC),
|
||||||
|
('connection_handle', 2)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
class HCI_LE_Set_Data_Length_Command(HCI_Command):
|
||||||
|
'''
|
||||||
|
See Bluetooth spec @ 7.8.33 LE Set Data Length Command
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@HCI_Command.command([
|
@HCI_Command.command([
|
||||||
('suggested_max_tx_octets', 2),
|
('suggested_max_tx_octets', 2),
|
||||||
@@ -3102,6 +3123,228 @@ class HCI_LE_Read_Maximum_Data_Length_Command(HCI_Command):
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
@HCI_Command.command(
|
||||||
|
fields=[
|
||||||
|
('connection_handle', 2)
|
||||||
|
],
|
||||||
|
return_parameters_fields=[
|
||||||
|
('status', STATUS_SPEC),
|
||||||
|
('connection_handle', 2),
|
||||||
|
('tx_phy', {'size': 1, 'mapper': HCI_Constant.le_phy_name}),
|
||||||
|
('rx_phy', {'size': 1, 'mapper': HCI_Constant.le_phy_name})
|
||||||
|
])
|
||||||
|
class HCI_LE_Read_PHY_Command(HCI_Command):
|
||||||
|
'''
|
||||||
|
See Bluetooth spec @ 7.8.47 LE Read PHY Command
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
@HCI_Command.command([
|
||||||
|
('connection_handle', 2),
|
||||||
|
('all_phys', 1),
|
||||||
|
('tx_phys', 1),
|
||||||
|
('rx_phys', 1),
|
||||||
|
('phy_options', 2)
|
||||||
|
])
|
||||||
|
class HCI_LE_Set_PHY_Command(HCI_Command):
|
||||||
|
'''
|
||||||
|
See Bluetooth spec @ 7.8.49 LE Set PHY Command
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
@HCI_Command.command([
|
||||||
|
('advertising_handle', 1),
|
||||||
|
('random_address', lambda data, offset: Address.parse_address_with_type(data, offset, Address.RANDOM_DEVICE_ADDRESS))
|
||||||
|
])
|
||||||
|
class HCI_LE_Set_Advertising_Set_Random_Address_Command(HCI_Command):
|
||||||
|
'''
|
||||||
|
See Bluetooth spec @ 7.8.52 LE Set Advertising Set Random Address Command
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
@HCI_Command.command(
|
||||||
|
fields=[
|
||||||
|
('advertising_handle', 1),
|
||||||
|
('advertising_event_properties', {'size': 2, 'mapper': lambda x: HCI_LE_Set_Extended_Advertising_Parameters_Command.advertising_properties_string(x)}),
|
||||||
|
('primary_advertising_interval_min', 3),
|
||||||
|
('primary_advertising_interval_max', 3),
|
||||||
|
('primary_advertising_channel_map', {'size': 1, 'mapper': lambda x: HCI_LE_Set_Extended_Advertising_Parameters_Command.channel_map_string(x)}),
|
||||||
|
('own_address_type', Address.ADDRESS_TYPE_SPEC),
|
||||||
|
('peer_address_type', Address.ADDRESS_TYPE_SPEC),
|
||||||
|
('peer_address', Address.parse_address_preceded_by_type),
|
||||||
|
('advertising_filter_policy', 1),
|
||||||
|
('advertising_tx_power', 1),
|
||||||
|
('primary_advertising_phy', {'size': 1, 'mapper': HCI_Constant.le_phy_name}),
|
||||||
|
('secondary_advertising_max_skip', 1),
|
||||||
|
('secondary_advertising_phy', {'size': 1, 'mapper': HCI_Constant.le_phy_name}),
|
||||||
|
('advertising_sid', 1),
|
||||||
|
('scan_request_notification_enable', 1)
|
||||||
|
],
|
||||||
|
return_parameters_fields=[
|
||||||
|
('status', STATUS_SPEC),
|
||||||
|
('selected_tx__power', 1)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
class HCI_LE_Set_Extended_Advertising_Parameters_Command(HCI_Command):
|
||||||
|
'''
|
||||||
|
See Bluetooth spec @ 7.8.53 LE Set Extended Advertising Parameters Command
|
||||||
|
'''
|
||||||
|
|
||||||
|
CONNECTABLE_ADVERTISING = 0
|
||||||
|
SCANNABLE_ADVERTISING = 1
|
||||||
|
DIRECTED_ADVERTISING = 2
|
||||||
|
HIGH_DUTY_CYCLE_DIRECTED_CONNECTABLE_ADVERTISING = 3
|
||||||
|
USE_LEGACY_ADVERTISING_PDUS = 4
|
||||||
|
ANONYMOUS_ADVERTISING = 5
|
||||||
|
INCLUDE_TX_POWER = 6
|
||||||
|
|
||||||
|
ADVERTISING_PROPERTIES_NAMES = (
|
||||||
|
'CONNECTABLE_ADVERTISING',
|
||||||
|
'SCANNABLE_ADVERTISING',
|
||||||
|
'DIRECTED_ADVERTISING',
|
||||||
|
'HIGH_DUTY_CYCLE_DIRECTED_CONNECTABLE_ADVERTISING',
|
||||||
|
'USE_LEGACY_ADVERTISING_PDUS',
|
||||||
|
'ANONYMOUS_ADVERTISING',
|
||||||
|
'INCLUDE_TX_POWER'
|
||||||
|
)
|
||||||
|
|
||||||
|
CHANNEL_37 = 0
|
||||||
|
CHANNEL_38 = 1
|
||||||
|
CHANNEL_39 = 2
|
||||||
|
|
||||||
|
CHANNEL_NAMES = ('37', '38', '39')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def advertising_properties_string(cls, properties):
|
||||||
|
return f'[{",".join(bit_flags_to_strings(properties, cls.ADVERTISING_PROPERTIES_NAMES))}]'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def channel_map_string(cls, channel_map):
|
||||||
|
return f'[{",".join(bit_flags_to_strings(channel_map, cls.CHANNEL_NAMES))}]'
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
@HCI_Command.command([
|
||||||
|
('advertising_handle', 1),
|
||||||
|
('operation', {'size': 1, 'mapper': lambda x: HCI_LE_Set_Extended_Advertising_Data_Command.operation_name(x)}),
|
||||||
|
('fragment_preference', 1),
|
||||||
|
('advertising_data', {
|
||||||
|
'parser': HCI_Object.parse_length_prefixed_bytes,
|
||||||
|
'serializer': functools.partial(HCI_Object.serialize_length_prefixed_bytes)
|
||||||
|
})
|
||||||
|
])
|
||||||
|
class HCI_LE_Set_Extended_Advertising_Data_Command(HCI_Command):
|
||||||
|
'''
|
||||||
|
See Bluetooth spec @ 7.8.54 LE Set Extended Advertising Data Command
|
||||||
|
'''
|
||||||
|
|
||||||
|
INTERMEDIATE_FRAGMENT = 0x00
|
||||||
|
FIRST_FRAGMENT = 0x01
|
||||||
|
LAST_FRAGMENT = 0x02
|
||||||
|
COMPLETE_DATA = 0x03
|
||||||
|
UNCHANGED_DATA = 0x04
|
||||||
|
|
||||||
|
OPERATION_NAMES = {
|
||||||
|
INTERMEDIATE_FRAGMENT: 'INTERMEDIATE_FRAGMENT',
|
||||||
|
FIRST_FRAGMENT: 'FIRST_FRAGMENT',
|
||||||
|
LAST_FRAGMENT: 'LAST_FRAGMENT',
|
||||||
|
COMPLETE_DATA: 'COMPLETE_DATA',
|
||||||
|
UNCHANGED_DATA: 'UNCHANGED_DATA'
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def operation_name(cls, operation):
|
||||||
|
return name_or_number(cls.OPERATION_NAMES, operation)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
@HCI_Command.command([
|
||||||
|
('advertising_handle', 1),
|
||||||
|
('operation', {'size': 1, 'mapper': lambda x: HCI_LE_Set_Extended_Advertising_Data_Command.operation_name(x)}),
|
||||||
|
('fragment_preference', 1),
|
||||||
|
('scan_response_data', {
|
||||||
|
'parser': HCI_Object.parse_length_prefixed_bytes,
|
||||||
|
'serializer': functools.partial(HCI_Object.serialize_length_prefixed_bytes)
|
||||||
|
})
|
||||||
|
])
|
||||||
|
class HCI_LE_Set_Extended_Scan_Response_Data_Command(HCI_Command):
|
||||||
|
'''
|
||||||
|
See Bluetooth spec @ 7.8.55 LE Set Extended Scan Response Data Command
|
||||||
|
'''
|
||||||
|
|
||||||
|
INTERMEDIATE_FRAGMENT = 0x00
|
||||||
|
FIRST_FRAGMENT = 0x01
|
||||||
|
LAST_FRAGMENT = 0x02
|
||||||
|
COMPLETE_DATA = 0x03
|
||||||
|
|
||||||
|
OPERATION_NAMES = {
|
||||||
|
INTERMEDIATE_FRAGMENT: 'INTERMEDIATE_FRAGMENT',
|
||||||
|
FIRST_FRAGMENT: 'FIRST_FRAGMENT',
|
||||||
|
LAST_FRAGMENT: 'LAST_FRAGMENT',
|
||||||
|
COMPLETE_DATA: 'COMPLETE_DATA'
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def operation_name(cls, operation):
|
||||||
|
return name_or_number(cls.OPERATION_NAMES, operation)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
@HCI_Command.command(fields=None)
|
||||||
|
class HCI_LE_Set_Extended_Advertising_Enable_Command(HCI_Command):
|
||||||
|
'''
|
||||||
|
See Bluetooth spec @ 7.8.56 LE Set Extended Advertising Enable Command
|
||||||
|
'''
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_parameters(cls, parameters):
|
||||||
|
enable = parameters[0]
|
||||||
|
num_sets = parameters[1]
|
||||||
|
advertising_handles = []
|
||||||
|
durations = []
|
||||||
|
max_extended_advertising_events = []
|
||||||
|
offset = 2
|
||||||
|
for _ in range(num_sets):
|
||||||
|
advertising_handles.append(parameters[offset])
|
||||||
|
durations.append(struct.unpack_from('<H', parameters, offset + 1)[0])
|
||||||
|
max_extended_advertising_events.append(parameters[offset + 3])
|
||||||
|
offset += 4
|
||||||
|
|
||||||
|
return cls(enable, advertising_handles, durations, max_extended_advertising_events)
|
||||||
|
|
||||||
|
def __init__(self, enable, advertising_handles, durations, max_extended_advertising_events):
|
||||||
|
super().__init__(HCI_LE_SET_EXTENDED_ADVERTISING_ENABLE_COMMAND)
|
||||||
|
self.enable = enable
|
||||||
|
self.advertising_handles = advertising_handles
|
||||||
|
self.durations = durations
|
||||||
|
self.max_extended_advertising_events = max_extended_advertising_events
|
||||||
|
|
||||||
|
self.parameters = bytes([enable, len(advertising_handles)]) + b''.join([
|
||||||
|
struct.pack(
|
||||||
|
'<BHB',
|
||||||
|
advertising_handles[i],
|
||||||
|
durations[i],
|
||||||
|
max_extended_advertising_events[i]
|
||||||
|
)
|
||||||
|
for i in range(len(advertising_handles))
|
||||||
|
])
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
fields = [('enable:', self.enable)]
|
||||||
|
for i in range(len(self.advertising_handles)):
|
||||||
|
fields.append((f'advertising_handle[{i}]: ', self.advertising_handles[i]))
|
||||||
|
fields.append((f'duration[{i}]: ', self.durations[i]))
|
||||||
|
fields.append((f'max_extended_advertising_events[{i}]:', self.max_extended_advertising_events[i]))
|
||||||
|
|
||||||
|
return color(self.name, 'green') + ':\n' + '\n'.join(
|
||||||
|
[color(field[0], 'cyan') + ' ' + str(field[1]) for field in fields]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@HCI_Command.command(return_parameters_fields=[
|
@HCI_Command.command(return_parameters_fields=[
|
||||||
('status', STATUS_SPEC),
|
('status', STATUS_SPEC),
|
||||||
@@ -3124,6 +3367,35 @@ class HCI_LE_Read_Number_Of_Supported_Advertising_Sets_Command(HCI_Command):
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
@HCI_Command.command([
|
||||||
|
('advertising_handle', 1)
|
||||||
|
])
|
||||||
|
class HCI_LE_Remove_Advertising_Set_Command(HCI_Command):
|
||||||
|
'''
|
||||||
|
See Bluetooth spec @ 7.8.59 LE Remove Advertising Set Command
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
@HCI_Command.command()
|
||||||
|
class HCI_LE_Clear_Advertising_Sets_Command(HCI_Command):
|
||||||
|
'''
|
||||||
|
See Bluetooth spec @ 7.8.60 LE Clear Advertising Sets Command
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
@HCI_Command.command([
|
||||||
|
('enable', 1),
|
||||||
|
('advertising_handle', 1)
|
||||||
|
])
|
||||||
|
class HCI_LE_Set_Periodic_Advertising_Enable_Command(HCI_Command):
|
||||||
|
'''
|
||||||
|
See Bluetooth spec @ 7.8.63 LE Set Periodic Advertising Enable Command
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@HCI_Command.command(fields=None)
|
@HCI_Command.command(fields=None)
|
||||||
class HCI_LE_Set_Extended_Scan_Parameters_Command(HCI_Command):
|
class HCI_LE_Set_Extended_Scan_Parameters_Command(HCI_Command):
|
||||||
@@ -3141,6 +3413,8 @@ class HCI_LE_Set_Extended_Scan_Parameters_Command(HCI_Command):
|
|||||||
LE_1M_PHY = 0x00
|
LE_1M_PHY = 0x00
|
||||||
LE_CODED_PHY = 0x02
|
LE_CODED_PHY = 0x02
|
||||||
|
|
||||||
|
SCANNING_PHY_NAMES = ['LE_1M_PHY', '', 'LE_CODED_PHY']
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_parameters(cls, parameters):
|
def from_parameters(cls, parameters):
|
||||||
own_address_type = parameters[0]
|
own_address_type = parameters[0]
|
||||||
@@ -3188,17 +3462,7 @@ class HCI_LE_Set_Extended_Scan_Parameters_Command(HCI_Command):
|
|||||||
self.parameters += struct.pack('<BHH', scan_types[i], scan_intervals[i], scan_windows[i])
|
self.parameters += struct.pack('<BHH', scan_types[i], scan_intervals[i], scan_windows[i])
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
scanning_phys_strs = []
|
scanning_phys_strs = bit_flags_to_strings(self.scanning_phys, self.SCANNING_PHY_NAMES)
|
||||||
|
|
||||||
for bit in range(8):
|
|
||||||
if self.scanning_phys & (1 << bit) != 0:
|
|
||||||
if bit == 0:
|
|
||||||
scanning_phys_strs.append('LE_1M_PHY')
|
|
||||||
elif bit == 2:
|
|
||||||
scanning_phys_strs.append('LE_CODED_PHY')
|
|
||||||
else:
|
|
||||||
scanning_phys_strs.append(f'0x{(1 << bit):02X}')
|
|
||||||
|
|
||||||
fields = [
|
fields = [
|
||||||
('own_address_type: ', Address.address_type_name(self.own_address_type)),
|
('own_address_type: ', Address.address_type_name(self.own_address_type)),
|
||||||
('scanning_filter_policy:', self.scanning_filter_policy),
|
('scanning_filter_policy:', self.scanning_filter_policy),
|
||||||
@@ -3227,6 +3491,17 @@ class HCI_LE_Set_Extended_Scan_Enable_Command(HCI_Command):
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
@HCI_Command.command([
|
||||||
|
('bit_number', 1),
|
||||||
|
('bit_value', 1)
|
||||||
|
])
|
||||||
|
class HCI_LE_Set_Host_Feature_Command(HCI_Command):
|
||||||
|
'''
|
||||||
|
See Bluetooth spec @ 7.8.115 LE Set Host Feature Command
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# HCI Events
|
# HCI Events
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -3440,7 +3715,8 @@ class HCI_LE_Advertising_Report_Event(HCI_LE_Meta_Event):
|
|||||||
SCAN_RSP: 'SCAN_RSP' # Scan Response
|
SCAN_RSP: 'SCAN_RSP' # Scan Response
|
||||||
}
|
}
|
||||||
|
|
||||||
REPORT_FIELDS = [
|
class Report(HCI_Object):
|
||||||
|
FIELDS = [
|
||||||
('event_type', 1),
|
('event_type', 1),
|
||||||
('address_type', Address.ADDRESS_TYPE_SPEC),
|
('address_type', Address.ADDRESS_TYPE_SPEC),
|
||||||
('address', Address.parse_address_preceded_by_type),
|
('address', Address.parse_address_preceded_by_type),
|
||||||
@@ -3448,6 +3724,17 @@ class HCI_LE_Advertising_Report_Event(HCI_LE_Meta_Event):
|
|||||||
('rssi', -1)
|
('rssi', -1)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_parameters(cls, parameters, offset):
|
||||||
|
return cls.from_bytes(parameters, offset, cls.FIELDS)
|
||||||
|
|
||||||
|
def to_string(self, prefix):
|
||||||
|
return super().to_string(prefix, {
|
||||||
|
'event_type': HCI_LE_Advertising_Report_Event.event_type_name,
|
||||||
|
'address_type': Address.address_type_name,
|
||||||
|
'data': lambda x: str(AdvertisingData.from_bytes(x))
|
||||||
|
})
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def event_type_name(cls, event_type):
|
def event_type_name(cls, event_type):
|
||||||
return name_or_number(cls.EVENT_TYPE_NAMES, event_type)
|
return name_or_number(cls.EVENT_TYPE_NAMES, event_type)
|
||||||
@@ -3458,7 +3745,7 @@ class HCI_LE_Advertising_Report_Event(HCI_LE_Meta_Event):
|
|||||||
reports = []
|
reports = []
|
||||||
offset = 2
|
offset = 2
|
||||||
for _ in range(num_reports):
|
for _ in range(num_reports):
|
||||||
report = HCI_Object.from_bytes(parameters, offset, cls.REPORT_FIELDS)
|
report = cls.Report.from_parameters(parameters, offset)
|
||||||
offset += 10 + len(report.data)
|
offset += 10 + len(report.data)
|
||||||
reports.append(report)
|
reports.append(report)
|
||||||
|
|
||||||
@@ -3473,11 +3760,7 @@ class HCI_LE_Advertising_Report_Event(HCI_LE_Meta_Event):
|
|||||||
super().__init__(self.subevent_code, parameters)
|
super().__init__(self.subevent_code, parameters)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
reports = '\n'.join([report.to_string(' ', {
|
reports = '\n'.join([report.to_string(' ') for report in self.reports])
|
||||||
'event_type': self.event_type_name,
|
|
||||||
'address_type': Address.address_type_name,
|
|
||||||
'data': lambda x: str(AdvertisingData.from_bytes(x))
|
|
||||||
}) for report in self.reports])
|
|
||||||
return f'{color(self.subevent_name(self.subevent_code), "magenta")}:\n{reports}'
|
return f'{color(self.subevent_name(self.subevent_code), "magenta")}:\n{reports}'
|
||||||
|
|
||||||
|
|
||||||
@@ -3590,7 +3873,7 @@ class HCI_LE_Extended_Advertising_Report_Event(HCI_LE_Meta_Event):
|
|||||||
'''
|
'''
|
||||||
subevent_code = HCI_LE_EXTENDED_ADVERTISING_REPORT_EVENT
|
subevent_code = HCI_LE_EXTENDED_ADVERTISING_REPORT_EVENT
|
||||||
|
|
||||||
# Event Types
|
# Event types flags
|
||||||
CONNECTABLE_ADVERTISING = 0
|
CONNECTABLE_ADVERTISING = 0
|
||||||
SCANNABLE_ADVERTISING = 1
|
SCANNABLE_ADVERTISING = 1
|
||||||
DIRECTED_ADVERTISING = 2
|
DIRECTED_ADVERTISING = 2
|
||||||
@@ -3601,13 +3884,13 @@ class HCI_LE_Extended_Advertising_Report_Event(HCI_LE_Meta_Event):
|
|||||||
DATA_INCOMPLETE_MORE_TO_COME = 0x01
|
DATA_INCOMPLETE_MORE_TO_COME = 0x01
|
||||||
DATA_INCOMPLETE_TRUNCATED_NO_MORE_TO_COME = 0x02
|
DATA_INCOMPLETE_TRUNCATED_NO_MORE_TO_COME = 0x02
|
||||||
|
|
||||||
EVENT_TYPE_NAMES = {
|
EVENT_TYPE_FLAG_NAMES = (
|
||||||
CONNECTABLE_ADVERTISING: 'CONNECTABLE_ADVERTISING',
|
'CONNECTABLE_ADVERTISING',
|
||||||
SCANNABLE_ADVERTISING: 'SCANNABLE_ADVERTISING',
|
'SCANNABLE_ADVERTISING',
|
||||||
DIRECTED_ADVERTISING: 'DIRECTED_ADVERTISING',
|
'DIRECTED_ADVERTISING',
|
||||||
SCAN_RESPONSE: 'SCAN_RESPONSE',
|
'SCAN_RESPONSE',
|
||||||
LEGACY_ADVERTISING_PDU_USED: 'LEGACY_ADVERTISING_PDU_USED'
|
'LEGACY_ADVERTISING_PDU_USED'
|
||||||
}
|
)
|
||||||
|
|
||||||
LEGACY_PDU_TYPE_MAP = {
|
LEGACY_PDU_TYPE_MAP = {
|
||||||
0b0011: HCI_LE_Advertising_Report_Event.ADV_IND,
|
0b0011: HCI_LE_Advertising_Report_Event.ADV_IND,
|
||||||
@@ -3618,7 +3901,14 @@ class HCI_LE_Extended_Advertising_Report_Event(HCI_LE_Meta_Event):
|
|||||||
0b1010: HCI_LE_Advertising_Report_Event.SCAN_RSP
|
0b1010: HCI_LE_Advertising_Report_Event.SCAN_RSP
|
||||||
}
|
}
|
||||||
|
|
||||||
REPORT_FIELDS = [
|
NO_ADI_FIELD_PROVIDED = 0xFF
|
||||||
|
TX_POWER_INFORMATION_NOT_AVAILABLE = 0x7F
|
||||||
|
RSSI_NOT_AVAILABLE = 0x7F
|
||||||
|
ANONYMOUS_ADDRESS_TYPE = 0xFF
|
||||||
|
UNRESOLVED_RESOLVABLE_ADDRESS_TYPE = 0xFE
|
||||||
|
|
||||||
|
class Report(HCI_Object):
|
||||||
|
FIELDS = [
|
||||||
('event_type', 2),
|
('event_type', 2),
|
||||||
('address_type', Address.ADDRESS_TYPE_SPEC),
|
('address_type', Address.ADDRESS_TYPE_SPEC),
|
||||||
('address', Address.parse_address_preceded_by_type),
|
('address', Address.parse_address_preceded_by_type),
|
||||||
@@ -3634,8 +3924,33 @@ class HCI_LE_Extended_Advertising_Report_Event(HCI_LE_Meta_Event):
|
|||||||
]
|
]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def event_type_name(cls, event_type):
|
def from_parameters(cls, parameters, offset):
|
||||||
return name_or_number(cls.EVENT_TYPE_NAMES, event_type)
|
return cls.from_bytes(parameters, offset, cls.FIELDS)
|
||||||
|
|
||||||
|
def to_string(self, prefix):
|
||||||
|
def event_type_string(event_type):
|
||||||
|
event_type_flags = bit_flags_to_strings(
|
||||||
|
event_type & 0x1F,
|
||||||
|
HCI_LE_Extended_Advertising_Report_Event.EVENT_TYPE_FLAG_NAMES,
|
||||||
|
)
|
||||||
|
event_type_flags.append(('COMPLETE', 'INCOMPLETE+', 'INCOMPLETE#', '?')[(event_type >> 5) & 3])
|
||||||
|
|
||||||
|
if event_type & (1 << HCI_LE_Extended_Advertising_Report_Event.LEGACY_ADVERTISING_PDU_USED):
|
||||||
|
legacy_pdu_type = HCI_LE_Extended_Advertising_Report_Event.LEGACY_PDU_TYPE_MAP.get(event_type & 0x0F)
|
||||||
|
if legacy_pdu_type is not None:
|
||||||
|
legacy_info_string = f'({HCI_LE_Advertising_Report_Event.event_type_name(legacy_pdu_type)})'
|
||||||
|
else:
|
||||||
|
legacy_info_string = ''
|
||||||
|
else:
|
||||||
|
legacy_info_string = ''
|
||||||
|
|
||||||
|
return f'0x{event_type:04X} [{",".join(event_type_flags)}]{legacy_info_string}'
|
||||||
|
|
||||||
|
return super().to_string(prefix, {
|
||||||
|
'event_type': event_type_string,
|
||||||
|
'address_type': Address.address_type_name,
|
||||||
|
'data': lambda x: str(AdvertisingData.from_bytes(x))
|
||||||
|
})
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_parameters(cls, parameters):
|
def from_parameters(cls, parameters):
|
||||||
@@ -3643,7 +3958,7 @@ class HCI_LE_Extended_Advertising_Report_Event(HCI_LE_Meta_Event):
|
|||||||
reports = []
|
reports = []
|
||||||
offset = 2
|
offset = 2
|
||||||
for _ in range(num_reports):
|
for _ in range(num_reports):
|
||||||
report = HCI_Object.from_bytes(parameters, offset, cls.REPORT_FIELDS)
|
report = cls.Report.from_parameters(parameters, offset)
|
||||||
offset += 24 + len(report.data)
|
offset += 24 + len(report.data)
|
||||||
reports.append(report)
|
reports.append(report)
|
||||||
|
|
||||||
@@ -3658,30 +3973,7 @@ class HCI_LE_Extended_Advertising_Report_Event(HCI_LE_Meta_Event):
|
|||||||
super().__init__(self.subevent_code, parameters)
|
super().__init__(self.subevent_code, parameters)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
def event_type_string(event_type):
|
reports = '\n'.join([report.to_string(' ') for report in self.reports])
|
||||||
event_type_flags = []
|
|
||||||
for bit in range(0, 5):
|
|
||||||
if event_type & (1 << bit):
|
|
||||||
event_type_flags.append(self.EVENT_TYPE_NAMES[bit])
|
|
||||||
event_type_flags.append(('COMPLETE', 'INCOMPLETE+', 'INCOMPLETE#', '?')[(event_type >> 5) & 3])
|
|
||||||
|
|
||||||
if event_type & (1 << self.LEGACY_ADVERTISING_PDU_USED):
|
|
||||||
legacy_pdu_type = self.LEGACY_PDU_TYPE_MAP.get(event_type & 0x0F)
|
|
||||||
if legacy_pdu_type is not None:
|
|
||||||
legacy_info_string = f'({HCI_LE_Advertising_Report_Event.event_type_name(legacy_pdu_type)})'
|
|
||||||
else:
|
|
||||||
legacy_info_string = ''
|
|
||||||
else:
|
|
||||||
legacy_info_string = ''
|
|
||||||
|
|
||||||
return f'0x{event_type:04X} [{",".join(event_type_flags)}]{legacy_info_string}'
|
|
||||||
|
|
||||||
reports = '\n'.join([report.to_string(' ', {
|
|
||||||
'event_type': event_type_string,
|
|
||||||
'address_type': Address.address_type_name,
|
|
||||||
'data': lambda x: str(AdvertisingData.from_bytes(x))
|
|
||||||
}) for report in self.reports])
|
|
||||||
|
|
||||||
return f'{color(self.subevent_name(self.subevent_code), "magenta")}:\n{reports}'
|
return f'{color(self.subevent_name(self.subevent_code), "magenta")}:\n{reports}'
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -467,13 +467,11 @@ class Host(EventEmitter):
|
|||||||
|
|
||||||
def on_hci_le_advertising_report_event(self, event):
|
def on_hci_le_advertising_report_event(self, event):
|
||||||
for report in event.reports:
|
for report in event.reports:
|
||||||
self.emit(
|
self.emit('advertising_report', report)
|
||||||
'advertising_report',
|
|
||||||
report.address,
|
def on_hci_le_extended_advertising_report_event(self, event):
|
||||||
report.data,
|
# TODO
|
||||||
report.rssi,
|
pass
|
||||||
report.event_type
|
|
||||||
)
|
|
||||||
|
|
||||||
def on_hci_le_remote_connection_parameter_request_event(self, event):
|
def on_hci_le_remote_connection_parameter_request_event(self, event):
|
||||||
if event.connection_handle not in self.connections:
|
if event.connection_handle not in self.connections:
|
||||||
|
|||||||
@@ -29,15 +29,15 @@ from bumble.transport import open_transport_or_link
|
|||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class ScannerListener(Device.Listener):
|
class ScannerListener(Device.Listener):
|
||||||
def on_advertisement(self, address, ad_data, rssi, connectable):
|
def on_advertisement(self, advertisement):
|
||||||
address_type_string = ('P', 'R', 'PI', 'RI')[address.address_type]
|
address_type_string = ('P', 'R', 'PI', 'RI')[advertisement.address.address_type]
|
||||||
address_color = 'yellow' if connectable else 'red'
|
address_color = 'yellow' if advertisement.is_connectable else 'red'
|
||||||
if address_type_string.startswith('P'):
|
if address_type_string.startswith('P'):
|
||||||
type_color = 'green'
|
type_color = 'green'
|
||||||
else:
|
else:
|
||||||
type_color = 'cyan'
|
type_color = 'cyan'
|
||||||
|
|
||||||
print(f'>>> {color(address, address_color)} [{color(address_type_string, type_color)}]: RSSI={rssi}, {ad_data}')
|
print(f'>>> {color(advertisement.address, address_color)} [{color(address_type_string, type_color)}]: RSSI={advertisement.rssi}, {advertisement.data}')
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -40,24 +40,24 @@ async def main():
|
|||||||
device = Device.with_hci('Bumble', 'F0:F1:F2:F3:F4:F5', hci_source, hci_sink)
|
device = Device.with_hci('Bumble', 'F0:F1:F2:F3:F4:F5', hci_source, hci_sink)
|
||||||
|
|
||||||
@device.on('advertisement')
|
@device.on('advertisement')
|
||||||
def _(address, ad_data, rssi, connectable):
|
def _(advertisement):
|
||||||
address_type_string = ('PUBLIC', 'RANDOM', 'PUBLIC_ID', 'RANDOM_ID')[address.address_type]
|
address_type_string = ('PUBLIC', 'RANDOM', 'PUBLIC_ID', 'RANDOM_ID')[advertisement.address.address_type]
|
||||||
address_color = 'yellow' if connectable else 'red'
|
address_color = 'yellow' if advertisement.is_connectable else 'red'
|
||||||
address_qualifier = ''
|
address_qualifier = ''
|
||||||
if address_type_string.startswith('P'):
|
if address_type_string.startswith('P'):
|
||||||
type_color = 'cyan'
|
type_color = 'cyan'
|
||||||
else:
|
else:
|
||||||
if address.is_static:
|
if advertisement.address.is_static:
|
||||||
type_color = 'green'
|
type_color = 'green'
|
||||||
address_qualifier = '(static)'
|
address_qualifier = '(static)'
|
||||||
elif address.is_resolvable:
|
elif advertisement.address.is_resolvable:
|
||||||
type_color = 'magenta'
|
type_color = 'magenta'
|
||||||
address_qualifier = '(resolvable)'
|
address_qualifier = '(resolvable)'
|
||||||
else:
|
else:
|
||||||
type_color = 'white'
|
type_color = 'white'
|
||||||
|
|
||||||
separator = '\n '
|
separator = '\n '
|
||||||
print(f'>>> {color(address, address_color)} [{color(address_type_string, type_color)}]{address_qualifier}:{separator}RSSI:{rssi}{separator}{ad_data.to_string(separator)}')
|
print(f'>>> {color(advertisement.address, address_color)} [{color(address_type_string, type_color)}]{address_qualifier}:{separator}RSSI:{advertisement.rssi}{separator}{advertisement.data.to_string(separator)}')
|
||||||
|
|
||||||
await device.power_on()
|
await device.power_on()
|
||||||
await device.start_scanning(filter_duplicates=filter_duplicates)
|
await device.start_scanning(filter_duplicates=filter_duplicates)
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ from bumble.transport import PacketParser
|
|||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class ScannerListener(Device.Listener):
|
class ScannerListener(Device.Listener):
|
||||||
def on_advertisement(self, address, ad_data, rssi, connectable):
|
def on_advertisement(self, advertisement):
|
||||||
address_type_string = ('P', 'R', 'PI', 'RI')[address.address_type]
|
address_type_string = ('P', 'R', 'PI', 'RI')[advertisement.address.address_type]
|
||||||
print(f'>>> {address} [{address_type_string}]: RSSI={rssi}, {ad_data}')
|
print(f'>>> {advertisement.address} [{address_type_string}]: RSSI={advertisement.rssi}, {advertisement.ad_data}')
|
||||||
|
|
||||||
|
|
||||||
class HciSource:
|
class HciSource:
|
||||||
|
|||||||
Reference in New Issue
Block a user