forked from auracaster/bumble_mirror
Dataclass-based HCI packets
This commit is contained in:
@@ -544,15 +544,14 @@ class Controller:
|
||||
acl_packet = HCI_AclDataPacket(connection.handle, 2, 0, len(data), data)
|
||||
self.send_hci_packet(acl_packet)
|
||||
|
||||
def on_link_advertising_data(self, sender_address, data):
|
||||
def on_link_advertising_data(self, sender_address: Address, data: bytes):
|
||||
# Ignore if we're not scanning
|
||||
if self.le_scan_enable == 0:
|
||||
return
|
||||
|
||||
# Send a scan report
|
||||
report = HCI_LE_Advertising_Report_Event.Report(
|
||||
HCI_LE_Advertising_Report_Event.Report.FIELDS,
|
||||
event_type=HCI_LE_Advertising_Report_Event.ADV_IND,
|
||||
event_type=HCI_LE_Advertising_Report_Event.EventType.ADV_IND,
|
||||
address_type=sender_address.address_type,
|
||||
address=sender_address,
|
||||
data=data,
|
||||
@@ -562,8 +561,7 @@ class Controller:
|
||||
|
||||
# Simulate a scan response
|
||||
report = HCI_LE_Advertising_Report_Event.Report(
|
||||
HCI_LE_Advertising_Report_Event.Report.FIELDS,
|
||||
event_type=HCI_LE_Advertising_Report_Event.SCAN_RSP,
|
||||
event_type=HCI_LE_Advertising_Report_Event.EventType.SCAN_RSP,
|
||||
address_type=sender_address.address_type,
|
||||
address=sender_address,
|
||||
data=data,
|
||||
|
||||
@@ -201,25 +201,35 @@ class Advertisement:
|
||||
# -----------------------------------------------------------------------------
|
||||
class LegacyAdvertisement(Advertisement):
|
||||
@classmethod
|
||||
def from_advertising_report(cls, report):
|
||||
def from_advertising_report(
|
||||
cls, report: hci.HCI_LE_Advertising_Report_Event.Report
|
||||
) -> Self:
|
||||
return cls(
|
||||
address=report.address,
|
||||
rssi=report.rssi,
|
||||
is_legacy=True,
|
||||
is_connectable=report.event_type
|
||||
in (
|
||||
hci.HCI_LE_Advertising_Report_Event.ADV_IND,
|
||||
hci.HCI_LE_Advertising_Report_Event.ADV_DIRECT_IND,
|
||||
is_connectable=(
|
||||
report.event_type
|
||||
in (
|
||||
hci.HCI_LE_Advertising_Report_Event.EventType.ADV_IND,
|
||||
hci.HCI_LE_Advertising_Report_Event.EventType.ADV_DIRECT_IND,
|
||||
)
|
||||
),
|
||||
is_directed=report.event_type
|
||||
== hci.HCI_LE_Advertising_Report_Event.ADV_DIRECT_IND,
|
||||
is_scannable=report.event_type
|
||||
in (
|
||||
hci.HCI_LE_Advertising_Report_Event.ADV_IND,
|
||||
hci.HCI_LE_Advertising_Report_Event.ADV_SCAN_IND,
|
||||
is_directed=(
|
||||
report.event_type
|
||||
== hci.HCI_LE_Advertising_Report_Event.EventType.ADV_DIRECT_IND
|
||||
),
|
||||
is_scannable=(
|
||||
report.event_type
|
||||
in (
|
||||
hci.HCI_LE_Advertising_Report_Event.EventType.ADV_IND,
|
||||
hci.HCI_LE_Advertising_Report_Event.EventType.ADV_SCAN_IND,
|
||||
)
|
||||
),
|
||||
is_scan_response=(
|
||||
report.event_type
|
||||
== hci.HCI_LE_Advertising_Report_Event.EventType.SCAN_RSP
|
||||
),
|
||||
is_scan_response=report.event_type
|
||||
== hci.HCI_LE_Advertising_Report_Event.SCAN_RSP,
|
||||
data_bytes=report.data,
|
||||
)
|
||||
|
||||
@@ -227,18 +237,20 @@ class LegacyAdvertisement(Advertisement):
|
||||
# -----------------------------------------------------------------------------
|
||||
class ExtendedAdvertisement(Advertisement):
|
||||
@classmethod
|
||||
def from_advertising_report(cls, report):
|
||||
def from_advertising_report(
|
||||
cls, report: hci.HCI_LE_Extended_Advertising_Report_Event.Report
|
||||
) -> Self:
|
||||
# fmt: off
|
||||
# pylint: disable=line-too-long
|
||||
return cls(
|
||||
address = report.address,
|
||||
rssi = report.rssi,
|
||||
is_legacy = report.event_type & (1 << hci.HCI_LE_Extended_Advertising_Report_Event.LEGACY_ADVERTISING_PDU_USED) != 0,
|
||||
is_legacy = (report.event_type & hci.HCI_LE_Extended_Advertising_Report_Event.EventType.LEGACY_ADVERTISING_PDU_USED) != 0,
|
||||
is_anonymous = report.address.address_type == hci.HCI_LE_Extended_Advertising_Report_Event.ANONYMOUS_ADDRESS_TYPE,
|
||||
is_connectable = report.event_type & (1 << hci.HCI_LE_Extended_Advertising_Report_Event.CONNECTABLE_ADVERTISING) != 0,
|
||||
is_directed = report.event_type & (1 << hci.HCI_LE_Extended_Advertising_Report_Event.DIRECTED_ADVERTISING) != 0,
|
||||
is_scannable = report.event_type & (1 << hci.HCI_LE_Extended_Advertising_Report_Event.SCANNABLE_ADVERTISING) != 0,
|
||||
is_scan_response = report.event_type & (1 << hci.HCI_LE_Extended_Advertising_Report_Event.SCAN_RESPONSE) != 0,
|
||||
is_connectable = (report.event_type & hci.HCI_LE_Extended_Advertising_Report_Event.EventType.CONNECTABLE_ADVERTISING) != 0,
|
||||
is_directed = (report.event_type & hci.HCI_LE_Extended_Advertising_Report_Event.EventType.DIRECTED_ADVERTISING) != 0,
|
||||
is_scannable = (report.event_type & hci.HCI_LE_Extended_Advertising_Report_Event.EventType.SCANNABLE_ADVERTISING) != 0,
|
||||
is_scan_response = (report.event_type & hci.HCI_LE_Extended_Advertising_Report_Event.EventType.SCAN_RESPONSE) != 0,
|
||||
is_complete = (report.event_type >> 5 & 3) == hci.HCI_LE_Extended_Advertising_Report_Event.DATA_COMPLETE,
|
||||
is_truncated = (report.event_type >> 5 & 3) == hci.HCI_LE_Extended_Advertising_Report_Event.DATA_INCOMPLETE_TRUNCATED_NO_MORE_TO_COME,
|
||||
primary_phy = report.primary_phy,
|
||||
|
||||
399
bumble/hci.py
399
bumble/hci.py
@@ -18,19 +18,19 @@
|
||||
from __future__ import annotations
|
||||
import collections
|
||||
import dataclasses
|
||||
from dataclasses import field
|
||||
import enum
|
||||
import functools
|
||||
import logging
|
||||
import secrets
|
||||
import struct
|
||||
from collections.abc import Sequence
|
||||
from typing import Any, Callable, Iterable, Optional, Union, TypeVar, ClassVar
|
||||
from typing import Any, Callable, Iterable, Optional, Union, TypeVar, ClassVar, cast
|
||||
from typing_extensions import Self
|
||||
|
||||
from bumble import crypto
|
||||
from bumble.colors import color
|
||||
from bumble.core import (
|
||||
AdvertisingData,
|
||||
DeviceClass,
|
||||
InvalidArgumentError,
|
||||
InvalidPacketError,
|
||||
@@ -1744,9 +1744,11 @@ class HCI_Object:
|
||||
|
||||
raise InvalidArgumentError(f'unknown field type {field_type}')
|
||||
|
||||
@staticmethod
|
||||
def dict_from_bytes(data, offset, fields):
|
||||
result = collections.OrderedDict()
|
||||
@classmethod
|
||||
def dict_and_offset_from_bytes(
|
||||
cls, data: bytes, offset: int, fields: Fields
|
||||
) -> tuple[int, collections.OrderedDict[str, Any]]:
|
||||
result = collections.OrderedDict[str, Any]()
|
||||
for field in fields:
|
||||
if isinstance(field, list):
|
||||
# This is an array field, starting with a 1-byte item count.
|
||||
@@ -1766,11 +1768,18 @@ class HCI_Object:
|
||||
continue
|
||||
|
||||
field_name, field_type = field
|
||||
field_value, field_size = HCI_Object.parse_field(data, offset, field_type)
|
||||
assert isinstance(field_name, str)
|
||||
field_value, field_size = HCI_Object.parse_field(
|
||||
data, offset, cast(FieldSpec, field_type)
|
||||
)
|
||||
result[field_name] = field_value
|
||||
offset += field_size
|
||||
|
||||
return result
|
||||
return (offset, result)
|
||||
|
||||
@staticmethod
|
||||
def dict_from_bytes(data, offset, fields):
|
||||
return HCI_Object.dict_and_offset_from_bytes(data, offset, fields)[1]
|
||||
|
||||
@staticmethod
|
||||
def serialize_field(field_value: Any, field_type: FieldSpec) -> bytes:
|
||||
@@ -1995,6 +2004,19 @@ class HCI_Object:
|
||||
return self.to_string()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@dataclasses.dataclass
|
||||
class HCI_Dataclass_Object(HCI_Object):
|
||||
def __post_init__(self) -> None:
|
||||
self.fields = HCI_Object.fields_from_dataclass(self)
|
||||
|
||||
@classmethod
|
||||
def parse_from_bytes(cls, data: bytes, offset: int) -> tuple[int, Self]:
|
||||
fields = HCI_Object.fields_from_dataclass(cls)
|
||||
offset, kwargs = HCI_Object.dict_and_offset_from_bytes(data, offset, fields)
|
||||
return offset, cls(**kwargs)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Bluetooth Address
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -2276,6 +2298,7 @@ class HCI_Command(HCI_Packet):
|
||||
op_code: int = -1
|
||||
fields: Fields = ()
|
||||
return_parameters_fields: Fields = ()
|
||||
parameters: bytes = b''
|
||||
|
||||
@staticmethod
|
||||
def command(
|
||||
@@ -2302,6 +2325,15 @@ class HCI_Command(HCI_Packet):
|
||||
|
||||
return inner
|
||||
|
||||
@classmethod
|
||||
def dataclass_command(cls, subclass):
|
||||
# TODO: Move this to __post_init__ when all packets become dataclasses.
|
||||
subclass.parameters = functools.cached_property(
|
||||
lambda self: HCI_Object.dict_to_bytes(self.__dict__, self.fields)
|
||||
)
|
||||
subclass.parameters.__set_name__(subclass, 'parameters')
|
||||
return HCI_Command.command(HCI_Object.fields_from_dataclass(subclass))(subclass)
|
||||
|
||||
@staticmethod
|
||||
def command_map(symbols: dict[str, Any]) -> dict[int, str]:
|
||||
return {
|
||||
@@ -2331,9 +2363,9 @@ class HCI_Command(HCI_Packet):
|
||||
|
||||
@classmethod
|
||||
def from_parameters(cls, parameters: bytes) -> HCI_Command:
|
||||
command = cls(parameters)
|
||||
HCI_Object.init_from_bytes(command, parameters, 0, cls.fields)
|
||||
return command
|
||||
if dataclasses.is_dataclass(cls):
|
||||
return cls(**HCI_Object.dict_from_bytes(parameters, 0, cls.fields))
|
||||
return cls(parameters, **HCI_Object.dict_from_bytes(parameters, 0, cls.fields))
|
||||
|
||||
@staticmethod
|
||||
def command_name(op_code):
|
||||
@@ -2373,7 +2405,7 @@ class HCI_Command(HCI_Packet):
|
||||
parameters = HCI_Object.dict_to_bytes(kwargs, self.fields)
|
||||
self.parameters = parameters or b''
|
||||
|
||||
def __bytes__(self):
|
||||
def __bytes__(self) -> bytes:
|
||||
parameters = b'' if self.parameters is None else self.parameters
|
||||
return (
|
||||
struct.pack('<BHB', HCI_COMMAND_PACKET, self.op_code, len(parameters))
|
||||
@@ -2394,18 +2426,19 @@ HCI_Command.register_commands(globals())
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command(
|
||||
[
|
||||
('lap', {'size': 3, 'mapper': HCI_Constant.inquiry_lap_name}),
|
||||
('inquiry_length', 1),
|
||||
('num_responses', 1),
|
||||
]
|
||||
)
|
||||
@HCI_Command.dataclass_command
|
||||
@dataclasses.dataclass
|
||||
class HCI_Inquiry_Command(HCI_Command):
|
||||
'''
|
||||
See Bluetooth spec @ 7.1.1 Inquiry Command
|
||||
'''
|
||||
|
||||
lap: int = field(
|
||||
metadata=metadata({'size': 3, 'mapper': HCI_Constant.inquiry_lap_name})
|
||||
)
|
||||
inquiry_length: int = field(metadata=metadata(1))
|
||||
num_responses: int = field(metadata=metadata(1))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command()
|
||||
@@ -5594,6 +5627,7 @@ class HCI_Event(HCI_Packet):
|
||||
vendor_factories: list[Callable[[bytes], Optional[HCI_Event]]] = []
|
||||
event_code: int = -1
|
||||
fields: Fields = ()
|
||||
parameters: bytes = b''
|
||||
|
||||
@staticmethod
|
||||
def event(fields: Optional[Fields] = ()):
|
||||
@@ -5618,6 +5652,15 @@ class HCI_Event(HCI_Packet):
|
||||
|
||||
return inner
|
||||
|
||||
@classmethod
|
||||
def dataclass_event(cls, subclass):
|
||||
# TODO: Move this to __post_init__ when all packets become dataclasses.
|
||||
subclass.parameters = functools.cached_property(
|
||||
lambda self: HCI_Object.dict_to_bytes(self.__dict__, self.fields)
|
||||
)
|
||||
subclass.parameters.__set_name__(subclass, 'parameters')
|
||||
return HCI_Event.event(HCI_Object.fields_from_dataclass(subclass))(subclass)
|
||||
|
||||
@staticmethod
|
||||
def event_map(symbols: dict[str, Any]) -> dict[int, str]:
|
||||
return {
|
||||
@@ -5701,10 +5744,9 @@ class HCI_Event(HCI_Packet):
|
||||
|
||||
@classmethod
|
||||
def from_parameters(cls, parameters: bytes) -> Self:
|
||||
self = cls(parameters)
|
||||
if self.fields:
|
||||
HCI_Object.init_from_bytes(self, parameters, 0, self.fields)
|
||||
return self
|
||||
if dataclasses.is_dataclass(cls):
|
||||
return cls(**HCI_Object.dict_from_bytes(parameters, 0, cls.fields))
|
||||
return cls(parameters, **HCI_Object.dict_from_bytes(parameters, 0, cls.fields))
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -5722,7 +5764,7 @@ class HCI_Event(HCI_Packet):
|
||||
parameters = HCI_Object.dict_to_bytes(kwargs, self.fields)
|
||||
self.parameters = parameters or b''
|
||||
|
||||
def __bytes__(self):
|
||||
def __bytes__(self) -> bytes:
|
||||
parameters = b'' if self.parameters is None else self.parameters
|
||||
return bytes([HCI_EVENT_PACKET, self.event_code, len(parameters)]) + parameters
|
||||
|
||||
@@ -5757,21 +5799,35 @@ class HCI_Extended_Event(HCI_Event):
|
||||
|
||||
ExtendedEvent = TypeVar("ExtendedEvent", bound=HCI_Extended_Event)
|
||||
|
||||
def inner(cls: type[ExtendedEvent]) -> type[ExtendedEvent]:
|
||||
cls.name = cls.__name__.upper()
|
||||
cls.subevent_code = key_with_value(cls.subevent_names, cls.name)
|
||||
if cls.subevent_code is None:
|
||||
raise KeyError(f'subevent {cls.name} not found in subevent_names')
|
||||
def inner(subclass: type[ExtendedEvent]) -> type[ExtendedEvent]:
|
||||
subclass.name = subclass.__name__.upper()
|
||||
subclass.subevent_code = key_with_value(
|
||||
subclass.subevent_names, subclass.name
|
||||
)
|
||||
if subclass.subevent_code is None:
|
||||
raise KeyError(f'subevent {subclass.name} not found in subevent_names')
|
||||
if fields is not None:
|
||||
cls.fields = fields
|
||||
subclass.fields = fields
|
||||
|
||||
# Register a factory for this class
|
||||
cls.subevent_classes[cls.subevent_code] = cls
|
||||
cls.subevent_classes[subclass.subevent_code] = subclass
|
||||
|
||||
return cls
|
||||
return subclass
|
||||
|
||||
return inner
|
||||
|
||||
@classmethod
|
||||
def dataclass_event(cls, subclass):
|
||||
# TODO: Move this to __post_init__ when all packets become dataclasses.
|
||||
subclass.parameters = functools.cached_property(
|
||||
lambda self: (
|
||||
bytes([self.subevent_code])
|
||||
+ HCI_Object.dict_to_bytes(self.__dict__, self.fields)
|
||||
)
|
||||
)
|
||||
subclass.parameters.__set_name__(subclass, 'parameters')
|
||||
return cls.event(HCI_Object.fields_from_dataclass(subclass))(subclass)
|
||||
|
||||
@classmethod
|
||||
def subevent_name(cls, subevent_code):
|
||||
subevent_name = cls.subevent_names.get(subevent_code)
|
||||
@@ -5809,10 +5865,9 @@ class HCI_Extended_Event(HCI_Event):
|
||||
@classmethod
|
||||
def from_parameters(cls, parameters: bytes) -> HCI_Extended_Event:
|
||||
"""Factory method for subclasses (the subevent code has already been parsed)"""
|
||||
event = cls(parameters)
|
||||
if event.fields:
|
||||
HCI_Object.init_from_bytes(event, parameters, 1, event.fields)
|
||||
return event
|
||||
if dataclasses.is_dataclass(cls):
|
||||
return cls(**HCI_Object.dict_from_bytes(parameters, 1, cls.fields))
|
||||
return cls(parameters, **HCI_Object.dict_from_bytes(parameters, 1, cls.fields))
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -5878,97 +5933,42 @@ class HCI_LE_Connection_Complete_Event(HCI_LE_Meta_Event):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_LE_Meta_Event.dataclass_event
|
||||
@dataclasses.dataclass
|
||||
class HCI_LE_Advertising_Report_Event(HCI_LE_Meta_Event):
|
||||
'''
|
||||
See Bluetooth spec @ 7.7.65.2 LE Advertising Report Event
|
||||
'''
|
||||
|
||||
subevent_code = HCI_LE_ADVERTISING_REPORT_EVENT
|
||||
name = 'HCI_LE_ADVERTISING_REPORT_EVENT'
|
||||
class EventType(utils.OpenIntEnum):
|
||||
ADV_IND = 0x00
|
||||
ADV_DIRECT_IND = 0x01
|
||||
ADV_SCAN_IND = 0x02
|
||||
ADV_NONCONN_IND = 0x03
|
||||
SCAN_RSP = 0x04
|
||||
|
||||
# Event Types
|
||||
ADV_IND = 0x00
|
||||
ADV_DIRECT_IND = 0x01
|
||||
ADV_SCAN_IND = 0x02
|
||||
ADV_NONCONN_IND = 0x03
|
||||
SCAN_RSP = 0x04
|
||||
|
||||
EVENT_TYPE_NAMES = {
|
||||
ADV_IND: 'ADV_IND', # Connectable and scannable undirected advertising
|
||||
ADV_DIRECT_IND: 'ADV_DIRECT_IND', # Connectable directed advertising
|
||||
ADV_SCAN_IND: 'ADV_SCAN_IND', # Scannable undirected advertising
|
||||
ADV_NONCONN_IND: 'ADV_NONCONN_IND', # Non connectable undirected advertising
|
||||
SCAN_RSP: 'SCAN_RSP', # Scan Response
|
||||
}
|
||||
|
||||
class Report(HCI_Object):
|
||||
FIELDS = [
|
||||
('event_type', 1),
|
||||
('address_type', Address.ADDRESS_TYPE_SPEC),
|
||||
('address', Address.parse_address_preceded_by_type),
|
||||
('data', 'v'),
|
||||
('rssi', -1),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def from_parameters(cls, parameters, offset):
|
||||
return cls.from_bytes(parameters, offset, cls.FIELDS)
|
||||
|
||||
def event_type_string(self):
|
||||
return HCI_LE_Advertising_Report_Event.event_type_name(self.event_type)
|
||||
|
||||
def to_string(self, indentation='', _=None):
|
||||
def data_to_str(data):
|
||||
try:
|
||||
return data.hex() + ': ' + str(AdvertisingData.from_bytes(data))
|
||||
except Exception:
|
||||
return data.hex()
|
||||
|
||||
return super().to_string(
|
||||
indentation,
|
||||
@dataclasses.dataclass
|
||||
class Report(HCI_Dataclass_Object):
|
||||
event_type: int = field(
|
||||
metadata=metadata(
|
||||
{
|
||||
'event_type': HCI_LE_Advertising_Report_Event.event_type_name,
|
||||
'address_type': Address.address_type_name,
|
||||
'data': data_to_str,
|
||||
},
|
||||
'size': 1,
|
||||
'mapper': lambda x: HCI_LE_Advertising_Report_Event.EventType(
|
||||
x
|
||||
).name,
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def event_type_name(cls, event_type):
|
||||
return name_or_number(cls.EVENT_TYPE_NAMES, event_type)
|
||||
|
||||
@classmethod
|
||||
def from_parameters(cls, parameters: bytes) -> Self:
|
||||
num_reports = parameters[1]
|
||||
reports = []
|
||||
offset = 2
|
||||
for _ in range(num_reports):
|
||||
report = cls.Report.from_parameters(parameters, offset)
|
||||
offset += 10 + len(report.data)
|
||||
reports.append(report)
|
||||
|
||||
return cls(reports)
|
||||
|
||||
def __init__(self, reports):
|
||||
self.reports = reports[:]
|
||||
|
||||
# Serialize the fields
|
||||
parameters = bytes([HCI_LE_ADVERTISING_REPORT_EVENT, len(reports)]) + b''.join(
|
||||
[bytes(report) for report in reports]
|
||||
)
|
||||
|
||||
super().__init__(parameters)
|
||||
|
||||
def __str__(self):
|
||||
reports = '\n'.join(
|
||||
[f'{i}:\n{report.to_string(" ")}' for i, report in enumerate(self.reports)]
|
||||
address_type: int = field(metadata=metadata(Address.ADDRESS_TYPE_SPEC))
|
||||
address: Address = field(
|
||||
metadata=metadata(Address.parse_address_preceded_by_type)
|
||||
)
|
||||
return f'{color(self.subevent_name(self.subevent_code), "magenta")}:\n{reports}'
|
||||
data: bytes = field(metadata=metadata('v'))
|
||||
rssi: int = field(metadata=metadata(-1))
|
||||
|
||||
|
||||
HCI_LE_Meta_Event.subevent_classes[HCI_LE_ADVERTISING_REPORT_EVENT] = (
|
||||
HCI_LE_Advertising_Report_Event
|
||||
)
|
||||
reports: Sequence[Report] = field(
|
||||
metadata=metadata(Report.parse_from_bytes, list_begin=True, list_end=True)
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -6107,40 +6107,31 @@ class HCI_LE_PHY_Update_Complete_Event(HCI_LE_Meta_Event):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_LE_Meta_Event.dataclass_event
|
||||
@dataclasses.dataclass
|
||||
class HCI_LE_Extended_Advertising_Report_Event(HCI_LE_Meta_Event):
|
||||
'''
|
||||
See Bluetooth spec @ 7.7.65.13 LE Extended Advertising Report Event
|
||||
'''
|
||||
|
||||
subevent_code = HCI_LE_EXTENDED_ADVERTISING_REPORT_EVENT
|
||||
name = 'HCI_LE_EXTENDED_ADVERTISING_REPORT_EVENT'
|
||||
|
||||
# Event types flags
|
||||
CONNECTABLE_ADVERTISING = 0
|
||||
SCANNABLE_ADVERTISING = 1
|
||||
DIRECTED_ADVERTISING = 2
|
||||
SCAN_RESPONSE = 3
|
||||
LEGACY_ADVERTISING_PDU_USED = 4
|
||||
class EventType(enum.IntFlag):
|
||||
CONNECTABLE_ADVERTISING = 1 << 0
|
||||
SCANNABLE_ADVERTISING = 1 << 1
|
||||
DIRECTED_ADVERTISING = 1 << 2
|
||||
SCAN_RESPONSE = 1 << 3
|
||||
LEGACY_ADVERTISING_PDU_USED = 1 << 4
|
||||
|
||||
DATA_COMPLETE = 0x00
|
||||
DATA_INCOMPLETE_MORE_TO_COME = 0x01
|
||||
DATA_INCOMPLETE_TRUNCATED_NO_MORE_TO_COME = 0x02
|
||||
|
||||
EVENT_TYPE_FLAG_NAMES = (
|
||||
'CONNECTABLE_ADVERTISING',
|
||||
'SCANNABLE_ADVERTISING',
|
||||
'DIRECTED_ADVERTISING',
|
||||
'SCAN_RESPONSE',
|
||||
'LEGACY_ADVERTISING_PDU_USED',
|
||||
)
|
||||
|
||||
LEGACY_PDU_TYPE_MAP = {
|
||||
0b0011: HCI_LE_Advertising_Report_Event.ADV_IND,
|
||||
0b0101: HCI_LE_Advertising_Report_Event.ADV_DIRECT_IND,
|
||||
0b0010: HCI_LE_Advertising_Report_Event.ADV_SCAN_IND,
|
||||
0b0000: HCI_LE_Advertising_Report_Event.ADV_NONCONN_IND,
|
||||
0b1011: HCI_LE_Advertising_Report_Event.SCAN_RSP,
|
||||
0b1010: HCI_LE_Advertising_Report_Event.SCAN_RSP,
|
||||
0b0011: HCI_LE_Advertising_Report_Event.EventType.ADV_IND,
|
||||
0b0101: HCI_LE_Advertising_Report_Event.EventType.ADV_DIRECT_IND,
|
||||
0b0010: HCI_LE_Advertising_Report_Event.EventType.ADV_SCAN_IND,
|
||||
0b0000: HCI_LE_Advertising_Report_Event.EventType.ADV_NONCONN_IND,
|
||||
0b1011: HCI_LE_Advertising_Report_Event.EventType.SCAN_RSP,
|
||||
0b1010: HCI_LE_Advertising_Report_Event.EventType.SCAN_RSP,
|
||||
}
|
||||
|
||||
NO_ADI_FIELD_PROVIDED = 0xFF
|
||||
@@ -6149,110 +6140,41 @@ class HCI_LE_Extended_Advertising_Report_Event(HCI_LE_Meta_Event):
|
||||
ANONYMOUS_ADDRESS_TYPE = 0xFF
|
||||
UNRESOLVED_RESOLVABLE_ADDRESS_TYPE = 0xFE
|
||||
|
||||
class Report(HCI_Object):
|
||||
FIELDS = [
|
||||
('event_type', 2),
|
||||
('address_type', Address.ADDRESS_TYPE_SPEC),
|
||||
('address', Address.parse_address_preceded_by_type),
|
||||
('primary_phy', {'size': 1, 'mapper': HCI_Constant.le_phy_name}),
|
||||
('secondary_phy', {'size': 1, 'mapper': HCI_Constant.le_phy_name}),
|
||||
('advertising_sid', 1),
|
||||
('tx_power', 1),
|
||||
('rssi', -1),
|
||||
('periodic_advertising_interval', 2),
|
||||
('direct_address_type', Address.ADDRESS_TYPE_SPEC),
|
||||
('direct_address', Address.parse_address_preceded_by_type),
|
||||
('data', 'v'),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def from_parameters(cls, parameters, offset):
|
||||
return cls.from_bytes(parameters, offset, cls.FIELDS)
|
||||
|
||||
def event_type_string(self):
|
||||
return HCI_LE_Extended_Advertising_Report_Event.event_type_string(
|
||||
self.event_type
|
||||
)
|
||||
|
||||
def to_string(self, indentation='', _=None):
|
||||
# pylint: disable=line-too-long
|
||||
def data_to_str(data):
|
||||
try:
|
||||
return data.hex() + ': ' + str(AdvertisingData.from_bytes(data))
|
||||
except Exception:
|
||||
return data.hex()
|
||||
|
||||
return super().to_string(
|
||||
indentation,
|
||||
@dataclasses.dataclass
|
||||
class Report(HCI_Dataclass_Object):
|
||||
event_type: int = field(
|
||||
metadata=metadata(
|
||||
{
|
||||
'event_type': HCI_LE_Extended_Advertising_Report_Event.event_type_string,
|
||||
'address_type': Address.address_type_name,
|
||||
'data': data_to_str,
|
||||
},
|
||||
'size': 2,
|
||||
'mapper': lambda x: HCI_LE_Extended_Advertising_Report_Event.EventType(
|
||||
x
|
||||
).name,
|
||||
}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
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]
|
||||
address_type: int = field(metadata=metadata(Address.ADDRESS_TYPE_SPEC))
|
||||
address: Address = field(
|
||||
metadata=metadata(Address.parse_address_preceded_by_type)
|
||||
)
|
||||
|
||||
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:
|
||||
# pylint: disable=line-too-long
|
||||
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}'
|
||||
|
||||
@classmethod
|
||||
def from_parameters(cls, parameters: bytes) -> Self:
|
||||
num_reports = parameters[1]
|
||||
reports: list[HCI_LE_Extended_Advertising_Report_Event.Report] = []
|
||||
offset = 2
|
||||
for _ in range(num_reports):
|
||||
report = cls.Report.from_parameters(parameters, offset)
|
||||
offset += 24 + len(report.data)
|
||||
reports.append(report)
|
||||
|
||||
return cls(reports)
|
||||
|
||||
def __init__(
|
||||
self, reports: Sequence[HCI_LE_Extended_Advertising_Report_Event.Report]
|
||||
):
|
||||
self.reports = reports[:]
|
||||
|
||||
# Serialize the fields
|
||||
parameters = bytes(
|
||||
[HCI_LE_EXTENDED_ADVERTISING_REPORT_EVENT, len(reports)]
|
||||
) + b''.join([bytes(report) for report in reports])
|
||||
|
||||
super().__init__(parameters)
|
||||
|
||||
def __str__(self):
|
||||
reports = '\n'.join(
|
||||
[f'{i}:\n{report.to_string(" ")}' for i, report in enumerate(self.reports)]
|
||||
primary_phy: int = field(
|
||||
metadata=metadata({'size': 1, 'mapper': HCI_Constant.le_phy_name})
|
||||
)
|
||||
return f'{color(self.subevent_name(self.subevent_code), "magenta")}:\n{reports}'
|
||||
secondary_phy: int = field(
|
||||
metadata=metadata({'size': 1, 'mapper': HCI_Constant.le_phy_name})
|
||||
)
|
||||
advertising_sid: int = field(metadata=metadata(1))
|
||||
tx_power: int = field(metadata=metadata(1))
|
||||
rssi: int = field(metadata=metadata(-1))
|
||||
periodic_advertising_interval: int = field(metadata=metadata(2))
|
||||
direct_address_type: int = field(metadata=metadata(Address.ADDRESS_TYPE_SPEC))
|
||||
direct_address: int = field(
|
||||
metadata=metadata(Address.parse_address_preceded_by_type)
|
||||
)
|
||||
data: bytes = field(metadata=metadata('v'))
|
||||
|
||||
|
||||
HCI_LE_Meta_Event.subevent_classes[HCI_LE_EXTENDED_ADVERTISING_REPORT_EVENT] = (
|
||||
HCI_LE_Extended_Advertising_Report_Event
|
||||
)
|
||||
reports: Sequence[Report] = field(
|
||||
metadata=metadata(Report.parse_from_bytes, list_begin=True, list_end=True)
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -6876,12 +6798,15 @@ class HCI_LE_CS_Test_End_Complete_Event(HCI_LE_Meta_Event):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Event.event([('status', STATUS_SPEC)])
|
||||
@HCI_Event.dataclass_event
|
||||
@dataclasses.dataclass
|
||||
class HCI_Inquiry_Complete_Event(HCI_Event):
|
||||
'''
|
||||
See Bluetooth spec @ 7.7.1 Inquiry Complete Event
|
||||
'''
|
||||
|
||||
status: int = field(metadata=metadata(STATUS_SPEC))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Event.event(
|
||||
|
||||
@@ -1126,11 +1126,19 @@ class Host(utils.EventEmitter):
|
||||
else:
|
||||
self.emit('connection_phy_update_failure', connection.handle, event.status)
|
||||
|
||||
def on_hci_le_advertising_report_event(self, event):
|
||||
def on_hci_le_advertising_report_event(
|
||||
self,
|
||||
event: (
|
||||
hci.HCI_LE_Advertising_Report_Event
|
||||
| hci.HCI_LE_Extended_Advertising_Report_Event
|
||||
),
|
||||
):
|
||||
for report in event.reports:
|
||||
self.emit('advertising_report', report)
|
||||
|
||||
def on_hci_le_extended_advertising_report_event(self, event):
|
||||
def on_hci_le_extended_advertising_report_event(
|
||||
self, event: hci.HCI_LE_Extended_Advertising_Report_Event
|
||||
):
|
||||
self.on_hci_le_advertising_report_event(event)
|
||||
|
||||
def on_hci_le_advertising_set_terminated_event(self, event):
|
||||
|
||||
@@ -21,60 +21,6 @@ import struct
|
||||
import pytest
|
||||
|
||||
from bumble import hci
|
||||
from bumble.hci import (
|
||||
HCI_DISCONNECT_COMMAND,
|
||||
HCI_LE_1M_PHY_BIT,
|
||||
HCI_LE_CODED_PHY_BIT,
|
||||
HCI_LE_READ_BUFFER_SIZE_COMMAND,
|
||||
HCI_RESET_COMMAND,
|
||||
HCI_VENDOR_EVENT,
|
||||
HCI_SUCCESS,
|
||||
HCI_LE_CONNECTION_COMPLETE_EVENT,
|
||||
HCI_LE_ENHANCED_CONNECTION_COMPLETE_V2_EVENT,
|
||||
Address,
|
||||
CodingFormat,
|
||||
CodecID,
|
||||
HCI_Command,
|
||||
HCI_Command_Complete_Event,
|
||||
HCI_Command_Status_Event,
|
||||
HCI_CustomPacket,
|
||||
HCI_Disconnect_Command,
|
||||
HCI_Event,
|
||||
HCI_IsoDataPacket,
|
||||
HCI_LE_Add_Device_To_Filter_Accept_List_Command,
|
||||
HCI_LE_Advertising_Report_Event,
|
||||
HCI_LE_Channel_Selection_Algorithm_Event,
|
||||
HCI_LE_Connection_Complete_Event,
|
||||
HCI_LE_Connection_Update_Command,
|
||||
HCI_LE_Connection_Update_Complete_Event,
|
||||
HCI_LE_Create_Connection_Command,
|
||||
HCI_LE_Extended_Create_Connection_Command,
|
||||
HCI_LE_Read_Buffer_Size_Command,
|
||||
HCI_LE_Read_Remote_Features_Command,
|
||||
HCI_LE_Read_Remote_Features_Complete_Event,
|
||||
HCI_LE_Remove_Device_From_Filter_Accept_List_Command,
|
||||
HCI_LE_Set_Advertising_Data_Command,
|
||||
HCI_LE_Set_Advertising_Parameters_Command,
|
||||
HCI_LE_Set_Default_PHY_Command,
|
||||
HCI_LE_Set_Event_Mask_Command,
|
||||
HCI_LE_Set_Extended_Advertising_Enable_Command,
|
||||
HCI_LE_Set_Extended_Scan_Parameters_Command,
|
||||
HCI_LE_Set_Random_Address_Command,
|
||||
HCI_LE_Set_Scan_Enable_Command,
|
||||
HCI_LE_Set_Scan_Parameters_Command,
|
||||
HCI_LE_Setup_ISO_Data_Path_Command,
|
||||
HCI_Number_Of_Completed_Packets_Event,
|
||||
HCI_Packet,
|
||||
HCI_PIN_Code_Request_Reply_Command,
|
||||
HCI_Read_Local_Supported_Codecs_Command,
|
||||
HCI_Read_Local_Supported_Codecs_V2_Command,
|
||||
HCI_Read_Local_Supported_Commands_Command,
|
||||
HCI_Read_Local_Supported_Features_Command,
|
||||
HCI_Read_Local_Version_Information_Command,
|
||||
HCI_Reset_Command,
|
||||
HCI_Set_Event_Mask_Command,
|
||||
HCI_Vendor_Event,
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -84,7 +30,7 @@ from bumble.hci import (
|
||||
def basic_check(x):
|
||||
packet = bytes(x)
|
||||
print(packet.hex())
|
||||
parsed = HCI_Packet.from_bytes(packet)
|
||||
parsed = hci.HCI_Packet.from_bytes(packet)
|
||||
x_str = str(x)
|
||||
parsed_str = str(parsed)
|
||||
print(x_str)
|
||||
@@ -95,18 +41,18 @@ def basic_check(x):
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_Event():
|
||||
event = HCI_Event(event_code=0xF9)
|
||||
event = hci.HCI_Event(event_code=0xF9)
|
||||
basic_check(event)
|
||||
|
||||
event = HCI_Event(event_code=0xF8, parameters=bytes.fromhex('AABBCC'))
|
||||
event = hci.HCI_Event(event_code=0xF8, parameters=bytes.fromhex('AABBCC'))
|
||||
basic_check(event)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_LE_Connection_Complete_Event():
|
||||
address = Address('00:11:22:33:44:55')
|
||||
event = HCI_LE_Connection_Complete_Event(
|
||||
status=HCI_SUCCESS,
|
||||
address = hci.Address('00:11:22:33:44:55')
|
||||
event = hci.HCI_LE_Connection_Complete_Event(
|
||||
status=hci.HCI_SUCCESS,
|
||||
connection_handle=1,
|
||||
role=1,
|
||||
peer_address_type=1,
|
||||
@@ -121,25 +67,47 @@ def test_HCI_LE_Connection_Complete_Event():
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_LE_Advertising_Report_Event():
|
||||
address = Address('00:11:22:33:44:55/P')
|
||||
report = HCI_LE_Advertising_Report_Event.Report(
|
||||
HCI_LE_Advertising_Report_Event.Report.FIELDS,
|
||||
event_type=HCI_LE_Advertising_Report_Event.ADV_IND,
|
||||
address_type=Address.PUBLIC_DEVICE_ADDRESS,
|
||||
address = hci.Address('00:11:22:33:44:55/P')
|
||||
report = hci.HCI_LE_Advertising_Report_Event.Report(
|
||||
event_type=hci.HCI_LE_Advertising_Report_Event.EventType.ADV_IND,
|
||||
address_type=hci.Address.PUBLIC_DEVICE_ADDRESS,
|
||||
address=address,
|
||||
data=bytes.fromhex(
|
||||
'0201061106ba5689a6fabfa2bd01467d6e00fbabad08160a181604659b03'
|
||||
),
|
||||
rssi=100,
|
||||
)
|
||||
event = HCI_LE_Advertising_Report_Event([report])
|
||||
event = hci.HCI_LE_Advertising_Report_Event([report])
|
||||
basic_check(event)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_LE_Extended_Advertising_Report_Event():
|
||||
address = hci.Address('00:11:22:33:44:55/P')
|
||||
report = hci.HCI_LE_Extended_Advertising_Report_Event.Report(
|
||||
event_type=hci.HCI_LE_Extended_Advertising_Report_Event.EventType.CONNECTABLE_ADVERTISING,
|
||||
address_type=hci.Address.PUBLIC_DEVICE_ADDRESS,
|
||||
address=address,
|
||||
data=bytes.fromhex(
|
||||
'0201061106ba5689a6fabfa2bd01467d6e00fbabad08160a181604659b03'
|
||||
),
|
||||
rssi=100,
|
||||
primary_phy=hci.HCI_LE_1M_PHY,
|
||||
secondary_phy=hci.HCI_LE_CODED_PHY,
|
||||
advertising_sid=0,
|
||||
tx_power=10,
|
||||
periodic_advertising_interval=2,
|
||||
direct_address=hci.Address('00:11:22:33:44:55/P'),
|
||||
direct_address_type=hci.Address.PUBLIC_DEVICE_ADDRESS,
|
||||
)
|
||||
event = hci.HCI_LE_Extended_Advertising_Report_Event([report])
|
||||
basic_check(event)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_LE_Read_Remote_Features_Complete_Event():
|
||||
event = HCI_LE_Read_Remote_Features_Complete_Event(
|
||||
status=HCI_SUCCESS,
|
||||
event = hci.HCI_LE_Read_Remote_Features_Complete_Event(
|
||||
status=hci.HCI_SUCCESS,
|
||||
connection_handle=0x007,
|
||||
le_features=bytes.fromhex('0011223344556677'),
|
||||
)
|
||||
@@ -148,8 +116,8 @@ def test_HCI_LE_Read_Remote_Features_Complete_Event():
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_LE_Connection_Update_Complete_Event():
|
||||
event = HCI_LE_Connection_Update_Complete_Event(
|
||||
status=HCI_SUCCESS,
|
||||
event = hci.HCI_LE_Connection_Update_Complete_Event(
|
||||
status=hci.HCI_SUCCESS,
|
||||
connection_handle=0x007,
|
||||
connection_interval=10,
|
||||
peripheral_latency=3,
|
||||
@@ -160,7 +128,7 @@ def test_HCI_LE_Connection_Update_Complete_Event():
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_LE_Channel_Selection_Algorithm_Event():
|
||||
event = HCI_LE_Channel_Selection_Algorithm_Event(
|
||||
event = hci.HCI_LE_Channel_Selection_Algorithm_Event(
|
||||
connection_handle=7, channel_selection_algorithm=1
|
||||
)
|
||||
basic_check(event)
|
||||
@@ -169,10 +137,10 @@ def test_HCI_LE_Channel_Selection_Algorithm_Event():
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_Command_Complete_Event():
|
||||
# With a serializable object
|
||||
event = HCI_Command_Complete_Event(
|
||||
event = hci.HCI_Command_Complete_Event(
|
||||
num_hci_command_packets=34,
|
||||
command_opcode=HCI_LE_READ_BUFFER_SIZE_COMMAND,
|
||||
return_parameters=HCI_LE_Read_Buffer_Size_Command.create_return_parameters(
|
||||
command_opcode=hci.HCI_LE_READ_BUFFER_SIZE_COMMAND,
|
||||
return_parameters=hci.HCI_LE_Read_Buffer_Size_Command.create_return_parameters(
|
||||
status=0,
|
||||
le_acl_data_packet_length=1234,
|
||||
total_num_le_acl_data_packets=56,
|
||||
@@ -181,26 +149,28 @@ def test_HCI_Command_Complete_Event():
|
||||
basic_check(event)
|
||||
|
||||
# With an arbitrary byte array
|
||||
event = HCI_Command_Complete_Event(
|
||||
event = hci.HCI_Command_Complete_Event(
|
||||
num_hci_command_packets=1,
|
||||
command_opcode=HCI_RESET_COMMAND,
|
||||
command_opcode=hci.HCI_RESET_COMMAND,
|
||||
return_parameters=bytes([1, 2, 3, 4]),
|
||||
)
|
||||
basic_check(event)
|
||||
|
||||
# With a simple status as a 1-byte array
|
||||
event = HCI_Command_Complete_Event(
|
||||
event = hci.HCI_Command_Complete_Event(
|
||||
num_hci_command_packets=1,
|
||||
command_opcode=HCI_RESET_COMMAND,
|
||||
command_opcode=hci.HCI_RESET_COMMAND,
|
||||
return_parameters=bytes([7]),
|
||||
)
|
||||
basic_check(event)
|
||||
event = HCI_Packet.from_bytes(bytes(event))
|
||||
event = hci.HCI_Packet.from_bytes(bytes(event))
|
||||
assert event.return_parameters == 7
|
||||
|
||||
# With a simple status as an integer status
|
||||
event = HCI_Command_Complete_Event(
|
||||
num_hci_command_packets=1, command_opcode=HCI_RESET_COMMAND, return_parameters=9
|
||||
event = hci.HCI_Command_Complete_Event(
|
||||
num_hci_command_packets=1,
|
||||
command_opcode=hci.HCI_RESET_COMMAND,
|
||||
return_parameters=9,
|
||||
)
|
||||
basic_check(event)
|
||||
assert event.return_parameters == 9
|
||||
@@ -208,15 +178,15 @@ def test_HCI_Command_Complete_Event():
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_Command_Status_Event():
|
||||
event = HCI_Command_Status_Event(
|
||||
status=0, num_hci_command_packets=37, command_opcode=HCI_DISCONNECT_COMMAND
|
||||
event = hci.HCI_Command_Status_Event(
|
||||
status=0, num_hci_command_packets=37, command_opcode=hci.HCI_DISCONNECT_COMMAND
|
||||
)
|
||||
basic_check(event)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_Number_Of_Completed_Packets_Event():
|
||||
event = HCI_Number_Of_Completed_Packets_Event(
|
||||
event = hci.HCI_Number_Of_Completed_Packets_Event(
|
||||
connection_handles=(1, 2),
|
||||
num_completed_packets=(3, 4),
|
||||
)
|
||||
@@ -226,16 +196,16 @@ def test_HCI_Number_Of_Completed_Packets_Event():
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_Vendor_Event():
|
||||
data = bytes.fromhex('01020304')
|
||||
event = HCI_Vendor_Event(data=data)
|
||||
event = hci.HCI_Vendor_Event(data=data)
|
||||
event_bytes = bytes(event)
|
||||
parsed = HCI_Packet.from_bytes(event_bytes)
|
||||
assert isinstance(parsed, HCI_Vendor_Event)
|
||||
parsed = hci.HCI_Packet.from_bytes(event_bytes)
|
||||
assert isinstance(parsed, hci.HCI_Vendor_Event)
|
||||
assert parsed.data == data
|
||||
|
||||
class HCI_Custom_Event(HCI_Event):
|
||||
class HCI_Custom_Event(hci.HCI_Event):
|
||||
def __init__(self, blabla):
|
||||
super().__init__(
|
||||
event_code=HCI_VENDOR_EVENT, parameters=struct.pack("<I", blabla)
|
||||
event_code=hci.HCI_VENDOR_EVENT, parameters=struct.pack("<I", blabla)
|
||||
)
|
||||
self.name = 'HCI_CUSTOM_EVENT'
|
||||
self.blabla = blabla
|
||||
@@ -245,27 +215,27 @@ def test_HCI_Vendor_Event():
|
||||
return HCI_Custom_Event(blabla=struct.unpack('<I', payload)[0])
|
||||
return None
|
||||
|
||||
HCI_Event.add_vendor_factory(create_event)
|
||||
parsed = HCI_Packet.from_bytes(event_bytes)
|
||||
hci.HCI_Event.add_vendor_factory(create_event)
|
||||
parsed = hci.HCI_Packet.from_bytes(event_bytes)
|
||||
assert isinstance(parsed, HCI_Custom_Event)
|
||||
assert parsed.blabla == 0x04030201
|
||||
event_bytes2 = event_bytes[:3] + bytes([7]) + event_bytes[4:]
|
||||
parsed = HCI_Packet.from_bytes(event_bytes2)
|
||||
parsed = hci.HCI_Packet.from_bytes(event_bytes2)
|
||||
assert not isinstance(parsed, HCI_Custom_Event)
|
||||
assert isinstance(parsed, HCI_Vendor_Event)
|
||||
HCI_Event.remove_vendor_factory(create_event)
|
||||
assert isinstance(parsed, hci.HCI_Vendor_Event)
|
||||
hci.HCI_Event.remove_vendor_factory(create_event)
|
||||
|
||||
parsed = HCI_Packet.from_bytes(event_bytes)
|
||||
parsed = hci.HCI_Packet.from_bytes(event_bytes)
|
||||
assert not isinstance(parsed, HCI_Custom_Event)
|
||||
assert isinstance(parsed, HCI_Vendor_Event)
|
||||
assert isinstance(parsed, hci.HCI_Vendor_Event)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_Command():
|
||||
command = HCI_Command(op_code=0x5566)
|
||||
command = hci.HCI_Command(op_code=0x5566)
|
||||
basic_check(command)
|
||||
|
||||
command = HCI_Command(op_code=0x5566, parameters=bytes.fromhex('AABBCC'))
|
||||
command = hci.HCI_Command(op_code=0x5566, parameters=bytes.fromhex('AABBCC'))
|
||||
basic_check(command)
|
||||
|
||||
|
||||
@@ -324,12 +294,12 @@ def test_HCI_PIN_Code_Request_Reply_Command():
|
||||
pin_code = b'1234'
|
||||
pin_code_length = len(pin_code)
|
||||
# here to make the test pass, we need to
|
||||
# pad pin_code, as HCI_Object.format_fields
|
||||
# pad pin_code, as hci.HCI_Object.format_fields
|
||||
# does not do it for us
|
||||
padded_pin_code = pin_code + bytes(16 - pin_code_length)
|
||||
command = HCI_PIN_Code_Request_Reply_Command(
|
||||
bd_addr=Address(
|
||||
'00:11:22:33:44:55', address_type=Address.PUBLIC_DEVICE_ADDRESS
|
||||
command = hci.HCI_PIN_Code_Request_Reply_Command(
|
||||
bd_addr=hci.Address(
|
||||
'00:11:22:33:44:55', address_type=hci.Address.PUBLIC_DEVICE_ADDRESS
|
||||
),
|
||||
pin_code_length=pin_code_length,
|
||||
pin_code=padded_pin_code,
|
||||
@@ -338,47 +308,49 @@ def test_HCI_PIN_Code_Request_Reply_Command():
|
||||
|
||||
|
||||
def test_HCI_Reset_Command():
|
||||
command = HCI_Reset_Command()
|
||||
command = hci.HCI_Reset_Command()
|
||||
basic_check(command)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_Read_Local_Version_Information_Command():
|
||||
command = HCI_Read_Local_Version_Information_Command()
|
||||
command = hci.HCI_Read_Local_Version_Information_Command()
|
||||
basic_check(command)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_Read_Local_Supported_Commands_Command():
|
||||
command = HCI_Read_Local_Supported_Commands_Command()
|
||||
command = hci.HCI_Read_Local_Supported_Commands_Command()
|
||||
basic_check(command)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_Read_Local_Supported_Features_Command():
|
||||
command = HCI_Read_Local_Supported_Features_Command()
|
||||
command = hci.HCI_Read_Local_Supported_Features_Command()
|
||||
basic_check(command)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_Disconnect_Command():
|
||||
command = HCI_Disconnect_Command(connection_handle=123, reason=0x11)
|
||||
command = hci.HCI_Disconnect_Command(connection_handle=123, reason=0x11)
|
||||
basic_check(command)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_Set_Event_Mask_Command():
|
||||
command = HCI_Set_Event_Mask_Command(event_mask=bytes.fromhex('0011223344556677'))
|
||||
command = hci.HCI_Set_Event_Mask_Command(
|
||||
event_mask=bytes.fromhex('0011223344556677')
|
||||
)
|
||||
basic_check(command)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_LE_Set_Event_Mask_Command():
|
||||
command = HCI_LE_Set_Event_Mask_Command(
|
||||
le_event_mask=HCI_LE_Set_Event_Mask_Command.mask(
|
||||
command = hci.HCI_LE_Set_Event_Mask_Command(
|
||||
le_event_mask=hci.HCI_LE_Set_Event_Mask_Command.mask(
|
||||
[
|
||||
HCI_LE_CONNECTION_COMPLETE_EVENT,
|
||||
HCI_LE_ENHANCED_CONNECTION_COMPLETE_V2_EVENT,
|
||||
hci.HCI_LE_CONNECTION_COMPLETE_EVENT,
|
||||
hci.HCI_LE_ENHANCED_CONNECTION_COMPLETE_V2_EVENT,
|
||||
]
|
||||
)
|
||||
)
|
||||
@@ -388,21 +360,21 @@ def test_HCI_LE_Set_Event_Mask_Command():
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_LE_Set_Random_Address_Command():
|
||||
command = HCI_LE_Set_Random_Address_Command(
|
||||
random_address=Address('00:11:22:33:44:55')
|
||||
command = hci.HCI_LE_Set_Random_Address_Command(
|
||||
random_address=hci.Address('00:11:22:33:44:55')
|
||||
)
|
||||
basic_check(command)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_LE_Set_Advertising_Parameters_Command():
|
||||
command = HCI_LE_Set_Advertising_Parameters_Command(
|
||||
command = hci.HCI_LE_Set_Advertising_Parameters_Command(
|
||||
advertising_interval_min=20,
|
||||
advertising_interval_max=30,
|
||||
advertising_type=HCI_LE_Set_Advertising_Parameters_Command.ADV_NONCONN_IND,
|
||||
own_address_type=Address.PUBLIC_DEVICE_ADDRESS,
|
||||
peer_address_type=Address.RANDOM_DEVICE_ADDRESS,
|
||||
peer_address=Address('00:11:22:33:44:55'),
|
||||
advertising_type=hci.HCI_LE_Set_Advertising_Parameters_Command.ADV_NONCONN_IND,
|
||||
own_address_type=hci.Address.PUBLIC_DEVICE_ADDRESS,
|
||||
peer_address_type=hci.Address.RANDOM_DEVICE_ADDRESS,
|
||||
peer_address=hci.Address('00:11:22:33:44:55'),
|
||||
advertising_channel_map=0x03,
|
||||
advertising_filter_policy=1,
|
||||
)
|
||||
@@ -411,7 +383,7 @@ def test_HCI_LE_Set_Advertising_Parameters_Command():
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_LE_Set_Advertising_Data_Command():
|
||||
command = HCI_LE_Set_Advertising_Data_Command(
|
||||
command = hci.HCI_LE_Set_Advertising_Data_Command(
|
||||
advertising_data=bytes.fromhex('AABBCC')
|
||||
)
|
||||
basic_check(command)
|
||||
@@ -419,7 +391,7 @@ def test_HCI_LE_Set_Advertising_Data_Command():
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_LE_Set_Scan_Parameters_Command():
|
||||
command = HCI_LE_Set_Scan_Parameters_Command(
|
||||
command = hci.HCI_LE_Set_Scan_Parameters_Command(
|
||||
le_scan_type=1,
|
||||
le_scan_interval=20,
|
||||
le_scan_window=10,
|
||||
@@ -431,18 +403,18 @@ def test_HCI_LE_Set_Scan_Parameters_Command():
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_LE_Set_Scan_Enable_Command():
|
||||
command = HCI_LE_Set_Scan_Enable_Command(le_scan_enable=1, filter_duplicates=0)
|
||||
command = hci.HCI_LE_Set_Scan_Enable_Command(le_scan_enable=1, filter_duplicates=0)
|
||||
basic_check(command)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_LE_Create_Connection_Command():
|
||||
command = HCI_LE_Create_Connection_Command(
|
||||
command = hci.HCI_LE_Create_Connection_Command(
|
||||
le_scan_interval=4,
|
||||
le_scan_window=5,
|
||||
initiator_filter_policy=1,
|
||||
peer_address_type=1,
|
||||
peer_address=Address('00:11:22:33:44:55'),
|
||||
peer_address=hci.Address('00:11:22:33:44:55'),
|
||||
own_address_type=2,
|
||||
connection_interval_min=7,
|
||||
connection_interval_max=8,
|
||||
@@ -456,11 +428,11 @@ def test_HCI_LE_Create_Connection_Command():
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_LE_Extended_Create_Connection_Command():
|
||||
command = HCI_LE_Extended_Create_Connection_Command(
|
||||
command = hci.HCI_LE_Extended_Create_Connection_Command(
|
||||
initiator_filter_policy=0,
|
||||
own_address_type=0,
|
||||
peer_address_type=1,
|
||||
peer_address=Address('00:11:22:33:44:55'),
|
||||
peer_address=hci.Address('00:11:22:33:44:55'),
|
||||
initiating_phys=3,
|
||||
scan_intervals=(10, 11),
|
||||
scan_windows=(12, 13),
|
||||
@@ -476,23 +448,23 @@ def test_HCI_LE_Extended_Create_Connection_Command():
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_LE_Add_Device_To_Filter_Accept_List_Command():
|
||||
command = HCI_LE_Add_Device_To_Filter_Accept_List_Command(
|
||||
address_type=1, address=Address('00:11:22:33:44:55')
|
||||
command = hci.HCI_LE_Add_Device_To_Filter_Accept_List_Command(
|
||||
address_type=1, address=hci.Address('00:11:22:33:44:55')
|
||||
)
|
||||
basic_check(command)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_LE_Remove_Device_From_Filter_Accept_List_Command():
|
||||
command = HCI_LE_Remove_Device_From_Filter_Accept_List_Command(
|
||||
address_type=1, address=Address('00:11:22:33:44:55')
|
||||
command = hci.HCI_LE_Remove_Device_From_Filter_Accept_List_Command(
|
||||
address_type=1, address=hci.Address('00:11:22:33:44:55')
|
||||
)
|
||||
basic_check(command)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_LE_Connection_Update_Command():
|
||||
command = HCI_LE_Connection_Update_Command(
|
||||
command = hci.HCI_LE_Connection_Update_Command(
|
||||
connection_handle=0x0002,
|
||||
connection_interval_min=10,
|
||||
connection_interval_max=20,
|
||||
@@ -506,27 +478,29 @@ def test_HCI_LE_Connection_Update_Command():
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_LE_Read_Remote_Features_Command():
|
||||
command = HCI_LE_Read_Remote_Features_Command(connection_handle=0x0002)
|
||||
command = hci.HCI_LE_Read_Remote_Features_Command(connection_handle=0x0002)
|
||||
basic_check(command)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_LE_Set_Default_PHY_Command():
|
||||
command = HCI_LE_Set_Default_PHY_Command(all_phys=0, tx_phys=1, rx_phys=1)
|
||||
command = hci.HCI_LE_Set_Default_PHY_Command(all_phys=0, tx_phys=1, rx_phys=1)
|
||||
basic_check(command)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_LE_Set_Extended_Scan_Parameters_Command():
|
||||
command = HCI_LE_Set_Extended_Scan_Parameters_Command(
|
||||
own_address_type=Address.RANDOM_DEVICE_ADDRESS,
|
||||
command = hci.HCI_LE_Set_Extended_Scan_Parameters_Command(
|
||||
own_address_type=hci.Address.RANDOM_DEVICE_ADDRESS,
|
||||
# pylint: disable-next=line-too-long
|
||||
scanning_filter_policy=HCI_LE_Set_Extended_Scan_Parameters_Command.BASIC_FILTERED_POLICY,
|
||||
scanning_phys=(1 << HCI_LE_1M_PHY_BIT | 1 << HCI_LE_CODED_PHY_BIT | 1 << 4),
|
||||
scanning_filter_policy=hci.HCI_LE_Set_Extended_Scan_Parameters_Command.BASIC_FILTERED_POLICY,
|
||||
scanning_phys=(
|
||||
1 << hci.HCI_LE_1M_PHY_BIT | 1 << hci.HCI_LE_CODED_PHY_BIT | 1 << 4
|
||||
),
|
||||
scan_types=[
|
||||
HCI_LE_Set_Extended_Scan_Parameters_Command.ACTIVE_SCANNING,
|
||||
HCI_LE_Set_Extended_Scan_Parameters_Command.ACTIVE_SCANNING,
|
||||
HCI_LE_Set_Extended_Scan_Parameters_Command.PASSIVE_SCANNING,
|
||||
hci.HCI_LE_Set_Extended_Scan_Parameters_Command.ACTIVE_SCANNING,
|
||||
hci.HCI_LE_Set_Extended_Scan_Parameters_Command.ACTIVE_SCANNING,
|
||||
hci.HCI_LE_Set_Extended_Scan_Parameters_Command.PASSIVE_SCANNING,
|
||||
],
|
||||
scan_intervals=[1, 2, 3],
|
||||
scan_windows=[4, 5, 6],
|
||||
@@ -536,7 +510,7 @@ def test_HCI_LE_Set_Extended_Scan_Parameters_Command():
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_LE_Set_Extended_Advertising_Enable_Command():
|
||||
command = HCI_Packet.from_bytes(
|
||||
command = hci.HCI_Packet.from_bytes(
|
||||
bytes.fromhex('0139200e010301050008020600090307000a')
|
||||
)
|
||||
assert command.enable == 1
|
||||
@@ -544,7 +518,7 @@ def test_HCI_LE_Set_Extended_Advertising_Enable_Command():
|
||||
assert command.durations == [5, 6, 7]
|
||||
assert command.max_extended_advertising_events == [8, 9, 10]
|
||||
|
||||
command = HCI_LE_Set_Extended_Advertising_Enable_Command(
|
||||
command = hci.HCI_LE_Set_Extended_Advertising_Enable_Command(
|
||||
enable=1,
|
||||
advertising_handles=[1, 2, 3],
|
||||
durations=[5, 6, 7],
|
||||
@@ -555,20 +529,22 @@ def test_HCI_LE_Set_Extended_Advertising_Enable_Command():
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_LE_Setup_ISO_Data_Path_Command():
|
||||
command = HCI_Packet.from_bytes(bytes.fromhex('016e200d60000001030000000000000000'))
|
||||
command = hci.HCI_Packet.from_bytes(
|
||||
bytes.fromhex('016e200d60000001030000000000000000')
|
||||
)
|
||||
|
||||
assert command.connection_handle == 0x0060
|
||||
assert command.data_path_direction == 0x00
|
||||
assert command.data_path_id == 0x01
|
||||
assert command.codec_id == CodingFormat(CodecID.TRANSPARENT)
|
||||
assert command.codec_id == hci.CodingFormat(hci.CodecID.TRANSPARENT)
|
||||
assert command.controller_delay == 0
|
||||
assert command.codec_configuration == b''
|
||||
|
||||
command = HCI_LE_Setup_ISO_Data_Path_Command(
|
||||
command = hci.HCI_LE_Setup_ISO_Data_Path_Command(
|
||||
connection_handle=0x0060,
|
||||
data_path_direction=0x00,
|
||||
data_path_id=0x01,
|
||||
codec_id=CodingFormat(CodecID.TRANSPARENT),
|
||||
codec_id=hci.CodingFormat(hci.CodecID.TRANSPARENT),
|
||||
controller_delay=0x00,
|
||||
codec_configuration=b'',
|
||||
)
|
||||
@@ -578,54 +554,63 @@ def test_HCI_LE_Setup_ISO_Data_Path_Command():
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_Read_Local_Supported_Codecs_Command_Complete():
|
||||
returned_parameters = (
|
||||
HCI_Read_Local_Supported_Codecs_Command.parse_return_parameters(
|
||||
bytes([HCI_SUCCESS, 3, CodecID.A_LOG, CodecID.CVSD, CodecID.LINEAR_PCM, 0])
|
||||
)
|
||||
)
|
||||
assert returned_parameters.standard_codec_ids == [
|
||||
CodecID.A_LOG,
|
||||
CodecID.CVSD,
|
||||
CodecID.LINEAR_PCM,
|
||||
]
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_Read_Local_Supported_Codecs_V2_Command_Complete():
|
||||
returned_parameters = (
|
||||
HCI_Read_Local_Supported_Codecs_V2_Command.parse_return_parameters(
|
||||
hci.HCI_Read_Local_Supported_Codecs_Command.parse_return_parameters(
|
||||
bytes(
|
||||
[
|
||||
HCI_SUCCESS,
|
||||
hci.HCI_SUCCESS,
|
||||
3,
|
||||
CodecID.A_LOG,
|
||||
HCI_Read_Local_Supported_Codecs_V2_Command.Transport.BR_EDR_ACL,
|
||||
CodecID.CVSD,
|
||||
HCI_Read_Local_Supported_Codecs_V2_Command.Transport.BR_EDR_SCO,
|
||||
CodecID.LINEAR_PCM,
|
||||
HCI_Read_Local_Supported_Codecs_V2_Command.Transport.LE_CIS,
|
||||
hci.CodecID.A_LOG,
|
||||
hci.CodecID.CVSD,
|
||||
hci.CodecID.LINEAR_PCM,
|
||||
0,
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
assert returned_parameters.standard_codec_ids == [
|
||||
CodecID.A_LOG,
|
||||
CodecID.CVSD,
|
||||
CodecID.LINEAR_PCM,
|
||||
hci.CodecID.A_LOG,
|
||||
hci.CodecID.CVSD,
|
||||
hci.CodecID.LINEAR_PCM,
|
||||
]
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_Read_Local_Supported_Codecs_V2_Command_Complete():
|
||||
returned_parameters = (
|
||||
hci.HCI_Read_Local_Supported_Codecs_V2_Command.parse_return_parameters(
|
||||
bytes(
|
||||
[
|
||||
hci.HCI_SUCCESS,
|
||||
3,
|
||||
hci.CodecID.A_LOG,
|
||||
hci.HCI_Read_Local_Supported_Codecs_V2_Command.Transport.BR_EDR_ACL,
|
||||
hci.CodecID.CVSD,
|
||||
hci.HCI_Read_Local_Supported_Codecs_V2_Command.Transport.BR_EDR_SCO,
|
||||
hci.CodecID.LINEAR_PCM,
|
||||
hci.HCI_Read_Local_Supported_Codecs_V2_Command.Transport.LE_CIS,
|
||||
0,
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
assert returned_parameters.standard_codec_ids == [
|
||||
hci.CodecID.A_LOG,
|
||||
hci.CodecID.CVSD,
|
||||
hci.CodecID.LINEAR_PCM,
|
||||
]
|
||||
assert returned_parameters.standard_codec_transports == [
|
||||
HCI_Read_Local_Supported_Codecs_V2_Command.Transport.BR_EDR_ACL,
|
||||
HCI_Read_Local_Supported_Codecs_V2_Command.Transport.BR_EDR_SCO,
|
||||
HCI_Read_Local_Supported_Codecs_V2_Command.Transport.LE_CIS,
|
||||
hci.HCI_Read_Local_Supported_Codecs_V2_Command.Transport.BR_EDR_ACL,
|
||||
hci.HCI_Read_Local_Supported_Codecs_V2_Command.Transport.BR_EDR_SCO,
|
||||
hci.HCI_Read_Local_Supported_Codecs_V2_Command.Transport.LE_CIS,
|
||||
]
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_address():
|
||||
a = Address('C4:F2:17:1A:1D:BB')
|
||||
a = hci.Address('C4:F2:17:1A:1D:BB')
|
||||
assert not a.is_public
|
||||
assert a.is_random
|
||||
assert a.address_type == Address.RANDOM_DEVICE_ADDRESS
|
||||
assert a.address_type == hci.Address.RANDOM_DEVICE_ADDRESS
|
||||
assert not a.is_resolvable
|
||||
assert not a.is_resolved
|
||||
assert a.is_static
|
||||
@@ -634,7 +619,7 @@ def test_address():
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_custom():
|
||||
data = bytes([0x77, 0x02, 0x01, 0x03])
|
||||
packet = HCI_CustomPacket(data)
|
||||
packet = hci.HCI_CustomPacket(data)
|
||||
assert packet.hci_packet_type == 0x77
|
||||
assert packet.payload == data
|
||||
|
||||
@@ -645,7 +630,7 @@ def test_iso_data_packet():
|
||||
'05616044002ac9f0a193003c00e83b477b00eba8d41dc018bf1a980f0290afe1e7c37652096697'
|
||||
'52b6a535a8df61e22931ef5a36281bc77ed6a3206d984bcdabee6be831c699cb50e2'
|
||||
)
|
||||
packet = HCI_IsoDataPacket.from_bytes(data)
|
||||
packet = hci.HCI_IsoDataPacket.from_bytes(data)
|
||||
assert packet.connection_handle == 0x0061
|
||||
assert packet.packet_status_flag == 0
|
||||
assert packet.pb_flag == 0x02
|
||||
|
||||
Reference in New Issue
Block a user