mirror of
https://github.com/google/bumble.git
synced 2026-05-08 03:58:01 +00:00
formatting and linting automation
Squashed commits: [cd479ba] formatting and linting automation [7fbfabb] formatting and linting automation [c4f9505] fix after rebase [f506ad4] rename job [441d517] update doc (+7 squashed commits) [2e1b416] fix invoke and github action [6ae5bb4] doc for git blame [44b5461] add GitHub action [b07474f] add docs [4cd9a6f] more linter fixes [db71901] wip [540dc88] wip
This commit is contained in:
@@ -16,10 +16,9 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import struct
|
||||
import bitstruct
|
||||
import logging
|
||||
from collections import namedtuple
|
||||
from colors import color
|
||||
import bitstruct
|
||||
|
||||
from .company_ids import COMPANY_IDENTIFIERS
|
||||
from .sdp import (
|
||||
@@ -134,14 +133,15 @@ MPEG_2_4_OBJECT_TYPE_NAMES = {
|
||||
# -----------------------------------------------------------------------------
|
||||
def flags_to_list(flags, values):
|
||||
result = []
|
||||
for i in range(len(values)):
|
||||
for i, value in enumerate(values):
|
||||
if flags & (1 << (len(values) - i - 1)):
|
||||
result.append(values[i])
|
||||
result.append(value)
|
||||
return result
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def make_audio_source_service_sdp_records(service_record_handle, version=(1, 3)):
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from .avdtp import AVDTP_PSM
|
||||
|
||||
version_int = version[0] << 8 | version[1]
|
||||
@@ -191,6 +191,7 @@ def make_audio_source_service_sdp_records(service_record_handle, version=(1, 3))
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def make_audio_sink_service_sdp_records(service_record_handle, version=(1, 3)):
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from .avdtp import AVDTP_PSM
|
||||
|
||||
version_int = version[0] << 8 | version[1]
|
||||
@@ -331,6 +332,7 @@ class SbcMediaCodecInformation(
|
||||
channel_modes = ['MONO', 'DUAL_CHANNEL', 'STEREO', 'JOINT_STEREO']
|
||||
allocation_methods = ['SNR', 'Loudness']
|
||||
return '\n'.join(
|
||||
# pylint: disable=line-too-long
|
||||
[
|
||||
'SbcMediaCodecInformation(',
|
||||
f' sampling_frequency: {",".join([str(x) for x in flags_to_list(self.sampling_frequency, SBC_SAMPLING_FREQUENCIES)])}',
|
||||
@@ -423,6 +425,7 @@ class AacMediaCodecInformation(
|
||||
'[7]',
|
||||
]
|
||||
channels = [1, 2]
|
||||
# pylint: disable=line-too-long
|
||||
return '\n'.join(
|
||||
[
|
||||
'AacMediaCodecInformation(',
|
||||
@@ -455,6 +458,7 @@ class VendorSpecificMediaCodecInformation:
|
||||
return struct.pack('<IH', self.vendor_id, self.codec_id, self.value)
|
||||
|
||||
def __str__(self):
|
||||
# pylint: disable=line-too-long
|
||||
return '\n'.join(
|
||||
[
|
||||
'VendorSpecificMediaCodecInformation(',
|
||||
@@ -489,7 +493,13 @@ class SbcFrame:
|
||||
return self.sample_count / self.sampling_frequency
|
||||
|
||||
def __str__(self):
|
||||
return f'SBC(sf={self.sampling_frequency},cm={self.channel_mode},br={self.bitrate},sc={self.sample_count},size={len(self.payload)})'
|
||||
return (
|
||||
f'SBC(sf={self.sampling_frequency},'
|
||||
f'cm={self.channel_mode},'
|
||||
f'br={self.bitrate},'
|
||||
f'sc={self.sample_count},'
|
||||
f'size={len(self.payload)})'
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -551,6 +561,7 @@ class SbcPacketSource:
|
||||
@property
|
||||
def packets(self):
|
||||
async def generate_packets():
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from .avdtp import MediaPacket # Import here to avoid a circular reference
|
||||
|
||||
sequence_number = 0
|
||||
@@ -582,7 +593,7 @@ class SbcPacketSource:
|
||||
|
||||
# Prepare for next packets
|
||||
sequence_number += 1
|
||||
timestamp += sum([frame.sample_count for frame in frames])
|
||||
timestamp += sum((frame.sample_count for frame in frames))
|
||||
frames = [frame]
|
||||
frames_size = len(frame.payload)
|
||||
else:
|
||||
|
||||
@@ -22,16 +22,19 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import struct
|
||||
from colors import color
|
||||
from pyee import EventEmitter
|
||||
|
||||
from .core import *
|
||||
from .hci import *
|
||||
from bumble.core import UUID, name_or_number
|
||||
from bumble.hci import HCI_Object, key_with_value
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
# fmt: off
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
ATT_CID = 0x04
|
||||
|
||||
@@ -165,21 +168,14 @@ ATT_ERROR_NAMES = {
|
||||
ATT_DEFAULT_MTU = 23
|
||||
|
||||
HANDLE_FIELD_SPEC = {'size': 2, 'mapper': lambda x: f'0x{x:04X}'}
|
||||
UUID_2_16_FIELD_SPEC = lambda x, y: UUID.parse_uuid(x, y) # noqa: E731
|
||||
# pylint: disable-next=unnecessary-lambda-assignment,unnecessary-lambda
|
||||
UUID_2_16_FIELD_SPEC = lambda x, y: UUID.parse_uuid(x, y)
|
||||
# pylint: disable-next=unnecessary-lambda-assignment,unnecessary-lambda
|
||||
UUID_2_FIELD_SPEC = lambda x, y: UUID.parse_uuid_2(x, y) # noqa: E731
|
||||
|
||||
# fmt: on
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Utils
|
||||
# -----------------------------------------------------------------------------
|
||||
def key_with_value(dictionary, target_value):
|
||||
for key, value in dictionary.items():
|
||||
if value == target_value:
|
||||
return key
|
||||
return None
|
||||
|
||||
# pylint: enable=line-too-long
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Exceptions
|
||||
@@ -203,6 +199,7 @@ class ATT_PDU:
|
||||
|
||||
pdu_classes = {}
|
||||
op_code = 0
|
||||
name = None
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(pdu):
|
||||
@@ -731,15 +728,15 @@ class Attribute(EventEmitter):
|
||||
self.permissions = permissions
|
||||
|
||||
# Convert the type to a UUID object if it isn't already
|
||||
if type(attribute_type) is str:
|
||||
if isinstance(attribute_type, str):
|
||||
self.type = UUID(attribute_type)
|
||||
elif type(attribute_type) is bytes:
|
||||
elif isinstance(attribute_type, bytes):
|
||||
self.type = UUID.from_bytes(attribute_type)
|
||||
else:
|
||||
self.type = attribute_type
|
||||
|
||||
# Convert the value to a byte array
|
||||
if type(value) is str:
|
||||
if isinstance(value, str):
|
||||
self.value = bytes(value, 'utf-8')
|
||||
else:
|
||||
self.value = value
|
||||
@@ -753,9 +750,11 @@ class Attribute(EventEmitter):
|
||||
def read_value(self, connection):
|
||||
if read := getattr(self.value, 'read', None):
|
||||
try:
|
||||
value = read(connection)
|
||||
value = read(connection) # pylint: disable=not-callable
|
||||
except ATT_Error as error:
|
||||
raise ATT_Error(error_code=error.error_code, att_handle=self.handle)
|
||||
raise ATT_Error(
|
||||
error_code=error.error_code, att_handle=self.handle
|
||||
) from error
|
||||
else:
|
||||
value = self.value
|
||||
|
||||
@@ -766,16 +765,18 @@ class Attribute(EventEmitter):
|
||||
|
||||
if write := getattr(self.value, 'write', None):
|
||||
try:
|
||||
write(connection, value)
|
||||
write(connection, value) # pylint: disable=not-callable
|
||||
except ATT_Error as error:
|
||||
raise ATT_Error(error_code=error.error_code, att_handle=self.handle)
|
||||
raise ATT_Error(
|
||||
error_code=error.error_code, att_handle=self.handle
|
||||
) from error
|
||||
else:
|
||||
self.value = value
|
||||
|
||||
self.emit('write', connection, value)
|
||||
|
||||
def __repr__(self):
|
||||
if type(self.value) is bytes:
|
||||
if isinstance(self.value, bytes):
|
||||
value_str = self.value.hex()
|
||||
else:
|
||||
value_str = str(self.value)
|
||||
@@ -783,4 +784,8 @@ class Attribute(EventEmitter):
|
||||
value_string = f', value={self.value.hex()}'
|
||||
else:
|
||||
value_string = ''
|
||||
return f'Attribute(handle=0x{self.handle:04X}, type={self.type}, permissions={self.permissions}{value_string})'
|
||||
return (
|
||||
f'Attribute(handle=0x{self.handle:04X}, '
|
||||
f'type={self.type}, '
|
||||
f'permissions={self.permissions}{value_string})'
|
||||
)
|
||||
|
||||
223
bumble/avdtp.py
223
bumble/avdtp.py
@@ -49,6 +49,7 @@ logger = logging.getLogger(__name__)
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
# fmt: off
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
AVDTP_PSM = 0x0019
|
||||
|
||||
@@ -198,6 +199,8 @@ AVDTP_STATE_NAMES = {
|
||||
}
|
||||
|
||||
# fmt: on
|
||||
# pylint: enable=line-too-long
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -318,7 +321,18 @@ class MediaPacket:
|
||||
return header + self.payload
|
||||
|
||||
def __str__(self):
|
||||
return f'RTP(v={self.version},p={self.padding},x={self.extension},m={self.marker},pt={self.payload_type},sn={self.sequence_number},ts={self.timestamp},ssrc={self.ssrc},csrcs={self.csrc_list},payload_size={len(self.payload)})'
|
||||
return (
|
||||
f'RTP(v={self.version},'
|
||||
f'p={self.padding},'
|
||||
f'x={self.extension},'
|
||||
f'm={self.marker},'
|
||||
f'pt={self.payload_type},'
|
||||
f'sn={self.sequence_number},'
|
||||
f'ts={self.timestamp},'
|
||||
f'ssrc={self.ssrc},'
|
||||
f'csrcs={self.csrc_list},'
|
||||
f'payload_size={len(self.payload)})'
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -369,7 +383,7 @@ class MediaPacketPump:
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class MessageAssembler:
|
||||
class MessageAssembler: # pylint: disable=attribute-defined-outside-init
|
||||
def __init__(self, callback):
|
||||
self.callback = callback
|
||||
self.reset()
|
||||
@@ -390,16 +404,16 @@ class MessageAssembler:
|
||||
message_type = pdu[0] & 3
|
||||
|
||||
logger.debug(
|
||||
f'transaction_label={transaction_label}, packet_type={Protocol.packet_type_name(packet_type)}, message_type={Message.message_type_name(message_type)}'
|
||||
f'transaction_label={transaction_label}, '
|
||||
f'packet_type={Protocol.packet_type_name(packet_type)}, '
|
||||
f'message_type={Message.message_type_name(message_type)}'
|
||||
)
|
||||
if (
|
||||
packet_type == Protocol.SINGLE_PACKET
|
||||
or packet_type == Protocol.START_PACKET
|
||||
):
|
||||
if packet_type in (Protocol.SINGLE_PACKET, Protocol.START_PACKET):
|
||||
if self.message is not None:
|
||||
# The previous message has not been terminated
|
||||
logger.warning(
|
||||
'received a start or single packet when expecting an end or continuation'
|
||||
'received a start or single packet when expecting an end or '
|
||||
'continuation'
|
||||
)
|
||||
self.reset()
|
||||
|
||||
@@ -413,23 +427,22 @@ class MessageAssembler:
|
||||
else:
|
||||
self.number_of_signal_packets = pdu[2]
|
||||
self.message = pdu[3:]
|
||||
elif (
|
||||
packet_type == Protocol.CONTINUE_PACKET
|
||||
or packet_type == Protocol.END_PACKET
|
||||
):
|
||||
elif packet_type in (Protocol.CONTINUE_PACKET, Protocol.END_PACKET):
|
||||
if self.packet_count == 0:
|
||||
logger.warning('unexpected continuation')
|
||||
return
|
||||
|
||||
if transaction_label != self.transaction_label:
|
||||
logger.warning(
|
||||
f'transaction label mismatch: expected {self.transaction_label}, received {transaction_label}'
|
||||
f'transaction label mismatch: expected {self.transaction_label}, '
|
||||
f'received {transaction_label}'
|
||||
)
|
||||
return
|
||||
|
||||
if message_type != self.message_type:
|
||||
logger.warning(
|
||||
f'message type mismatch: expected {self.message_type}, received {message_type}'
|
||||
f'message type mismatch: expected {self.message_type}, '
|
||||
f'received {message_type}'
|
||||
)
|
||||
return
|
||||
|
||||
@@ -438,7 +451,9 @@ class MessageAssembler:
|
||||
if packet_type == Protocol.END_PACKET:
|
||||
if self.packet_count != self.number_of_signal_packets:
|
||||
logger.warning(
|
||||
f'incomplete fragmented message: expected {self.number_of_signal_packets} packets, received {self.packet_count}'
|
||||
'incomplete fragmented message: '
|
||||
f'expected {self.number_of_signal_packets} packets, '
|
||||
f'received {self.packet_count}'
|
||||
)
|
||||
self.reset()
|
||||
return
|
||||
@@ -447,7 +462,9 @@ class MessageAssembler:
|
||||
else:
|
||||
if self.packet_count > self.number_of_signal_packets:
|
||||
logger.warning(
|
||||
f'too many packets: expected {self.number_of_signal_packets}, received {self.packet_count}'
|
||||
'too many packets: '
|
||||
f'expected {self.number_of_signal_packets}, '
|
||||
f'received {self.packet_count}'
|
||||
)
|
||||
self.reset()
|
||||
return
|
||||
@@ -515,7 +532,7 @@ class ServiceCapabilities:
|
||||
self.service_category = service_category
|
||||
self.service_capabilities_bytes = service_capabilities_bytes
|
||||
|
||||
def to_string(self, details=[]):
|
||||
def to_string(self, details=[]): # pylint: disable=dangerous-default-value
|
||||
attributes = ','.join(
|
||||
[name_or_number(AVDTP_SERVICE_CATEGORY_NAMES, self.service_category)]
|
||||
+ details
|
||||
@@ -562,10 +579,16 @@ class MediaCodecCapabilities(ServiceCapabilities):
|
||||
self.media_codec_information = media_codec_information
|
||||
|
||||
def __str__(self):
|
||||
codec_info = (
|
||||
self.media_codec_information.hex()
|
||||
if isinstance(self.media_codec_information, bytes)
|
||||
else str(self.media_codec_information)
|
||||
)
|
||||
|
||||
details = [
|
||||
f'media_type={name_or_number(AVDTP_MEDIA_TYPE_NAMES, self.media_type)}',
|
||||
f'codec={name_or_number(A2DP_CODEC_TYPE_NAMES, self.media_codec_type)}',
|
||||
f'codec_info={self.media_codec_information.hex() if type(self.media_codec_information) is bytes else str(self.media_codec_information)}',
|
||||
f'codec_info={codec_info}',
|
||||
]
|
||||
return self.to_string(details)
|
||||
|
||||
@@ -591,7 +614,7 @@ class EndPointInfo:
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Message:
|
||||
class Message: # pylint:disable=attribute-defined-outside-init
|
||||
COMMAND = 0
|
||||
GENERAL_REJECT = 1
|
||||
RESPONSE_ACCEPT = 2
|
||||
@@ -611,11 +634,11 @@ class Message:
|
||||
return name_or_number(Message.MESSAGE_TYPE_NAMES, message_type)
|
||||
|
||||
@staticmethod
|
||||
def subclass(cls):
|
||||
def subclass(subclass):
|
||||
# Infer the signal identifier and message subtype from the class name
|
||||
name = cls.__name__
|
||||
name = subclass.__name__
|
||||
if name == 'General_Reject':
|
||||
cls.signal_identifier = 0
|
||||
subclass.signal_identifier = 0
|
||||
signal_identifier_str = None
|
||||
message_type = Message.COMMAND
|
||||
elif name.endswith('_Command'):
|
||||
@@ -630,22 +653,23 @@ class Message:
|
||||
else:
|
||||
raise ValueError('invalid class name')
|
||||
|
||||
cls.message_type = message_type
|
||||
subclass.message_type = message_type
|
||||
|
||||
if signal_identifier_str is not None:
|
||||
for (name, signal_identifier) in AVDTP_SIGNAL_IDENTIFIERS.items():
|
||||
if name.lower().endswith(signal_identifier_str.lower()):
|
||||
cls.signal_identifier = signal_identifier
|
||||
subclass.signal_identifier = signal_identifier
|
||||
break
|
||||
|
||||
# Register the subclass
|
||||
Message.subclasses.setdefault(cls.signal_identifier, {})[
|
||||
cls.message_type
|
||||
] = cls
|
||||
Message.subclasses.setdefault(subclass.signal_identifier, {})[
|
||||
subclass.message_type
|
||||
] = subclass
|
||||
|
||||
return cls
|
||||
return subclass
|
||||
|
||||
# Factory method to create a subclass based on the signal identifier and message type
|
||||
# Factory method to create a subclass based on the signal identifier and message
|
||||
# type
|
||||
@staticmethod
|
||||
def create(signal_identifier, message_type, payload):
|
||||
# Look for a registered subclass
|
||||
@@ -676,18 +700,23 @@ class Message:
|
||||
self.payload = payload
|
||||
|
||||
def to_string(self, details):
|
||||
base = f'{color(f"{name_or_number(AVDTP_SIGNAL_NAMES, self.signal_identifier)}_{Message.message_type_name(self.message_type)}", "yellow")}'
|
||||
base = color(
|
||||
f'{name_or_number(AVDTP_SIGNAL_NAMES, self.signal_identifier)}_'
|
||||
f'{Message.message_type_name(self.message_type)}',
|
||||
'yellow',
|
||||
)
|
||||
|
||||
if details:
|
||||
if type(details) is str:
|
||||
if isinstance(details, str):
|
||||
return f'{base}: {details}'
|
||||
else:
|
||||
return (
|
||||
base
|
||||
+ ':\n'
|
||||
+ '\n'.join([' ' + color(detail, 'cyan') for detail in details])
|
||||
)
|
||||
else:
|
||||
return base
|
||||
|
||||
return (
|
||||
base
|
||||
+ ':\n'
|
||||
+ '\n'.join([' ' + color(detail, 'cyan') for detail in details])
|
||||
)
|
||||
|
||||
return base
|
||||
|
||||
def __str__(self):
|
||||
return self.to_string(self.payload.hex())
|
||||
@@ -703,8 +732,8 @@ class Simple_Command(Message):
|
||||
self.acp_seid = self.payload[0] >> 2
|
||||
|
||||
def __init__(self, seid):
|
||||
super().__init__(payload=bytes([seid << 2]))
|
||||
self.acp_seid = seid
|
||||
self.payload = bytes([seid << 2])
|
||||
|
||||
def __str__(self):
|
||||
return self.to_string([f'ACP SEID: {self.acp_seid}'])
|
||||
@@ -720,8 +749,8 @@ class Simple_Reject(Message):
|
||||
self.error_code = self.payload[0]
|
||||
|
||||
def __init__(self, error_code):
|
||||
super().__init__(payload=bytes([error_code]))
|
||||
self.error_code = error_code
|
||||
self.payload = bytes([self.error_code])
|
||||
|
||||
def __str__(self):
|
||||
details = [f'error_code: {name_or_number(AVDTP_ERROR_NAMES, self.error_code)}']
|
||||
@@ -752,13 +781,14 @@ class Discover_Response(Message):
|
||||
)
|
||||
|
||||
def __init__(self, endpoints):
|
||||
super().__init__(payload=b''.join([bytes(endpoint) for endpoint in endpoints]))
|
||||
self.endpoints = endpoints
|
||||
self.payload = b''.join([bytes(endpoint) for endpoint in endpoints])
|
||||
|
||||
def __str__(self):
|
||||
details = []
|
||||
for endpoint in self.endpoints:
|
||||
details.extend(
|
||||
# pylint: disable=line-too-long
|
||||
[
|
||||
f'ACP SEID: {endpoint.seid}',
|
||||
f' in_use: {endpoint.in_use}',
|
||||
@@ -788,8 +818,10 @@ class Get_Capabilities_Response(Message):
|
||||
self.capabilities = ServiceCapabilities.parse_capabilities(self.payload)
|
||||
|
||||
def __init__(self, capabilities):
|
||||
super().__init__(
|
||||
payload=ServiceCapabilities.serialize_capabilities(capabilities)
|
||||
)
|
||||
self.capabilities = capabilities
|
||||
self.payload = ServiceCapabilities.serialize_capabilities(capabilities)
|
||||
|
||||
def __str__(self):
|
||||
details = [str(capability) for capability in self.capabilities]
|
||||
@@ -841,12 +873,13 @@ class Set_Configuration_Command(Message):
|
||||
self.capabilities = ServiceCapabilities.parse_capabilities(self.payload[2:])
|
||||
|
||||
def __init__(self, acp_seid, int_seid, capabilities):
|
||||
super().__init__(
|
||||
payload=bytes([acp_seid << 2, int_seid << 2])
|
||||
+ ServiceCapabilities.serialize_capabilities(capabilities)
|
||||
)
|
||||
self.acp_seid = acp_seid
|
||||
self.int_seid = int_seid
|
||||
self.capabilities = capabilities
|
||||
self.payload = bytes(
|
||||
[acp_seid << 2, int_seid << 2]
|
||||
) + ServiceCapabilities.serialize_capabilities(capabilities)
|
||||
|
||||
def __str__(self):
|
||||
details = [f'ACP SEID: {self.acp_seid}', f'INT SEID: {self.int_seid}'] + [
|
||||
@@ -875,14 +908,20 @@ class Set_Configuration_Reject(Message):
|
||||
self.error_code = self.payload[1]
|
||||
|
||||
def __init__(self, service_category, error_code):
|
||||
super().__init__(payload=bytes([service_category, error_code]))
|
||||
self.service_category = service_category
|
||||
self.error_code = error_code
|
||||
self.payload = bytes([service_category, self.error_code])
|
||||
|
||||
def __str__(self):
|
||||
details = [
|
||||
f'service_category: {name_or_number(AVDTP_SERVICE_CATEGORY_NAMES, self.service_category)}',
|
||||
f'error_code: {name_or_number(AVDTP_ERROR_NAMES, self.error_code)}',
|
||||
(
|
||||
'service_category: '
|
||||
f'{name_or_number(AVDTP_SERVICE_CATEGORY_NAMES, self.service_category)}'
|
||||
),
|
||||
(
|
||||
'error_code: '
|
||||
f'{name_or_number(AVDTP_ERROR_NAMES, self.error_code)}'
|
||||
),
|
||||
]
|
||||
return self.to_string(details)
|
||||
|
||||
@@ -906,8 +945,10 @@ class Get_Configuration_Response(Message):
|
||||
self.capabilities = ServiceCapabilities.parse_capabilities(self.payload)
|
||||
|
||||
def __init__(self, capabilities):
|
||||
super().__init__(
|
||||
payload=ServiceCapabilities.serialize_capabilities(capabilities)
|
||||
)
|
||||
self.capabilities = capabilities
|
||||
self.payload = ServiceCapabilities.serialize_capabilities(capabilities)
|
||||
|
||||
def __str__(self):
|
||||
details = [str(capability) for capability in self.capabilities]
|
||||
@@ -930,6 +971,7 @@ class Reconfigure_Command(Message):
|
||||
'''
|
||||
|
||||
def init_from_payload(self):
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
self.acp_seid = self.payload[0] >> 2
|
||||
self.capabilities = ServiceCapabilities.parse_capabilities(self.payload[1:])
|
||||
|
||||
@@ -991,8 +1033,8 @@ class Start_Command(Message):
|
||||
self.acp_seids = [x >> 2 for x in self.payload]
|
||||
|
||||
def __init__(self, seids):
|
||||
super().__init__(payload=bytes([seid << 2 for seid in seids]))
|
||||
self.acp_seids = seids
|
||||
self.payload = bytes([seid << 2 for seid in self.acp_seids])
|
||||
|
||||
def __str__(self):
|
||||
return self.to_string([f'ACP SEIDs: {self.acp_seids}'])
|
||||
@@ -1018,9 +1060,9 @@ class Start_Reject(Message):
|
||||
self.error_code = self.payload[1]
|
||||
|
||||
def __init__(self, acp_seid, error_code):
|
||||
super().__init__(payload=bytes([acp_seid << 2, error_code]))
|
||||
self.acp_seid = acp_seid
|
||||
self.error_code = error_code
|
||||
self.payload = bytes([self.acp_seid << 2, self.error_code])
|
||||
|
||||
def __str__(self):
|
||||
details = [
|
||||
@@ -1126,7 +1168,7 @@ class General_Reject(Message):
|
||||
'''
|
||||
|
||||
def to_string(self, details):
|
||||
return f'{color(f"GENERAL_REJECT", "yellow")}'
|
||||
return color('GENERAL_REJECT', 'yellow')
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -1137,6 +1179,7 @@ class DelayReport_Command(Message):
|
||||
'''
|
||||
|
||||
def init_from_payload(self):
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
self.acp_seid = self.payload[0] >> 2
|
||||
self.delay = (self.payload[1] << 8) | (self.payload[2])
|
||||
|
||||
@@ -1206,9 +1249,11 @@ class Protocol:
|
||||
l2cap_channel.on('open', self.on_l2cap_channel_open)
|
||||
|
||||
def get_local_endpoint_by_seid(self, seid):
|
||||
if seid > 0 and seid <= len(self.local_endpoints):
|
||||
if 0 < seid <= len(self.local_endpoints):
|
||||
return self.local_endpoints[seid - 1]
|
||||
|
||||
return None
|
||||
|
||||
def add_source(self, codec_capabilities, packet_pump):
|
||||
seid = len(self.local_endpoints) + 1
|
||||
source = LocalSource(self, seid, codec_capabilities, packet_pump)
|
||||
@@ -1288,12 +1333,15 @@ class Protocol:
|
||||
if has_media_transport and has_codec:
|
||||
return endpoint
|
||||
|
||||
return None
|
||||
|
||||
def on_pdu(self, pdu):
|
||||
self.message_assembler.on_pdu(pdu)
|
||||
|
||||
def on_message(self, transaction_label, message):
|
||||
logger.debug(
|
||||
f'{color("<<< Received AVDTP message", "magenta")}: [{transaction_label}] {message}'
|
||||
f'{color("<<< Received AVDTP message", "magenta")}: '
|
||||
f'[{transaction_label}] {message}'
|
||||
)
|
||||
|
||||
# Check that the identifier is not reserved
|
||||
@@ -1311,7 +1359,12 @@ class Protocol:
|
||||
|
||||
if message.message_type == Message.COMMAND:
|
||||
# Command
|
||||
handler_name = f'on_{AVDTP_SIGNAL_NAMES.get(message.signal_identifier,"").replace("AVDTP_","").lower()}_command'
|
||||
signal_name = (
|
||||
AVDTP_SIGNAL_NAMES.get(message.signal_identifier, "")
|
||||
.replace("AVDTP_", "")
|
||||
.lower()
|
||||
)
|
||||
handler_name = f'on_{signal_name}_command'
|
||||
handler = getattr(self, handler_name, None)
|
||||
if handler:
|
||||
try:
|
||||
@@ -1344,7 +1397,8 @@ class Protocol:
|
||||
|
||||
def send_message(self, transaction_label, message):
|
||||
logger.debug(
|
||||
f'{color(">>> Sending AVDTP message", "magenta")}: [{transaction_label}] {message}'
|
||||
f'{color(">>> Sending AVDTP message", "magenta")}: '
|
||||
f'[{transaction_label}] {message}'
|
||||
)
|
||||
max_fragment_size = (
|
||||
self.l2cap_channel.mtu - 3
|
||||
@@ -1398,10 +1452,7 @@ class Protocol:
|
||||
response = await transaction_result
|
||||
|
||||
# Check for errors
|
||||
if (
|
||||
response.message_type == Message.GENERAL_REJECT
|
||||
or response.message_type == Message.RESPONSE_REJECT
|
||||
):
|
||||
if response.message_type in (Message.GENERAL_REJECT, Message.RESPONSE_REJECT):
|
||||
raise ProtocolError(response.error_code, 'avdtp')
|
||||
|
||||
return response
|
||||
@@ -1424,8 +1475,8 @@ class Protocol:
|
||||
async def get_capabilities(self, seid):
|
||||
if self.version > (1, 2):
|
||||
return await self.send_command(Get_All_Capabilities_Command(seid))
|
||||
else:
|
||||
return await self.send_command(Get_Capabilities_Command(seid))
|
||||
|
||||
return await self.send_command(Get_Capabilities_Command(seid))
|
||||
|
||||
async def set_configuration(self, acp_seid, int_seid, capabilities):
|
||||
return await self.send_command(
|
||||
@@ -1451,7 +1502,7 @@ class Protocol:
|
||||
async def abort(self, seid):
|
||||
return await self.send_command(Abort_Command(seid))
|
||||
|
||||
def on_discover_command(self, command):
|
||||
def on_discover_command(self, _command):
|
||||
endpoint_infos = [
|
||||
EndPointInfo(endpoint.seid, 0, endpoint.media_type, endpoint.tsep)
|
||||
for endpoint in self.local_endpoints
|
||||
@@ -1689,7 +1740,7 @@ class Stream:
|
||||
self.change_state(AVDTP_OPEN_STATE)
|
||||
|
||||
async def close(self):
|
||||
if self.state not in {AVDTP_OPEN_STATE, AVDTP_STREAMING_STATE}:
|
||||
if self.state not in (AVDTP_OPEN_STATE, AVDTP_STREAMING_STATE):
|
||||
raise InvalidStateError('current state is not OPEN or STREAMING')
|
||||
|
||||
logger.debug('closing local endpoint')
|
||||
@@ -1718,13 +1769,14 @@ class Stream:
|
||||
return result
|
||||
|
||||
self.change_state(AVDTP_CONFIGURED_STATE)
|
||||
return None
|
||||
|
||||
def on_get_configuration_command(self, configuration):
|
||||
if self.state not in {
|
||||
if self.state not in (
|
||||
AVDTP_CONFIGURED_STATE,
|
||||
AVDTP_OPEN_STATE,
|
||||
AVDTP_STREAMING_STATE,
|
||||
}:
|
||||
):
|
||||
return Get_Configuration_Reject(AVDTP_BAD_STATE_ERROR)
|
||||
|
||||
return self.local_endpoint.on_get_configuration_command(configuration)
|
||||
@@ -1737,6 +1789,8 @@ class Stream:
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
return None
|
||||
|
||||
def on_open_command(self):
|
||||
if self.state != AVDTP_CONFIGURED_STATE:
|
||||
return Open_Reject(AVDTP_BAD_STATE_ERROR)
|
||||
@@ -1749,6 +1803,7 @@ class Stream:
|
||||
self.protocol.channel_acceptor = self
|
||||
|
||||
self.change_state(AVDTP_OPEN_STATE)
|
||||
return None
|
||||
|
||||
def on_start_command(self):
|
||||
if self.state != AVDTP_OPEN_STATE:
|
||||
@@ -1764,6 +1819,7 @@ class Stream:
|
||||
return result
|
||||
|
||||
self.change_state(AVDTP_STREAMING_STATE)
|
||||
return None
|
||||
|
||||
def on_suspend_command(self):
|
||||
if self.state != AVDTP_STREAMING_STATE:
|
||||
@@ -1774,9 +1830,10 @@ class Stream:
|
||||
return result
|
||||
|
||||
self.change_state(AVDTP_OPEN_STATE)
|
||||
return None
|
||||
|
||||
def on_close_command(self):
|
||||
if self.state not in {AVDTP_OPEN_STATE, AVDTP_STREAMING_STATE}:
|
||||
if self.state not in (AVDTP_OPEN_STATE, AVDTP_STREAMING_STATE):
|
||||
return Open_Reject(AVDTP_BAD_STATE_ERROR)
|
||||
|
||||
result = self.local_endpoint.on_close_command()
|
||||
@@ -1792,6 +1849,8 @@ class Stream:
|
||||
# TODO: set a timer as we wait for the RTP channel to be closed
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def on_abort_command(self):
|
||||
if self.rtp_channel is None:
|
||||
# No need to wait
|
||||
@@ -1819,7 +1878,7 @@ class Stream:
|
||||
self.local_endpoint.in_use = 0
|
||||
self.rtp_channel = None
|
||||
|
||||
if self.state in {AVDTP_CLOSING_STATE, AVDTP_ABORTING_STATE}:
|
||||
if self.state in (AVDTP_CLOSING_STATE, AVDTP_ABORTING_STATE):
|
||||
self.change_state(AVDTP_IDLE_STATE)
|
||||
else:
|
||||
logger.warning('unexpected channel close while not CLOSING or ABORTING')
|
||||
@@ -1839,7 +1898,10 @@ class Stream:
|
||||
local_endpoint.in_use = 1
|
||||
|
||||
def __str__(self):
|
||||
return f'Stream({self.local_endpoint.seid} -> {self.remote_endpoint.seid} {self.state_name(self.state)})'
|
||||
return (
|
||||
f'Stream({self.local_endpoint.seid} -> '
|
||||
f'{self.remote_endpoint.seid} {self.state_name(self.state)})'
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -1852,12 +1914,14 @@ class StreamEndPoint:
|
||||
self.capabilities = capabilities
|
||||
|
||||
def __str__(self):
|
||||
media_type = f'{name_or_number(AVDTP_MEDIA_TYPE_NAMES, self.media_type)}'
|
||||
tsep = f'{name_or_number(AVDTP_TSEP_NAMES, self.tsep)}'
|
||||
return '\n'.join(
|
||||
[
|
||||
'SEP(',
|
||||
f' seid={self.seid}',
|
||||
f' media_type={name_or_number(AVDTP_MEDIA_TYPE_NAMES, self.media_type)}',
|
||||
f' tsep={name_or_number(AVDTP_TSEP_NAMES, self.tsep)}',
|
||||
f' media_type={media_type}',
|
||||
f' tsep={tsep}',
|
||||
f' in_use={self.in_use}',
|
||||
' capabilities=[',
|
||||
'\n'.join([f' {x}' for x in self.capabilities]),
|
||||
@@ -1902,11 +1966,11 @@ class DiscoveredStreamEndPoint(StreamEndPoint, StreamEndPointProxy):
|
||||
# -----------------------------------------------------------------------------
|
||||
class LocalStreamEndPoint(StreamEndPoint):
|
||||
def __init__(
|
||||
self, protocol, seid, media_type, tsep, capabilities, configuration=[]
|
||||
self, protocol, seid, media_type, tsep, capabilities, configuration=None
|
||||
):
|
||||
super().__init__(seid, media_type, tsep, 0, capabilities)
|
||||
self.protocol = protocol
|
||||
self.configuration = configuration
|
||||
self.configuration = configuration if configuration is not None else []
|
||||
self.stream = None
|
||||
|
||||
async def start(self):
|
||||
@@ -1968,14 +2032,14 @@ class LocalSource(LocalStreamEndPoint, EventEmitter):
|
||||
async def start(self):
|
||||
if self.packet_pump:
|
||||
return await self.packet_pump.start(self.stream.rtp_channel)
|
||||
else:
|
||||
self.emit('start', self.stream.rtp_channel)
|
||||
|
||||
self.emit('start', self.stream.rtp_channel)
|
||||
|
||||
async def stop(self):
|
||||
if self.packet_pump:
|
||||
return await self.packet_pump.stop()
|
||||
else:
|
||||
self.emit('stop')
|
||||
|
||||
self.emit('stop')
|
||||
|
||||
def on_set_configuration_command(self, configuration):
|
||||
# For now, blindly accept the configuration
|
||||
@@ -2018,6 +2082,7 @@ class LocalSink(LocalStreamEndPoint, EventEmitter):
|
||||
def on_avdtp_packet(self, packet):
|
||||
rtp_packet = MediaPacket.from_bytes(packet)
|
||||
logger.debug(
|
||||
f'{color("<<< RTP Packet:", "green")} {rtp_packet} {rtp_packet.payload[:16].hex()}'
|
||||
f'{color("<<< RTP Packet:", "green")} '
|
||||
f'{rtp_packet} {rtp_packet.payload[:16].hex()}'
|
||||
)
|
||||
self.emit('rtp_packet', rtp_packet)
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
# the `generate_company_id_list.py` script
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# pylint: disable=line-too-long
|
||||
COMPANY_IDENTIFIERS = {
|
||||
0x0000: "Ericsson Technology Licensing",
|
||||
0x0001: "Nokia Mobile Phones",
|
||||
@@ -196,28 +197,28 @@ COMPANY_IDENTIFIERS = {
|
||||
0x00AF: "Cinetix",
|
||||
0x00B0: "Passif Semiconductor Corp",
|
||||
0x00B1: "Saris Cycling Group, Inc",
|
||||
0x00B2: "Bekey A/S",
|
||||
0x00B3: "Clarinox Technologies Pty. Ltd.",
|
||||
0x00B4: "BDE Technology Co., Ltd.",
|
||||
0x00B2: "Bekey A/S",
|
||||
0x00B3: "Clarinox Technologies Pty. Ltd.",
|
||||
0x00B4: "BDE Technology Co., Ltd.",
|
||||
0x00B5: "Swirl Networks",
|
||||
0x00B6: "Meso international",
|
||||
0x00B7: "TreLab Ltd",
|
||||
0x00B8: "Qualcomm Innovation Center, Inc. (QuIC)",
|
||||
0x00B9: "Johnson Controls, Inc.",
|
||||
0x00BA: "Starkey Laboratories Inc.",
|
||||
0x00BB: "S-Power Electronics Limited",
|
||||
0x00BC: "Ace Sensor Inc",
|
||||
0x00BD: "Aplix Corporation",
|
||||
0x00BE: "AAMP of America",
|
||||
0x00BF: "Stalmart Technology Limited",
|
||||
0x00C0: "AMICCOM Electronics Corporation",
|
||||
0x00C1: "Shenzhen Excelsecu Data Technology Co.,Ltd",
|
||||
0x00C2: "Geneq Inc.",
|
||||
0x00C3: "adidas AG",
|
||||
0x00C4: "LG Electronics",
|
||||
0x00C5: "Onset Computer Corporation",
|
||||
0x00C6: "Selfly BV",
|
||||
0x00C7: "Quuppa Oy.",
|
||||
0x00B6: "Meso international",
|
||||
0x00B7: "TreLab Ltd",
|
||||
0x00B8: "Qualcomm Innovation Center, Inc. (QuIC)",
|
||||
0x00B9: "Johnson Controls, Inc.",
|
||||
0x00BA: "Starkey Laboratories Inc.",
|
||||
0x00BB: "S-Power Electronics Limited",
|
||||
0x00BC: "Ace Sensor Inc",
|
||||
0x00BD: "Aplix Corporation",
|
||||
0x00BE: "AAMP of America",
|
||||
0x00BF: "Stalmart Technology Limited",
|
||||
0x00C0: "AMICCOM Electronics Corporation",
|
||||
0x00C1: "Shenzhen Excelsecu Data Technology Co.,Ltd",
|
||||
0x00C2: "Geneq Inc.",
|
||||
0x00C3: "adidas AG",
|
||||
0x00C4: "LG Electronics",
|
||||
0x00C5: "Onset Computer Corporation",
|
||||
0x00C6: "Selfly BV",
|
||||
0x00C7: "Quuppa Oy.",
|
||||
0x00C8: "GeLo Inc",
|
||||
0x00C9: "Evluma",
|
||||
0x00CA: "MC10",
|
||||
@@ -249,10 +250,10 @@ COMPANY_IDENTIFIERS = {
|
||||
0x00E4: "Laird Connectivity, Inc. formerly L.S. Research Inc.",
|
||||
0x00E5: "Eden Software Consultants Ltd.",
|
||||
0x00E6: "Freshtemp",
|
||||
0x00E7: "KS Technologies",
|
||||
0x00E8: "ACTS Technologies",
|
||||
0x00E9: "Vtrack Systems",
|
||||
0x00EA: "Nielsen-Kellerman Company",
|
||||
0x00E7: "KS Technologies",
|
||||
0x00E8: "ACTS Technologies",
|
||||
0x00E9: "Vtrack Systems",
|
||||
0x00EA: "Nielsen-Kellerman Company",
|
||||
0x00EB: "Server Technology Inc.",
|
||||
0x00EC: "BioResearch Associates",
|
||||
0x00ED: "Jolly Logic, LLC",
|
||||
|
||||
@@ -19,9 +19,37 @@ import logging
|
||||
import asyncio
|
||||
import itertools
|
||||
import random
|
||||
import struct
|
||||
from colors import color
|
||||
from bumble.core import BT_CENTRAL_ROLE, BT_PERIPHERAL_ROLE
|
||||
|
||||
from bumble.hci import (
|
||||
HCI_ACL_DATA_PACKET,
|
||||
HCI_COMMAND_DISALLOWED_ERROR,
|
||||
HCI_COMMAND_PACKET,
|
||||
HCI_COMMAND_STATUS_PENDING,
|
||||
HCI_CONNECTION_TIMEOUT_ERROR,
|
||||
HCI_EVENT_PACKET,
|
||||
HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR,
|
||||
HCI_LE_1M_PHY,
|
||||
HCI_SUCCESS,
|
||||
HCI_UNKNOWN_HCI_COMMAND_ERROR,
|
||||
HCI_VERSION_BLUETOOTH_CORE_5_0,
|
||||
Address,
|
||||
HCI_AclDataPacket,
|
||||
HCI_AclDataPacketAssembler,
|
||||
HCI_Command_Complete_Event,
|
||||
HCI_Command_Status_Event,
|
||||
HCI_Disconnection_Complete_Event,
|
||||
HCI_Encryption_Change_Event,
|
||||
HCI_LE_Advertising_Report_Event,
|
||||
HCI_LE_Connection_Complete_Event,
|
||||
HCI_LE_Read_Remote_Features_Complete_Event,
|
||||
HCI_Number_Of_Completed_Packets_Event,
|
||||
HCI_Object,
|
||||
HCI_Packet,
|
||||
)
|
||||
|
||||
from .hci import *
|
||||
from .l2cap import *
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -83,13 +111,19 @@ class Controller:
|
||||
self.manufacturer_name = 0xFFFF
|
||||
self.hc_le_data_packet_length = 27
|
||||
self.hc_total_num_le_data_packets = 64
|
||||
self.event_mask = 0
|
||||
self.event_mask_page_2 = 0
|
||||
self.supported_commands = bytes.fromhex(
|
||||
'2000800000c000000000e40000002822000000000000040000f7ffff7f00000030f0f9ff01008004000000000000000000000000000000000000000000000000'
|
||||
'2000800000c000000000e40000002822000000000000040000f7ffff7f000000'
|
||||
'30f0f9ff01008004000000000000000000000000000000000000000000000000'
|
||||
)
|
||||
self.le_event_mask = 0
|
||||
self.advertising_parameters = None
|
||||
self.le_features = bytes.fromhex('ff49010000000000')
|
||||
self.le_states = bytes.fromhex('ffff3fffff030000')
|
||||
self.advertising_channel_tx_power = 0
|
||||
self.filter_accept_list_size = 8
|
||||
self.filter_duplicates = False
|
||||
self.resolving_list_size = 8
|
||||
self.supported_max_tx_octets = 27
|
||||
self.supported_max_tx_time = 10000 # microseconds
|
||||
@@ -133,7 +167,8 @@ class Controller:
|
||||
@host.setter
|
||||
def host(self, host):
|
||||
'''
|
||||
Sets the host (sink) for this controller, and set this controller as the controller (sink) for the host
|
||||
Sets the host (sink) for this controller, and set this controller as the
|
||||
controller (sink) for the host
|
||||
'''
|
||||
self.set_packet_sink(host)
|
||||
if host:
|
||||
@@ -151,7 +186,7 @@ class Controller:
|
||||
|
||||
@public_address.setter
|
||||
def public_address(self, address):
|
||||
if type(address) is str:
|
||||
if isinstance(address, str):
|
||||
address = Address(address)
|
||||
self._public_address = address
|
||||
|
||||
@@ -161,7 +196,7 @@ class Controller:
|
||||
|
||||
@random_address.setter
|
||||
def random_address(self, address):
|
||||
if type(address) is str:
|
||||
if isinstance(address, str):
|
||||
address = Address(address)
|
||||
self._random_address = address
|
||||
logger.debug(f'new random address: {address}')
|
||||
@@ -175,7 +210,8 @@ class Controller:
|
||||
|
||||
def on_hci_packet(self, packet):
|
||||
logger.debug(
|
||||
f'{color("<<<", "blue")} [{self.name}] {color("HOST -> CONTROLLER", "blue")}: {packet}'
|
||||
f'{color("<<<", "blue")} [{self.name}] '
|
||||
f'{color("HOST -> CONTROLLER", "blue")}: {packet}'
|
||||
)
|
||||
|
||||
# If the packet is a command, invoke the handler for this packet
|
||||
@@ -192,7 +228,7 @@ class Controller:
|
||||
handler_name = f'on_{command.name.lower()}'
|
||||
handler = getattr(self, handler_name, self.on_hci_command)
|
||||
result = handler(command)
|
||||
if type(result) is bytes:
|
||||
if isinstance(result, bytes):
|
||||
self.send_hci_packet(
|
||||
HCI_Command_Complete_Event(
|
||||
num_hci_command_packets=1,
|
||||
@@ -201,7 +237,7 @@ class Controller:
|
||||
)
|
||||
)
|
||||
|
||||
def on_hci_event_packet(self, event):
|
||||
def on_hci_event_packet(self, _event):
|
||||
logger.warning('!!! unexpected event packet')
|
||||
|
||||
def on_hci_acl_data_packet(self, packet):
|
||||
@@ -218,7 +254,8 @@ class Controller:
|
||||
|
||||
def send_hci_packet(self, packet):
|
||||
logger.debug(
|
||||
f'{color(">>>", "green")} [{self.name}] {color("CONTROLLER -> HOST", "green")}: {packet}'
|
||||
f'{color(">>>", "green")} [{self.name}] '
|
||||
f'{color("CONTROLLER -> HOST", "green")}: {packet}'
|
||||
)
|
||||
if self.host:
|
||||
self.host.on_packet(packet.to_bytes())
|
||||
@@ -312,7 +349,7 @@ class Controller:
|
||||
# Remove the connection
|
||||
del self.peripheral_connections[peer_address]
|
||||
else:
|
||||
logger.warn(f'!!! No peripheral connection found for {peer_address}')
|
||||
logger.warning(f'!!! No peripheral connection found for {peer_address}')
|
||||
|
||||
def on_link_peripheral_connection_complete(
|
||||
self, le_create_connection_command, status
|
||||
@@ -339,6 +376,7 @@ class Controller:
|
||||
|
||||
# Say that the connection has completed
|
||||
self.send_hci_packet(
|
||||
# pylint: disable=line-too-long
|
||||
HCI_LE_Connection_Complete_Event(
|
||||
status=status,
|
||||
connection_handle=connection.handle if connection else 0,
|
||||
@@ -391,9 +429,9 @@ class Controller:
|
||||
# Remove the connection
|
||||
del self.central_connections[peer_address]
|
||||
else:
|
||||
logger.warn(f'!!! No central connection found for {peer_address}')
|
||||
logger.warning(f'!!! No central connection found for {peer_address}')
|
||||
|
||||
def on_link_encrypted(self, peer_address, rand, ediv, ltk):
|
||||
def on_link_encrypted(self, peer_address, _rand, _ediv, _ltk):
|
||||
# For now, just setup the encryption without asking the host
|
||||
if connection := self.find_connection_by_address(peer_address):
|
||||
self.send_hci_packet(
|
||||
@@ -505,7 +543,7 @@ class Controller:
|
||||
command.connection_handle
|
||||
)
|
||||
):
|
||||
logger.warn('connection not found')
|
||||
logger.warning('connection not found')
|
||||
return
|
||||
|
||||
if self.link:
|
||||
@@ -521,7 +559,7 @@ class Controller:
|
||||
self.event_mask = command.event_mask
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_reset_command(self, command):
|
||||
def on_hci_reset_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.3.2 Reset Command
|
||||
'''
|
||||
@@ -543,7 +581,7 @@ class Controller:
|
||||
pass
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_read_local_name_command(self, command):
|
||||
def on_hci_read_local_name_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.3.12 Read Local Name Command
|
||||
'''
|
||||
@@ -553,21 +591,22 @@ class Controller:
|
||||
|
||||
return bytes([HCI_SUCCESS]) + local_name
|
||||
|
||||
def on_hci_read_class_of_device_command(self, command):
|
||||
def on_hci_read_class_of_device_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.3.25 Read Class of Device Command
|
||||
'''
|
||||
return bytes([HCI_SUCCESS, 0, 0, 0])
|
||||
|
||||
def on_hci_write_class_of_device_command(self, command):
|
||||
def on_hci_write_class_of_device_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.3.26 Write Class of Device Command
|
||||
'''
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_read_synchronous_flow_control_enable_command(self, command):
|
||||
def on_hci_read_synchronous_flow_control_enable_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.3.36 Read Synchronous Flow Control Enable Command
|
||||
See Bluetooth spec Vol 2, Part E - 7.3.36 Read Synchronous Flow Control Enable
|
||||
Command
|
||||
'''
|
||||
if self.sync_flow_control:
|
||||
ret = 1
|
||||
@@ -577,7 +616,8 @@ class Controller:
|
||||
|
||||
def on_hci_write_synchronous_flow_control_enable_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.3.37 Write Synchronous Flow Control Enable Command
|
||||
See Bluetooth spec Vol 2, Part E - 7.3.37 Write Synchronous Flow Control Enable
|
||||
Command
|
||||
'''
|
||||
ret = HCI_SUCCESS
|
||||
if command.synchronous_flow_control_enable == 1:
|
||||
@@ -588,7 +628,7 @@ class Controller:
|
||||
ret = HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR
|
||||
return bytes([ret])
|
||||
|
||||
def on_hci_write_simple_pairing_mode_command(self, command):
|
||||
def on_hci_write_simple_pairing_mode_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.3.59 Write Simple Pairing Mode Command
|
||||
'''
|
||||
@@ -601,13 +641,13 @@ class Controller:
|
||||
self.event_mask_page_2 = command.event_mask_page_2
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_read_le_host_support_command(self, command):
|
||||
def on_hci_read_le_host_support_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.3.78 Write LE Host Support Command
|
||||
'''
|
||||
return bytes([HCI_SUCCESS, 1, 0])
|
||||
|
||||
def on_hci_write_le_host_support_command(self, command):
|
||||
def on_hci_write_le_host_support_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.3.79 Write LE Host Support Command
|
||||
'''
|
||||
@@ -616,12 +656,13 @@ class Controller:
|
||||
|
||||
def on_hci_write_authenticated_payload_timeout_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.3.94 Write Authenticated Payload Timeout Command
|
||||
See Bluetooth spec Vol 2, Part E - 7.3.94 Write Authenticated Payload Timeout
|
||||
Command
|
||||
'''
|
||||
# TODO
|
||||
return struct.pack('<BH', HCI_SUCCESS, command.connection_handle)
|
||||
|
||||
def on_hci_read_local_version_information_command(self, command):
|
||||
def on_hci_read_local_version_information_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.4.1 Read Local Version Information Command
|
||||
'''
|
||||
@@ -635,19 +676,19 @@ class Controller:
|
||||
self.lmp_subversion,
|
||||
)
|
||||
|
||||
def on_hci_read_local_supported_commands_command(self, command):
|
||||
def on_hci_read_local_supported_commands_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.4.2 Read Local Supported Commands Command
|
||||
'''
|
||||
return bytes([HCI_SUCCESS]) + self.supported_commands
|
||||
|
||||
def on_hci_read_local_supported_features_command(self, command):
|
||||
def on_hci_read_local_supported_features_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.4.3 Read Local Supported Features Command
|
||||
'''
|
||||
return bytes([HCI_SUCCESS]) + self.lmp_features
|
||||
|
||||
def on_hci_read_bd_addr_command(self, command):
|
||||
def on_hci_read_bd_addr_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.4.6 Read BD_ADDR Command
|
||||
'''
|
||||
@@ -665,7 +706,7 @@ class Controller:
|
||||
self.le_event_mask = command.le_event_mask
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_le_read_buffer_size_command(self, command):
|
||||
def on_hci_le_read_buffer_size_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.2 LE Read Buffer Size Command
|
||||
'''
|
||||
@@ -676,9 +717,10 @@ class Controller:
|
||||
self.hc_total_num_le_data_packets,
|
||||
)
|
||||
|
||||
def on_hci_le_read_local_supported_features_command(self, command):
|
||||
def on_hci_le_read_local_supported_features_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.3 LE Read Local Supported Features Command
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.3 LE Read Local Supported Features
|
||||
Command
|
||||
'''
|
||||
return bytes([HCI_SUCCESS]) + self.le_features
|
||||
|
||||
@@ -696,9 +738,10 @@ class Controller:
|
||||
self.advertising_parameters = command
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_le_read_advertising_channel_tx_power_command(self, command):
|
||||
def on_hci_le_read_advertising_channel_tx_power_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.6 LE Read Advertising Channel Tx Power Command
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.6 LE Read Advertising Channel Tx Power
|
||||
Command
|
||||
'''
|
||||
return bytes([HCI_SUCCESS, self.advertising_channel_tx_power])
|
||||
|
||||
@@ -779,33 +822,36 @@ class Controller:
|
||||
)
|
||||
)
|
||||
|
||||
def on_hci_le_create_connection_cancel_command(self, command):
|
||||
def on_hci_le_create_connection_cancel_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.13 LE Create Connection Cancel Command
|
||||
'''
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_le_read_filter_accept_list_size_command(self, command):
|
||||
def on_hci_le_read_filter_accept_list_size_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.14 LE Read Filter Accept List Size Command
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.14 LE Read Filter Accept List Size
|
||||
Command
|
||||
'''
|
||||
return bytes([HCI_SUCCESS, self.filter_accept_list_size])
|
||||
|
||||
def on_hci_le_clear_filter_accept_list_command(self, command):
|
||||
def on_hci_le_clear_filter_accept_list_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.15 LE Clear Filter Accept List Command
|
||||
'''
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_le_add_device_to_filter_accept_list_command(self, command):
|
||||
def on_hci_le_add_device_to_filter_accept_list_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.16 LE Add Device To Filter Accept List Command
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.16 LE Add Device To Filter Accept List
|
||||
Command
|
||||
'''
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_le_remove_device_from_filter_accept_list_command(self, command):
|
||||
def on_hci_le_remove_device_from_filter_accept_list_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.17 LE Remove Device From Filter Accept List Command
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.17 LE Remove Device From Filter Accept
|
||||
List Command
|
||||
'''
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
@@ -832,7 +878,7 @@ class Controller:
|
||||
)
|
||||
)
|
||||
|
||||
def on_hci_le_rand_command(self, command):
|
||||
def on_hci_le_rand_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.23 LE Rand Command
|
||||
'''
|
||||
@@ -849,7 +895,7 @@ class Controller:
|
||||
command.connection_handle
|
||||
)
|
||||
):
|
||||
logger.warn('connection not found')
|
||||
logger.warning('connection not found')
|
||||
return bytes([HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR])
|
||||
|
||||
# Notify that the connection is now encrypted
|
||||
@@ -869,15 +915,18 @@ class Controller:
|
||||
)
|
||||
)
|
||||
|
||||
def on_hci_le_read_supported_states_command(self, command):
|
||||
return None
|
||||
|
||||
def on_hci_le_read_supported_states_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.27 LE Read Supported States Command
|
||||
'''
|
||||
return bytes([HCI_SUCCESS]) + self.le_states
|
||||
|
||||
def on_hci_le_read_suggested_default_data_length_command(self, command):
|
||||
def on_hci_le_read_suggested_default_data_length_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.34 LE Read Suggested Default Data Length Command
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.34 LE Read Suggested Default Data Length
|
||||
Command
|
||||
'''
|
||||
return struct.pack(
|
||||
'<BHH',
|
||||
@@ -888,33 +937,35 @@ class Controller:
|
||||
|
||||
def on_hci_le_write_suggested_default_data_length_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.35 LE Write Suggested Default Data Length Command
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.35 LE Write Suggested Default Data Length
|
||||
Command
|
||||
'''
|
||||
self.suggested_max_tx_octets, self.suggested_max_tx_time = struct.unpack(
|
||||
'<HH', command.parameters[:4]
|
||||
)
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_le_read_local_p_256_public_key_command(self, command):
|
||||
def on_hci_le_read_local_p_256_public_key_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.36 LE Read P-256 Public Key Command
|
||||
'''
|
||||
# TODO create key and send HCI_LE_Read_Local_P-256_Public_Key_Complete event
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_le_add_device_to_resolving_list_command(self, command):
|
||||
def on_hci_le_add_device_to_resolving_list_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.38 LE Add Device To Resolving List Command
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.38 LE Add Device To Resolving List
|
||||
Command
|
||||
'''
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_le_clear_resolving_list_command(self, command):
|
||||
def on_hci_le_clear_resolving_list_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.40 LE Clear Resolving List Command
|
||||
'''
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_le_read_resolving_list_size_command(self, command):
|
||||
def on_hci_le_read_resolving_list_size_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.41 LE Read Resolving List Size Command
|
||||
'''
|
||||
@@ -922,7 +973,8 @@ class Controller:
|
||||
|
||||
def on_hci_le_set_address_resolution_enable_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.44 LE Set Address Resolution Enable Command
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.44 LE Set Address Resolution Enable
|
||||
Command
|
||||
'''
|
||||
ret = HCI_SUCCESS
|
||||
if command.address_resolution_enable == 1:
|
||||
@@ -935,12 +987,13 @@ class Controller:
|
||||
|
||||
def on_hci_le_set_resolvable_private_address_timeout_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.45 LE Set Resolvable Private Address Timeout Command
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.45 LE Set Resolvable Private Address
|
||||
Timeout Command
|
||||
'''
|
||||
self.le_rpa_timeout = command.rpa_timeout
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_le_read_maximum_data_length_command(self, command):
|
||||
def on_hci_le_read_maximum_data_length_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.46 LE Read Maximum Data Length Command
|
||||
'''
|
||||
|
||||
140
bumble/core.py
140
bumble/core.py
@@ -100,7 +100,7 @@ class ProtocolError(BaseError):
|
||||
"""Protocol Error"""
|
||||
|
||||
|
||||
class TimeoutError(Exception):
|
||||
class TimeoutError(Exception): # pylint: disable=redefined-builtin
|
||||
"""Timeout Error"""
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ class InvalidStateError(Exception):
|
||||
"""Invalid State Error"""
|
||||
|
||||
|
||||
class ConnectionError(BaseError):
|
||||
class ConnectionError(BaseError): # pylint: disable=redefined-builtin
|
||||
"""Connection Error"""
|
||||
|
||||
FAILURE = 0x01
|
||||
@@ -148,7 +148,7 @@ class UUID:
|
||||
UUIDS = [] # Registry of all instances created
|
||||
|
||||
def __init__(self, uuid_str_or_int, name=None):
|
||||
if type(uuid_str_or_int) is int:
|
||||
if isinstance(uuid_str_or_int, int):
|
||||
self.uuid_bytes = struct.pack('<H', uuid_str_or_int)
|
||||
else:
|
||||
if len(uuid_str_or_int) == 36:
|
||||
@@ -168,7 +168,8 @@ class UUID:
|
||||
self.name = name
|
||||
|
||||
def register(self):
|
||||
# Register this object in the class registry, and update the entry's name if it wasn't set already
|
||||
# Register this object in the class registry, and update the entry's name if
|
||||
# it wasn't set already
|
||||
for uuid in self.UUIDS:
|
||||
if self == uuid:
|
||||
if uuid.name is None:
|
||||
@@ -180,14 +181,14 @@ class UUID:
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, uuid_bytes, name=None):
|
||||
if len(uuid_bytes) in {2, 4, 16}:
|
||||
if len(uuid_bytes) in (2, 4, 16):
|
||||
self = cls.__new__(cls)
|
||||
self.uuid_bytes = uuid_bytes
|
||||
self.name = name
|
||||
|
||||
return self.register()
|
||||
else:
|
||||
raise ValueError('only 2, 4 and 16 bytes are allowed')
|
||||
|
||||
raise ValueError('only 2, 4 and 16 bytes are allowed')
|
||||
|
||||
@classmethod
|
||||
def from_16_bits(cls, uuid_16, name=None):
|
||||
@@ -198,20 +199,21 @@ class UUID:
|
||||
return cls.from_bytes(struct.pack('<I', uuid_32), name)
|
||||
|
||||
@classmethod
|
||||
def parse_uuid(cls, bytes, offset):
|
||||
return len(bytes), cls.from_bytes(bytes[offset:])
|
||||
def parse_uuid(cls, uuid_as_bytes, offset):
|
||||
return len(uuid_as_bytes), cls.from_bytes(uuid_as_bytes[offset:])
|
||||
|
||||
@classmethod
|
||||
def parse_uuid_2(cls, bytes, offset):
|
||||
return offset + 2, cls.from_bytes(bytes[offset : offset + 2])
|
||||
def parse_uuid_2(cls, uuid_as_bytes, offset):
|
||||
return offset + 2, cls.from_bytes(uuid_as_bytes[offset : offset + 2])
|
||||
|
||||
def to_bytes(self, force_128=False):
|
||||
if len(self.uuid_bytes) == 16 or not force_128:
|
||||
return self.uuid_bytes
|
||||
elif len(self.uuid_bytes) == 4:
|
||||
|
||||
if len(self.uuid_bytes) == 4:
|
||||
return self.uuid_bytes + UUID.BASE_UUID
|
||||
else:
|
||||
return self.uuid_bytes + bytes([0, 0]) + UUID.BASE_UUID
|
||||
|
||||
return self.uuid_bytes + bytes([0, 0]) + UUID.BASE_UUID
|
||||
|
||||
def to_pdu_bytes(self):
|
||||
'''
|
||||
@@ -225,16 +227,16 @@ class UUID:
|
||||
def to_hex_str(self):
|
||||
if len(self.uuid_bytes) == 2 or len(self.uuid_bytes) == 4:
|
||||
return bytes(reversed(self.uuid_bytes)).hex().upper()
|
||||
else:
|
||||
return ''.join(
|
||||
[
|
||||
bytes(reversed(self.uuid_bytes[12:16])).hex(),
|
||||
bytes(reversed(self.uuid_bytes[10:12])).hex(),
|
||||
bytes(reversed(self.uuid_bytes[8:10])).hex(),
|
||||
bytes(reversed(self.uuid_bytes[6:8])).hex(),
|
||||
bytes(reversed(self.uuid_bytes[0:6])).hex(),
|
||||
]
|
||||
).upper()
|
||||
|
||||
return ''.join(
|
||||
[
|
||||
bytes(reversed(self.uuid_bytes[12:16])).hex(),
|
||||
bytes(reversed(self.uuid_bytes[10:12])).hex(),
|
||||
bytes(reversed(self.uuid_bytes[8:10])).hex(),
|
||||
bytes(reversed(self.uuid_bytes[6:8])).hex(),
|
||||
bytes(reversed(self.uuid_bytes[0:6])).hex(),
|
||||
]
|
||||
).upper()
|
||||
|
||||
def __bytes__(self):
|
||||
return self.to_bytes()
|
||||
@@ -242,7 +244,8 @@ class UUID:
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, UUID):
|
||||
return self.to_bytes(force_128=True) == other.to_bytes(force_128=True)
|
||||
elif type(other) is str:
|
||||
|
||||
if isinstance(other, str):
|
||||
return UUID(other) == self
|
||||
|
||||
return False
|
||||
@@ -252,11 +255,11 @@ class UUID:
|
||||
|
||||
def __str__(self):
|
||||
if len(self.uuid_bytes) == 2:
|
||||
v = struct.unpack('<H', self.uuid_bytes)[0]
|
||||
result = f'UUID-16:{v:04X}'
|
||||
uuid = struct.unpack('<H', self.uuid_bytes)[0]
|
||||
result = f'UUID-16:{uuid:04X}'
|
||||
elif len(self.uuid_bytes) == 4:
|
||||
v = struct.unpack('<I', self.uuid_bytes)[0]
|
||||
result = f'UUID-32:{v:08X}'
|
||||
uuid = struct.unpack('<I', self.uuid_bytes)[0]
|
||||
result = f'UUID-32:{uuid:08X}'
|
||||
else:
|
||||
result = '-'.join(
|
||||
[
|
||||
@@ -267,10 +270,11 @@ class UUID:
|
||||
bytes(reversed(self.uuid_bytes[0:6])).hex(),
|
||||
]
|
||||
).upper()
|
||||
|
||||
if self.name is not None:
|
||||
return result + f' ({self.name})'
|
||||
else:
|
||||
return result
|
||||
|
||||
return result
|
||||
|
||||
def __repr__(self):
|
||||
return str(self)
|
||||
@@ -280,6 +284,7 @@ class UUID:
|
||||
# Common UUID constants
|
||||
# -----------------------------------------------------------------------------
|
||||
# fmt: off
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
# Protocol Identifiers
|
||||
BT_SDP_PROTOCOL_ID = UUID.from_16_bits(0x0001, 'SDP')
|
||||
@@ -386,6 +391,7 @@ BT_HDP_SOURCE_SERVICE = UUID.from_16_bits(0x1401,
|
||||
BT_HDP_SINK_SERVICE = UUID.from_16_bits(0x1402, 'HDP Sink')
|
||||
|
||||
# fmt: on
|
||||
# pylint: enable=line-too-long
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -393,6 +399,7 @@ BT_HDP_SINK_SERVICE = UUID.from_16_bits(0x1402,
|
||||
# -----------------------------------------------------------------------------
|
||||
class DeviceClass:
|
||||
# fmt: off
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
# Major Service Classes (flags combined with OR)
|
||||
LIMITED_DISCOVERABLE_MODE_SERVICE_CLASS = (1 << 0)
|
||||
@@ -562,6 +569,7 @@ class DeviceClass:
|
||||
}
|
||||
|
||||
# fmt: on
|
||||
# pylint: enable=line-too-long
|
||||
|
||||
@staticmethod
|
||||
def split_class_of_device(class_of_device):
|
||||
@@ -600,6 +608,7 @@ class DeviceClass:
|
||||
# -----------------------------------------------------------------------------
|
||||
class AdvertisingData:
|
||||
# fmt: off
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
# This list is only partial, it still needs to be filled in from the spec
|
||||
FLAGS = 0x01
|
||||
@@ -713,8 +722,11 @@ class AdvertisingData:
|
||||
BR_EDR_HOST_FLAG = 0x10
|
||||
|
||||
# fmt: on
|
||||
# pylint: enable=line-too-long
|
||||
|
||||
def __init__(self, ad_structures=[]):
|
||||
def __init__(self, ad_structures=None):
|
||||
if ad_structures is None:
|
||||
ad_structures = []
|
||||
self.ad_structures = ad_structures[:]
|
||||
|
||||
@staticmethod
|
||||
@@ -814,53 +826,65 @@ class AdvertisingData:
|
||||
|
||||
return f'[{ad_type_str}]: {ad_data_str}'
|
||||
|
||||
# pylint: disable=too-many-return-statements
|
||||
@staticmethod
|
||||
def ad_data_to_object(ad_type, ad_data):
|
||||
if ad_type in {
|
||||
if ad_type in (
|
||||
AdvertisingData.COMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
|
||||
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
|
||||
AdvertisingData.LIST_OF_16_BIT_SERVICE_SOLICITATION_UUIDS,
|
||||
}:
|
||||
):
|
||||
return AdvertisingData.uuid_list_to_objects(ad_data, 2)
|
||||
elif ad_type in {
|
||||
|
||||
if ad_type in (
|
||||
AdvertisingData.COMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS,
|
||||
AdvertisingData.INCOMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS,
|
||||
AdvertisingData.LIST_OF_32_BIT_SERVICE_SOLICITATION_UUIDS,
|
||||
}:
|
||||
):
|
||||
return AdvertisingData.uuid_list_to_objects(ad_data, 4)
|
||||
elif ad_type in {
|
||||
|
||||
if ad_type in (
|
||||
AdvertisingData.COMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS,
|
||||
AdvertisingData.INCOMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS,
|
||||
AdvertisingData.LIST_OF_128_BIT_SERVICE_SOLICITATION_UUIDS,
|
||||
}:
|
||||
):
|
||||
return AdvertisingData.uuid_list_to_objects(ad_data, 16)
|
||||
elif ad_type == AdvertisingData.SERVICE_DATA_16_BIT_UUID:
|
||||
|
||||
if ad_type == AdvertisingData.SERVICE_DATA_16_BIT_UUID:
|
||||
return (UUID.from_bytes(ad_data[:2]), ad_data[2:])
|
||||
elif ad_type == AdvertisingData.SERVICE_DATA_32_BIT_UUID:
|
||||
|
||||
if ad_type == AdvertisingData.SERVICE_DATA_32_BIT_UUID:
|
||||
return (UUID.from_bytes(ad_data[:4]), ad_data[4:])
|
||||
elif ad_type == AdvertisingData.SERVICE_DATA_128_BIT_UUID:
|
||||
|
||||
if ad_type == AdvertisingData.SERVICE_DATA_128_BIT_UUID:
|
||||
return (UUID.from_bytes(ad_data[:16]), ad_data[16:])
|
||||
elif ad_type in {
|
||||
|
||||
if ad_type in (
|
||||
AdvertisingData.SHORTENED_LOCAL_NAME,
|
||||
AdvertisingData.COMPLETE_LOCAL_NAME,
|
||||
AdvertisingData.URI,
|
||||
}:
|
||||
):
|
||||
return ad_data.decode("utf-8")
|
||||
elif ad_type in {AdvertisingData.TX_POWER_LEVEL, AdvertisingData.FLAGS}:
|
||||
|
||||
if ad_type in (AdvertisingData.TX_POWER_LEVEL, AdvertisingData.FLAGS):
|
||||
return ad_data[0]
|
||||
elif ad_type in {
|
||||
|
||||
if ad_type in (
|
||||
AdvertisingData.APPEARANCE,
|
||||
AdvertisingData.ADVERTISING_INTERVAL,
|
||||
}:
|
||||
):
|
||||
return struct.unpack('<H', ad_data)[0]
|
||||
elif ad_type == AdvertisingData.CLASS_OF_DEVICE:
|
||||
|
||||
if ad_type == AdvertisingData.CLASS_OF_DEVICE:
|
||||
return struct.unpack('<I', bytes([*ad_data, 0]))[0]
|
||||
elif ad_type == AdvertisingData.PERIPHERAL_CONNECTION_INTERVAL_RANGE:
|
||||
|
||||
if ad_type == AdvertisingData.PERIPHERAL_CONNECTION_INTERVAL_RANGE:
|
||||
return struct.unpack('<HH', ad_data)
|
||||
elif ad_type == AdvertisingData.MANUFACTURER_SPECIFIC_DATA:
|
||||
|
||||
if ad_type == AdvertisingData.MANUFACTURER_SPECIFIC_DATA:
|
||||
return (struct.unpack_from('<H', ad_data, 0)[0], ad_data[2:])
|
||||
else:
|
||||
return ad_data
|
||||
|
||||
return ad_data
|
||||
|
||||
def append(self, data):
|
||||
offset = 0
|
||||
@@ -888,15 +912,11 @@ class AdvertisingData:
|
||||
return [
|
||||
process_ad_data(ad[1]) for ad in self.ad_structures if ad[0] == type_id
|
||||
]
|
||||
else:
|
||||
return next(
|
||||
(
|
||||
process_ad_data(ad[1])
|
||||
for ad in self.ad_structures
|
||||
if ad[0] == type_id
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
return next(
|
||||
(process_ad_data(ad[1]) for ad in self.ad_structures if ad[0] == type_id),
|
||||
None,
|
||||
)
|
||||
|
||||
def __bytes__(self):
|
||||
return b''.join(
|
||||
|
||||
@@ -125,7 +125,7 @@ def e(key, data):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def ah(k, r):
|
||||
def ah(k, r): # pylint: disable=redefined-outer-name
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part H - 2.2.2 Random Address Hash function ah
|
||||
'''
|
||||
@@ -136,9 +136,10 @@ def ah(k, r):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def c1(k, r, preq, pres, iat, rat, ia, ra):
|
||||
def c1(k, r, preq, pres, iat, rat, ia, ra): # pylint: disable=redefined-outer-name
|
||||
'''
|
||||
See Bluetooth spec, Vol 3, Part H - 2.2.3 Confirm value generation function c1 for LE Legacy Pairing
|
||||
See Bluetooth spec, Vol 3, Part H - 2.2.3 Confirm value generation function c1 for
|
||||
LE Legacy Pairing
|
||||
'''
|
||||
|
||||
p1 = bytes([iat, rat]) + preq + pres
|
||||
@@ -149,7 +150,8 @@ def c1(k, r, preq, pres, iat, rat, ia, ra):
|
||||
# -----------------------------------------------------------------------------
|
||||
def s1(k, r1, r2):
|
||||
'''
|
||||
See Bluetooth spec, Vol 3, Part H - 2.2.4 Key generation function s1 for LE Legacy Pairing
|
||||
See Bluetooth spec, Vol 3, Part H - 2.2.4 Key generation function s1 for LE Legacy
|
||||
Pairing
|
||||
'''
|
||||
|
||||
return e(k, r2[0:8] + r1[0:8])
|
||||
@@ -170,7 +172,8 @@ def aes_cmac(m, k):
|
||||
# -----------------------------------------------------------------------------
|
||||
def f4(u, v, x, z):
|
||||
'''
|
||||
See Bluetooth spec, Vol 3, Part H - 2.2.6 LE Secure Connections Confirm Value Generation Function f4
|
||||
See Bluetooth spec, Vol 3, Part H - 2.2.6 LE Secure Connections Confirm Value
|
||||
Generation Function f4
|
||||
'''
|
||||
return bytes(
|
||||
reversed(
|
||||
@@ -182,7 +185,8 @@ def f4(u, v, x, z):
|
||||
# -----------------------------------------------------------------------------
|
||||
def f5(w, n1, n2, a1, a2):
|
||||
'''
|
||||
See Bluetooth spec, Vol 3, Part H - 2.2.7 LE Secure Connections Key Generation Function f5
|
||||
See Bluetooth spec, Vol 3, Part H - 2.2.7 LE Secure Connections Key Generation
|
||||
Function f5
|
||||
|
||||
NOTE: this returns a tuple: (MacKey, LTK) in little-endian byte order
|
||||
'''
|
||||
@@ -222,9 +226,10 @@ def f5(w, n1, n2, a1, a2):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def f6(w, n1, n2, r, io_cap, a1, a2):
|
||||
def f6(w, n1, n2, r, io_cap, a1, a2): # pylint: disable=redefined-outer-name
|
||||
'''
|
||||
See Bluetooth spec, Vol 3, Part H - 2.2.8 LE Secure Connections Check Value Generation Function f6
|
||||
See Bluetooth spec, Vol 3, Part H - 2.2.8 LE Secure Connections Check Value
|
||||
Generation Function f6
|
||||
'''
|
||||
return bytes(
|
||||
reversed(
|
||||
@@ -244,7 +249,8 @@ def f6(w, n1, n2, r, io_cap, a1, a2):
|
||||
# -----------------------------------------------------------------------------
|
||||
def g2(u, v, x, y):
|
||||
'''
|
||||
See Bluetooth spec, Vol 3, Part H - 2.2.9 LE Secure Connections Numeric Comparison Value Generation Function g2
|
||||
See Bluetooth spec, Vol 3, Part H - 2.2.9 LE Secure Connections Numeric Comparison
|
||||
Value Generation Function g2
|
||||
'''
|
||||
return int.from_bytes(
|
||||
aes_cmac(
|
||||
|
||||
436
bumble/device.py
436
bumble/device.py
@@ -16,29 +16,130 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from enum import IntEnum
|
||||
import functools
|
||||
import json
|
||||
import asyncio
|
||||
import logging
|
||||
from contextlib import asynccontextmanager, AsyncExitStack
|
||||
from dataclasses import dataclass
|
||||
from colors import color
|
||||
|
||||
from .hci import *
|
||||
from .att import ATT_CID, ATT_DEFAULT_MTU, ATT_PDU
|
||||
from .gatt import Characteristic, Descriptor, Service
|
||||
from .hci import (
|
||||
HCI_CENTRAL_ROLE,
|
||||
HCI_COMMAND_STATUS_PENDING,
|
||||
HCI_CONNECTION_REJECTED_DUE_TO_LIMITED_RESOURCES_ERROR,
|
||||
HCI_DISPLAY_ONLY_IO_CAPABILITY,
|
||||
HCI_DISPLAY_YES_NO_IO_CAPABILITY,
|
||||
HCI_EXTENDED_INQUIRY_MODE,
|
||||
HCI_GENERAL_INQUIRY_LAP,
|
||||
HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR,
|
||||
HCI_KEYBOARD_ONLY_IO_CAPABILITY,
|
||||
HCI_LE_1M_PHY,
|
||||
HCI_LE_1M_PHY_BIT,
|
||||
HCI_LE_2M_PHY,
|
||||
HCI_LE_2M_PHY_LE_SUPPORTED_FEATURE,
|
||||
HCI_LE_CLEAR_RESOLVING_LIST_COMMAND,
|
||||
HCI_LE_CODED_PHY,
|
||||
HCI_LE_CODED_PHY_BIT,
|
||||
HCI_LE_CODED_PHY_LE_SUPPORTED_FEATURE,
|
||||
HCI_LE_EXTENDED_ADVERTISING_LE_SUPPORTED_FEATURE,
|
||||
HCI_LE_EXTENDED_CREATE_CONNECTION_COMMAND,
|
||||
HCI_LE_READ_PHY_COMMAND,
|
||||
HCI_MITM_NOT_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS,
|
||||
HCI_MITM_NOT_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS,
|
||||
HCI_MITM_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS,
|
||||
HCI_MITM_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS,
|
||||
HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
|
||||
HCI_R2_PAGE_SCAN_REPETITION_MODE,
|
||||
HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR,
|
||||
HCI_SUCCESS,
|
||||
HCI_WRITE_LE_HOST_SUPPORT_COMMAND,
|
||||
Address,
|
||||
HCI_Accept_Connection_Request_Command,
|
||||
HCI_Authentication_Requested_Command,
|
||||
HCI_Command_Status_Event,
|
||||
HCI_Constant,
|
||||
HCI_Create_Connection_Cancel_Command,
|
||||
HCI_Create_Connection_Command,
|
||||
HCI_Disconnect_Command,
|
||||
HCI_Encryption_Change_Event,
|
||||
HCI_Error,
|
||||
HCI_IO_Capability_Request_Reply_Command,
|
||||
HCI_Inquiry_Cancel_Command,
|
||||
HCI_Inquiry_Command,
|
||||
HCI_LE_Add_Device_To_Resolving_List_Command,
|
||||
HCI_LE_Advertising_Report_Event,
|
||||
HCI_LE_Clear_Resolving_List_Command,
|
||||
HCI_LE_Connection_Update_Command,
|
||||
HCI_LE_Create_Connection_Cancel_Command,
|
||||
HCI_LE_Create_Connection_Command,
|
||||
HCI_LE_Enable_Encryption_Command,
|
||||
HCI_LE_Extended_Advertising_Report_Event,
|
||||
HCI_LE_Extended_Create_Connection_Command,
|
||||
HCI_LE_Read_PHY_Command,
|
||||
HCI_LE_Set_Advertising_Data_Command,
|
||||
HCI_LE_Set_Advertising_Enable_Command,
|
||||
HCI_LE_Set_Advertising_Parameters_Command,
|
||||
HCI_LE_Set_Default_PHY_Command,
|
||||
HCI_LE_Set_Extended_Scan_Enable_Command,
|
||||
HCI_LE_Set_Extended_Scan_Parameters_Command,
|
||||
HCI_LE_Set_PHY_Command,
|
||||
HCI_LE_Set_Random_Address_Command,
|
||||
HCI_LE_Set_Scan_Enable_Command,
|
||||
HCI_LE_Set_Scan_Parameters_Command,
|
||||
HCI_LE_Set_Scan_Response_Data_Command,
|
||||
HCI_Read_BD_ADDR_Command,
|
||||
HCI_Read_RSSI_Command,
|
||||
HCI_Reject_Connection_Request_Command,
|
||||
HCI_Remote_Name_Request_Command,
|
||||
HCI_Set_Connection_Encryption_Command,
|
||||
HCI_StatusError,
|
||||
HCI_User_Confirmation_Request_Negative_Reply_Command,
|
||||
HCI_User_Confirmation_Request_Reply_Command,
|
||||
HCI_User_Passkey_Request_Negative_Reply_Command,
|
||||
HCI_User_Passkey_Request_Reply_Command,
|
||||
HCI_Write_Class_Of_Device_Command,
|
||||
HCI_Write_Extended_Inquiry_Response_Command,
|
||||
HCI_Write_Inquiry_Mode_Command,
|
||||
HCI_Write_LE_Host_Support_Command,
|
||||
HCI_Write_Local_Name_Command,
|
||||
HCI_Write_Scan_Enable_Command,
|
||||
HCI_Write_Secure_Connections_Host_Support_Command,
|
||||
HCI_Write_Simple_Pairing_Mode_Command,
|
||||
OwnAddressType,
|
||||
phy_list_to_bits,
|
||||
)
|
||||
from .host import Host
|
||||
from .gatt import *
|
||||
from .gap import GenericAccessService
|
||||
from .core import AdvertisingData, BT_CENTRAL_ROLE, BT_PERIPHERAL_ROLE
|
||||
from .core import (
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
BT_CENTRAL_ROLE,
|
||||
BT_LE_TRANSPORT,
|
||||
BT_PERIPHERAL_ROLE,
|
||||
AdvertisingData,
|
||||
CommandTimeoutError,
|
||||
ConnectionPHY,
|
||||
InvalidStateError,
|
||||
)
|
||||
from .utils import (
|
||||
AsyncRunner,
|
||||
CompositeEventEmitter,
|
||||
setup_event_forwarding,
|
||||
composite_listener,
|
||||
)
|
||||
from .keys import (
|
||||
KeyStore,
|
||||
PairingKeys,
|
||||
)
|
||||
from . import gatt_client
|
||||
from . import gatt_server
|
||||
from . import smp
|
||||
from . import sdp
|
||||
from . import l2cap
|
||||
from . import keys
|
||||
from . import core
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -49,6 +150,7 @@ logger = logging.getLogger(__name__)
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
# fmt: off
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
DEVICE_MIN_SCAN_INTERVAL = 25
|
||||
DEVICE_MAX_SCAN_INTERVAL = 10240
|
||||
@@ -81,6 +183,7 @@ DEVICE_DEFAULT_L2CAP_COC_MPS = l2cap.L2CAP_LE_CREDIT_BASED_CONN
|
||||
DEVICE_DEFAULT_L2CAP_COC_MAX_CREDITS = l2cap.L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_INITIAL_CREDITS
|
||||
|
||||
# fmt: on
|
||||
# pylint: enable=line-too-long
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -98,9 +201,13 @@ class Advertisement:
|
||||
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):
|
||||
|
||||
if isinstance(report, HCI_LE_Extended_Advertising_Report_Event.Report):
|
||||
return ExtendedAdvertisement.from_advertising_report(report)
|
||||
|
||||
return None
|
||||
|
||||
# pylint: disable=line-too-long
|
||||
def __init__(
|
||||
self,
|
||||
address,
|
||||
@@ -145,17 +252,17 @@ class LegacyAdvertisement(Advertisement):
|
||||
rssi=report.rssi,
|
||||
is_legacy=True,
|
||||
is_connectable=report.event_type
|
||||
in {
|
||||
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 {
|
||||
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,
|
||||
@@ -167,6 +274,7 @@ class ExtendedAdvertisement(Advertisement):
|
||||
@classmethod
|
||||
def from_advertising_report(cls, report):
|
||||
# fmt: off
|
||||
# pylint: disable=line-too-long
|
||||
return cls(
|
||||
address = report.address,
|
||||
rssi = report.rssi,
|
||||
@@ -231,6 +339,7 @@ class AdvertisementDataAccumulator:
|
||||
# -----------------------------------------------------------------------------
|
||||
class AdvertisingType(IntEnum):
|
||||
# fmt: off
|
||||
# pylint: disable=line-too-long
|
||||
UNDIRECTED_CONNECTABLE_SCANNABLE = 0x00 # Undirected, connectable, scannable
|
||||
DIRECTED_CONNECTABLE_HIGH_DUTY = 0x01 # Directed, connectable, non-scannable
|
||||
UNDIRECTED_SCANNABLE = 0x02 # Undirected, non-connectable, scannable
|
||||
@@ -240,33 +349,33 @@ class AdvertisingType(IntEnum):
|
||||
|
||||
@property
|
||||
def has_data(self):
|
||||
return self in {
|
||||
return self in (
|
||||
AdvertisingType.UNDIRECTED_CONNECTABLE_SCANNABLE,
|
||||
AdvertisingType.UNDIRECTED_SCANNABLE,
|
||||
AdvertisingType.UNDIRECTED,
|
||||
}
|
||||
)
|
||||
|
||||
@property
|
||||
def is_connectable(self):
|
||||
return self in {
|
||||
return self in (
|
||||
AdvertisingType.UNDIRECTED_CONNECTABLE_SCANNABLE,
|
||||
AdvertisingType.DIRECTED_CONNECTABLE_HIGH_DUTY,
|
||||
AdvertisingType.DIRECTED_CONNECTABLE_LOW_DUTY,
|
||||
}
|
||||
)
|
||||
|
||||
@property
|
||||
def is_scannable(self):
|
||||
return self in {
|
||||
return self in (
|
||||
AdvertisingType.UNDIRECTED_CONNECTABLE_SCANNABLE,
|
||||
AdvertisingType.UNDIRECTED_SCANNABLE,
|
||||
}
|
||||
)
|
||||
|
||||
@property
|
||||
def is_directed(self):
|
||||
return self in {
|
||||
return self in (
|
||||
AdvertisingType.DIRECTED_CONNECTABLE_HIGH_DUTY,
|
||||
AdvertisingType.DIRECTED_CONNECTABLE_LOW_DUTY,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -304,13 +413,13 @@ class Peer:
|
||||
async def discover_service(self, uuid):
|
||||
return await self.gatt_client.discover_service(uuid)
|
||||
|
||||
async def discover_services(self, uuids=[]):
|
||||
async def discover_services(self, uuids=()):
|
||||
return await self.gatt_client.discover_services(uuids)
|
||||
|
||||
async def discover_included_services(self, service):
|
||||
return await self.gatt_client.discover_included_services(service)
|
||||
|
||||
async def discover_characteristics(self, uuids=[], service=None):
|
||||
async def discover_characteristics(self, uuids=(), service=None):
|
||||
return await self.gatt_client.discover_characteristics(
|
||||
uuids=uuids, service=service
|
||||
)
|
||||
@@ -369,7 +478,7 @@ class Peer:
|
||||
async def __aenter__(self):
|
||||
await self.discover_services()
|
||||
for service in self.services:
|
||||
await self.discover_characteristics()
|
||||
await service.discover_characteristics()
|
||||
|
||||
return self
|
||||
|
||||
@@ -460,7 +569,8 @@ class Connection(CompositeEventEmitter):
|
||||
@classmethod
|
||||
def incomplete(cls, device, peer_address):
|
||||
"""
|
||||
Instantiate an incomplete connection (ie. one waiting for a HCI Connection Complete event).
|
||||
Instantiate an incomplete connection (ie. one waiting for a HCI Connection
|
||||
Complete event).
|
||||
Once received it shall be completed using the `.complete` method.
|
||||
"""
|
||||
return cls(
|
||||
@@ -576,13 +686,17 @@ class Connection(CompositeEventEmitter):
|
||||
if exc_type is None:
|
||||
try:
|
||||
await self.disconnect()
|
||||
except HCI_StatusError as e:
|
||||
except HCI_StatusError as error:
|
||||
# Invalid parameter means the connection is no longer valid
|
||||
if e.error_code != HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR:
|
||||
if error.error_code != HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR:
|
||||
raise
|
||||
|
||||
def __str__(self):
|
||||
return f'Connection(handle=0x{self.handle:04X}, role={self.role_name}, address={self.peer_address})'
|
||||
return (
|
||||
f'Connection(handle=0x{self.handle:04X}, '
|
||||
f'role={self.role_name}, '
|
||||
f'address={self.peer_address})'
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -645,7 +759,8 @@ class DeviceConfiguration:
|
||||
self.irk = bytes.fromhex(irk)
|
||||
else:
|
||||
# Construct an IRK from the address bytes
|
||||
# NOTE: this is not secure, but will always give the same IRK for the same address
|
||||
# NOTE: this is not secure, but will always give the same IRK for the same
|
||||
# address
|
||||
address_bytes = bytes(self.address)
|
||||
self.irk = (address_bytes * 3)[:16]
|
||||
|
||||
@@ -655,7 +770,7 @@ class DeviceConfiguration:
|
||||
self.advertising_data = bytes.fromhex(advertising_data)
|
||||
|
||||
def load_from_file(self, filename):
|
||||
with open(filename, 'r') as file:
|
||||
with open(filename, 'r', encoding='utf-8') as file:
|
||||
self.load_from_dict(json.load(file))
|
||||
|
||||
|
||||
@@ -691,7 +806,8 @@ def with_connection_from_address(function):
|
||||
return wrapper
|
||||
|
||||
|
||||
# Decorator that tries to convert the first argument from a bluetooth address to a connection
|
||||
# Decorator that tries to convert the first argument from a bluetooth address to a
|
||||
# connection
|
||||
def try_with_connection_from_address(function):
|
||||
@functools.wraps(function)
|
||||
def wrapper(self, address, *args, **kwargs):
|
||||
@@ -816,7 +932,7 @@ class Device(CompositeEventEmitter):
|
||||
self.advertising_data = config.advertising_data
|
||||
self.advertising_interval_min = config.advertising_interval_min
|
||||
self.advertising_interval_max = config.advertising_interval_max
|
||||
self.keystore = keys.KeyStore.create_for_device(config)
|
||||
self.keystore = KeyStore.create_for_device(config)
|
||||
self.irk = config.irk
|
||||
self.le_enabled = config.le_enabled
|
||||
self.le_simultaneous_enabled = config.le_simultaneous_enabled
|
||||
@@ -832,7 +948,7 @@ class Device(CompositeEventEmitter):
|
||||
descriptors = []
|
||||
for descriptor in characteristic.get("descriptors", []):
|
||||
new_descriptor = Descriptor(
|
||||
descriptor_type=descriptor["descriptor_type"],
|
||||
attribute_type=descriptor["descriptor_type"],
|
||||
permissions=descriptor["permission"],
|
||||
)
|
||||
descriptors.append(new_descriptor)
|
||||
@@ -852,7 +968,7 @@ class Device(CompositeEventEmitter):
|
||||
|
||||
# If an address is passed, override the address from the config
|
||||
if address:
|
||||
if type(address) is str:
|
||||
if isinstance(address, str):
|
||||
address = Address(address)
|
||||
self.random_address = address
|
||||
|
||||
@@ -914,6 +1030,8 @@ class Device(CompositeEventEmitter):
|
||||
if connection := self.connections.get(connection_handle):
|
||||
return connection
|
||||
|
||||
return None
|
||||
|
||||
def find_connection_by_bd_addr(
|
||||
self, bd_addr, transport=None, check_address_type=False
|
||||
):
|
||||
@@ -927,6 +1045,8 @@ class Device(CompositeEventEmitter):
|
||||
if transport is None or connection.transport == transport:
|
||||
return connection
|
||||
|
||||
return None
|
||||
|
||||
def create_l2cap_connector(self, connection, psm):
|
||||
return lambda: self.l2cap_channel_manager.connect(connection, psm)
|
||||
|
||||
@@ -968,9 +1088,9 @@ class Device(CompositeEventEmitter):
|
||||
return await asyncio.wait_for(
|
||||
self.host.send_command(command, check_result), self.command_timeout
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
except asyncio.TimeoutError as error:
|
||||
logger.warning('!!! Command timed out')
|
||||
raise CommandTimeoutError()
|
||||
raise CommandTimeoutError() from error
|
||||
|
||||
async def power_on(self):
|
||||
# Reset the controller
|
||||
@@ -1017,7 +1137,9 @@ class Device(CompositeEventEmitter):
|
||||
|
||||
# Enable address resolution
|
||||
# await self.send_command(
|
||||
# HCI_LE_Set_Address_Resolution_Enable_Command(address_resolution_enable=1)
|
||||
# HCI_LE_Set_Address_Resolution_Enable_Command(
|
||||
# address_resolution_enable=1)
|
||||
# )
|
||||
# )
|
||||
|
||||
# Create a host-side address resolver
|
||||
@@ -1171,7 +1293,7 @@ class Device(CompositeEventEmitter):
|
||||
raise ValueError('scan_interval out of range')
|
||||
|
||||
# Reset the accumulators
|
||||
self.advertisement_accumulator = {}
|
||||
self.advertisement_accumulators = {}
|
||||
|
||||
# Enable scanning
|
||||
if not legacy and self.supports_le_feature(
|
||||
@@ -1230,6 +1352,7 @@ class Device(CompositeEventEmitter):
|
||||
else HCI_LE_Set_Scan_Parameters_Command.PASSIVE_SCANNING
|
||||
)
|
||||
await self.send_command(
|
||||
# pylint: disable=line-too-long
|
||||
HCI_LE_Set_Scan_Parameters_Command(
|
||||
le_scan_type=scan_type,
|
||||
le_scan_interval=int(scan_window / 0.625),
|
||||
@@ -1376,17 +1499,19 @@ class Device(CompositeEventEmitter):
|
||||
):
|
||||
'''
|
||||
Request a connection to a peer.
|
||||
When transport is BLE, this method cannot be called if there is already a pending connection.
|
||||
When transport is BLE, this method cannot be called if there is already a
|
||||
pending connection.
|
||||
|
||||
connection_parameters_preferences: (BLE only, ignored for BR/EDR)
|
||||
* None: use all PHYs with default parameters
|
||||
* map: each entry has a PHY as key and a ConnectionParametersPreferences object as value
|
||||
* map: each entry has a PHY as key and a ConnectionParametersPreferences
|
||||
object as value
|
||||
|
||||
own_address_type: (BLE only)
|
||||
'''
|
||||
|
||||
# Check parameters
|
||||
if transport not in {BT_LE_TRANSPORT, BT_BR_EDR_TRANSPORT}:
|
||||
if transport not in (BT_LE_TRANSPORT, BT_BR_EDR_TRANSPORT):
|
||||
raise ValueError('invalid transport')
|
||||
|
||||
# Adjust the transport automatically if we need to
|
||||
@@ -1399,7 +1524,7 @@ class Device(CompositeEventEmitter):
|
||||
if transport == BT_LE_TRANSPORT and self.is_le_connecting:
|
||||
raise InvalidStateError('connection already pending')
|
||||
|
||||
if type(peer_address) is str:
|
||||
if isinstance(peer_address, str):
|
||||
try:
|
||||
peer_address = Address.from_string_for_transport(
|
||||
peer_address, transport
|
||||
@@ -1590,27 +1715,26 @@ class Device(CompositeEventEmitter):
|
||||
# Wait for the connection process to complete
|
||||
if transport == BT_LE_TRANSPORT:
|
||||
self.le_connecting = True
|
||||
|
||||
if timeout is None:
|
||||
return await self.abort_on('flush', pending_connection)
|
||||
else:
|
||||
try:
|
||||
return await asyncio.wait_for(
|
||||
asyncio.shield(pending_connection), timeout
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
if transport == BT_LE_TRANSPORT:
|
||||
await self.send_command(
|
||||
HCI_LE_Create_Connection_Cancel_Command()
|
||||
)
|
||||
else:
|
||||
await self.send_command(
|
||||
HCI_Create_Connection_Cancel_Command(bd_addr=peer_address)
|
||||
)
|
||||
|
||||
try:
|
||||
return await self.abort_on('flush', pending_connection)
|
||||
except ConnectionError:
|
||||
raise TimeoutError()
|
||||
try:
|
||||
return await asyncio.wait_for(
|
||||
asyncio.shield(pending_connection), timeout
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
if transport == BT_LE_TRANSPORT:
|
||||
await self.send_command(HCI_LE_Create_Connection_Cancel_Command())
|
||||
else:
|
||||
await self.send_command(
|
||||
HCI_Create_Connection_Cancel_Command(bd_addr=peer_address)
|
||||
)
|
||||
|
||||
try:
|
||||
return await self.abort_on('flush', pending_connection)
|
||||
except ConnectionError as error:
|
||||
raise core.TimeoutError() from error
|
||||
finally:
|
||||
self.remove_listener('connection', on_connection)
|
||||
self.remove_listener('connection_failure', on_connection_failure)
|
||||
@@ -1627,15 +1751,17 @@ class Device(CompositeEventEmitter):
|
||||
timeout=DEVICE_DEFAULT_CONNECT_TIMEOUT,
|
||||
):
|
||||
'''
|
||||
Wait and accept any incoming connection or a connection from `peer_address` when set.
|
||||
Wait and accept any incoming connection or a connection from `peer_address` when
|
||||
set.
|
||||
|
||||
Notes:
|
||||
* A `connect` to the same peer will also complete this call.
|
||||
* The `timeout` parameter is only handled while waiting for the connection request,
|
||||
once received and accepted, the controller shall issue a connection complete event.
|
||||
* The `timeout` parameter is only handled while waiting for the connection
|
||||
request, once received and accepted, the controller shall issue a connection
|
||||
complete event.
|
||||
'''
|
||||
|
||||
if type(peer_address) is str:
|
||||
if isinstance(peer_address, str):
|
||||
try:
|
||||
peer_address = Address(peer_address)
|
||||
except ValueError:
|
||||
@@ -1680,7 +1806,7 @@ class Device(CompositeEventEmitter):
|
||||
return result
|
||||
|
||||
# Otherwise, result came from `on_connection_request`
|
||||
peer_address, class_of_device, link_type = result
|
||||
peer_address, _class_of_device, _link_type = result
|
||||
|
||||
# Create a future so that we can wait for the connection's result
|
||||
pending_connection = asyncio.get_running_loop().create_future()
|
||||
@@ -1749,9 +1875,10 @@ class Device(CompositeEventEmitter):
|
||||
)
|
||||
|
||||
# BR/EDR: try to cancel to ongoing connection
|
||||
# NOTE: This API does not prevent from trying to cancel a connection which is not currently being created
|
||||
# NOTE: This API does not prevent from trying to cancel a connection which is
|
||||
# not currently being created
|
||||
else:
|
||||
if type(peer_address) is str:
|
||||
if isinstance(peer_address, str):
|
||||
try:
|
||||
peer_address = Address(peer_address)
|
||||
except ValueError:
|
||||
@@ -1804,7 +1931,8 @@ class Device(CompositeEventEmitter):
|
||||
max_ce_length=0,
|
||||
):
|
||||
'''
|
||||
NOTE: the name of the parameters may look odd, but it just follows the names used in the Bluetooth spec.
|
||||
NOTE: the name of the parameters may look odd, but it just follows the names
|
||||
used in the Bluetooth spec.
|
||||
'''
|
||||
await self.send_command(
|
||||
HCI_LE_Connection_Update_Command(
|
||||
@@ -1881,8 +2009,10 @@ class Device(CompositeEventEmitter):
|
||||
if local_name.decode('utf-8') == name:
|
||||
peer_address.set_result(address)
|
||||
|
||||
handler = None
|
||||
was_scanning = self.scanning
|
||||
was_discovering = self.discovering
|
||||
try:
|
||||
handler = None
|
||||
if transport == BT_LE_TRANSPORT:
|
||||
event_name = 'advertisement'
|
||||
handler = self.on(
|
||||
@@ -1892,7 +2022,6 @@ class Device(CompositeEventEmitter):
|
||||
),
|
||||
)
|
||||
|
||||
was_scanning = self.scanning
|
||||
if not self.scanning:
|
||||
await self.start_scanning(filter_duplicates=True)
|
||||
|
||||
@@ -1905,7 +2034,6 @@ class Device(CompositeEventEmitter):
|
||||
),
|
||||
)
|
||||
|
||||
was_discovering = self.discovering
|
||||
if not self.discovering:
|
||||
await self.start_discovery()
|
||||
else:
|
||||
@@ -1951,9 +2079,11 @@ class Device(CompositeEventEmitter):
|
||||
logger.debug('found keys in the key store')
|
||||
if keys.ltk:
|
||||
return keys.ltk.value
|
||||
elif connection.role == BT_CENTRAL_ROLE and keys.ltk_central:
|
||||
|
||||
if connection.role == BT_CENTRAL_ROLE and keys.ltk_central:
|
||||
return keys.ltk_central.value
|
||||
elif connection.role == BT_PERIPHERAL_ROLE and keys.ltk_peripheral:
|
||||
|
||||
if connection.role == BT_PERIPHERAL_ROLE and keys.ltk_peripheral:
|
||||
return keys.ltk_peripheral.value
|
||||
|
||||
async def get_link_key(self, address):
|
||||
@@ -1986,8 +2116,9 @@ class Device(CompositeEventEmitter):
|
||||
)
|
||||
)
|
||||
if result.status != HCI_COMMAND_STATUS_PENDING:
|
||||
logger.warn(
|
||||
f'HCI_Authentication_Requested_Command failed: {HCI_Constant.error_name(result.status)}'
|
||||
logger.warning(
|
||||
'HCI_Authentication_Requested_Command failed: '
|
||||
f'{HCI_Constant.error_name(result.status)}'
|
||||
)
|
||||
raise HCI_StatusError(result)
|
||||
|
||||
@@ -2050,20 +2181,23 @@ class Device(CompositeEventEmitter):
|
||||
)
|
||||
|
||||
if result.status != HCI_COMMAND_STATUS_PENDING:
|
||||
logger.warn(
|
||||
f'HCI_LE_Enable_Encryption_Command failed: {HCI_Constant.error_name(result.status)}'
|
||||
logger.warning(
|
||||
'HCI_LE_Enable_Encryption_Command failed: '
|
||||
f'{HCI_Constant.error_name(result.status)}'
|
||||
)
|
||||
raise HCI_StatusError(result)
|
||||
else:
|
||||
result = await self.send_command(
|
||||
HCI_Set_Connection_Encryption_Command(
|
||||
connection_handle=connection.handle, encryption_enable=0x01 if enable else 0x00
|
||||
connection_handle=connection.handle,
|
||||
encryption_enable=0x01 if enable else 0x00,
|
||||
)
|
||||
)
|
||||
|
||||
if result.status != HCI_COMMAND_STATUS_PENDING:
|
||||
logger.warn(
|
||||
f'HCI_Set_Connection_Encryption_Command failed: {HCI_Constant.error_name(result.status)}'
|
||||
logger.warning(
|
||||
'HCI_Set_Connection_Encryption_Command failed: '
|
||||
f'{HCI_Constant.error_name(result.status)}'
|
||||
)
|
||||
raise HCI_StatusError(result)
|
||||
|
||||
@@ -2082,7 +2216,7 @@ class Device(CompositeEventEmitter):
|
||||
# Set up event handlers
|
||||
pending_name = asyncio.get_running_loop().create_future()
|
||||
|
||||
peer_address = remote if type(remote) == Address else remote.peer_address
|
||||
peer_address = remote if isinstance(remote, Address) else remote.peer_address
|
||||
|
||||
handler = self.on(
|
||||
'remote_name',
|
||||
@@ -2103,15 +2237,17 @@ class Device(CompositeEventEmitter):
|
||||
result = await self.send_command(
|
||||
HCI_Remote_Name_Request_Command(
|
||||
bd_addr=peer_address,
|
||||
page_scan_repetition_mode=HCI_Remote_Name_Request_Command.R0, # TODO investigate other options
|
||||
# TODO investigate other options
|
||||
page_scan_repetition_mode=HCI_Remote_Name_Request_Command.R0,
|
||||
reserved=0,
|
||||
clock_offset=0, # TODO investigate non-0 values
|
||||
)
|
||||
)
|
||||
|
||||
if result.status != HCI_COMMAND_STATUS_PENDING:
|
||||
logger.warn(
|
||||
f'HCI_Set_Connection_Encryption_Command failed: {HCI_Constant.error_name(result.status)}'
|
||||
logger.warning(
|
||||
'HCI_Set_Connection_Encryption_Command failed: '
|
||||
f'{HCI_Constant.error_name(result.status)}'
|
||||
)
|
||||
raise HCI_StatusError(result)
|
||||
|
||||
@@ -2133,14 +2269,14 @@ class Device(CompositeEventEmitter):
|
||||
def on_link_key(self, bd_addr, link_key, key_type):
|
||||
# Store the keys in the key store
|
||||
if self.keystore:
|
||||
pairing_keys = keys.PairingKeys()
|
||||
pairing_keys.link_key = keys.PairingKeys.Key(value=link_key)
|
||||
pairing_keys = PairingKeys()
|
||||
pairing_keys.link_key = PairingKeys.Key(value=link_key)
|
||||
|
||||
async def store_keys():
|
||||
try:
|
||||
await self.keystore.update(str(bd_addr), pairing_keys)
|
||||
except Exception as error:
|
||||
logger.warn(f'!!! error while storing keys: {error}')
|
||||
logger.warning(f'!!! error while storing keys: {error}')
|
||||
|
||||
self.abort_on('flush', store_keys())
|
||||
|
||||
@@ -2183,10 +2319,11 @@ class Device(CompositeEventEmitter):
|
||||
connection_parameters,
|
||||
):
|
||||
logger.debug(
|
||||
f'*** Connection: [0x{connection_handle:04X}] {peer_address} as {HCI_Constant.role_name(role)}'
|
||||
f'*** Connection: [0x{connection_handle:04X}] '
|
||||
f'{peer_address} as {HCI_Constant.role_name(role)}'
|
||||
)
|
||||
if connection_handle in self.connections:
|
||||
logger.warn(
|
||||
logger.warning(
|
||||
'new connection reuses the same handle as a previous connection'
|
||||
)
|
||||
|
||||
@@ -2198,10 +2335,12 @@ class Device(CompositeEventEmitter):
|
||||
)
|
||||
self.connections[connection_handle] = connection
|
||||
|
||||
# We may have an accept ongoing waiting for a connection request for `peer_address`.
|
||||
# Typically happen when using `connect` to the same `peer_address` we are waiting with
|
||||
# an `accept` for.
|
||||
# In this case, set the completed `connection` to the `accept` future result.
|
||||
# We may have an accept ongoing waiting for a connection request for
|
||||
# `peer_address`.
|
||||
# Typically happen when using `connect` to the same `peer_address` we are
|
||||
# waiting for with an `accept`.
|
||||
# In this case, set the completed `connection` to the `accept` future
|
||||
# result.
|
||||
if peer_address in self.classic_pending_accepts:
|
||||
future = self.classic_pending_accepts.pop(peer_address)
|
||||
future.set_result(connection)
|
||||
@@ -2234,10 +2373,14 @@ class Device(CompositeEventEmitter):
|
||||
async def new_connection():
|
||||
# Figure out which PHY we're connected with
|
||||
if self.host.supports_command(HCI_LE_READ_PHY_COMMAND):
|
||||
result = await asyncio.shield(self.send_command(
|
||||
HCI_LE_Read_PHY_Command(connection_handle=connection_handle),
|
||||
check_result=True,
|
||||
))
|
||||
result = await asyncio.shield(
|
||||
self.send_command(
|
||||
HCI_LE_Read_PHY_Command(
|
||||
connection_handle=connection_handle
|
||||
),
|
||||
check_result=True,
|
||||
)
|
||||
)
|
||||
phy = ConnectionPHY(
|
||||
result.return_parameters.tx_phy, result.return_parameters.rx_phy
|
||||
)
|
||||
@@ -2332,7 +2475,8 @@ class Device(CompositeEventEmitter):
|
||||
@with_connection_from_handle
|
||||
def on_disconnection(self, connection, reason):
|
||||
logger.debug(
|
||||
f'*** Disconnection: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}, reason={reason}'
|
||||
f'*** Disconnection: [0x{connection.handle:04X}] '
|
||||
f'{connection.peer_address} as {connection.role_name}, reason={reason}'
|
||||
)
|
||||
connection.emit('disconnection', reason)
|
||||
|
||||
@@ -2345,10 +2489,11 @@ class Device(CompositeEventEmitter):
|
||||
# Restart advertising if auto-restart is enabled
|
||||
if self.auto_restart_advertising:
|
||||
logger.debug('restarting advertising')
|
||||
self.abort_on('flush',
|
||||
self.abort_on(
|
||||
'flush',
|
||||
self.start_advertising(
|
||||
advertising_type=self.advertising_type, auto_restart=True
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
@host_event_handler
|
||||
@@ -2379,7 +2524,8 @@ class Device(CompositeEventEmitter):
|
||||
@with_connection_from_handle
|
||||
def on_connection_authentication(self, connection):
|
||||
logger.debug(
|
||||
f'*** Connection Authentication: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}'
|
||||
f'*** Connection Authentication: [0x{connection.handle:04X}] '
|
||||
f'{connection.peer_address} as {connection.role_name}'
|
||||
)
|
||||
connection.authenticated = True
|
||||
connection.emit('connection_authentication')
|
||||
@@ -2388,10 +2534,25 @@ class Device(CompositeEventEmitter):
|
||||
@with_connection_from_handle
|
||||
def on_connection_authentication_failure(self, connection, error):
|
||||
logger.debug(
|
||||
f'*** Connection Authentication Failure: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}, error={error}'
|
||||
f'*** Connection Authentication Failure: [0x{connection.handle:04X}] '
|
||||
f'{connection.peer_address} as {connection.role_name}, error={error}'
|
||||
)
|
||||
connection.emit('connection_authentication_failure', error)
|
||||
|
||||
@host_event_handler
|
||||
@with_connection_from_address
|
||||
def on_ssp_complete(self, connection):
|
||||
# On Secure Simple Pairing complete, in case:
|
||||
# - Connection isn't already authenticated
|
||||
# - AND we are not the initiator of the authentication
|
||||
# We must trigger authentication to known if we are truly authenticated
|
||||
if not connection.authenticating and not connection.authenticated:
|
||||
logger.debug(
|
||||
f'*** Trigger Connection Authentication: [0x{connection.handle:04X}] '
|
||||
f'{connection.peer_address}'
|
||||
)
|
||||
asyncio.create_task(connection.authenticate())
|
||||
|
||||
# [Classic only]
|
||||
@host_event_handler
|
||||
@with_connection_from_address
|
||||
@@ -2400,6 +2561,7 @@ class Device(CompositeEventEmitter):
|
||||
pairing_config = self.pairing_config_factory(connection)
|
||||
|
||||
# Map the SMP IO capability to a Classic IO capability
|
||||
# pylint: disable=line-too-long
|
||||
io_capability = {
|
||||
smp.SMP_DISPLAY_ONLY_IO_CAPABILITY: HCI_DISPLAY_ONLY_IO_CAPABILITY,
|
||||
smp.SMP_DISPLAY_YES_NO_IO_CAPABILITY: HCI_DISPLAY_YES_NO_IO_CAPABILITY,
|
||||
@@ -2445,19 +2607,18 @@ class Device(CompositeEventEmitter):
|
||||
# Ask what the pairing config should be for this connection
|
||||
pairing_config = self.pairing_config_factory(connection)
|
||||
|
||||
can_compare = pairing_config.delegate.io_capability not in {
|
||||
can_compare = pairing_config.delegate.io_capability not in (
|
||||
smp.SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
|
||||
smp.SMP_DISPLAY_ONLY_IO_CAPABILITY,
|
||||
}
|
||||
)
|
||||
|
||||
# Respond
|
||||
if can_compare:
|
||||
|
||||
async def compare_numbers():
|
||||
numbers_match = await connection.abort_on('disconnection',
|
||||
pairing_config.delegate.compare_numbers(
|
||||
code, digits=6
|
||||
)
|
||||
numbers_match = await connection.abort_on(
|
||||
'disconnection',
|
||||
pairing_config.delegate.compare_numbers(code, digits=6),
|
||||
)
|
||||
if numbers_match:
|
||||
await self.host.send_command(
|
||||
@@ -2476,8 +2637,9 @@ class Device(CompositeEventEmitter):
|
||||
else:
|
||||
|
||||
async def confirm():
|
||||
confirm = await connection.abort_on('disconnection',
|
||||
pairing_config.delegate.confirm())
|
||||
confirm = await connection.abort_on(
|
||||
'disconnection', pairing_config.delegate.confirm()
|
||||
)
|
||||
if confirm:
|
||||
await self.host.send_command(
|
||||
HCI_User_Confirmation_Request_Reply_Command(
|
||||
@@ -2500,17 +2662,18 @@ class Device(CompositeEventEmitter):
|
||||
# Ask what the pairing config should be for this connection
|
||||
pairing_config = self.pairing_config_factory(connection)
|
||||
|
||||
can_input = pairing_config.delegate.io_capability in {
|
||||
can_input = pairing_config.delegate.io_capability in (
|
||||
smp.SMP_KEYBOARD_ONLY_IO_CAPABILITY,
|
||||
smp.SMP_KEYBOARD_DISPLAY_IO_CAPABILITY,
|
||||
}
|
||||
)
|
||||
|
||||
# Respond
|
||||
if can_input:
|
||||
|
||||
async def get_number():
|
||||
number = await connection.abort_on('disconnection',
|
||||
pairing_config.delegate.get_number())
|
||||
number = await connection.abort_on(
|
||||
'disconnection', pairing_config.delegate.get_number()
|
||||
)
|
||||
if number is not None:
|
||||
await self.host.send_command(
|
||||
HCI_User_Passkey_Request_Reply_Command(
|
||||
@@ -2539,7 +2702,9 @@ class Device(CompositeEventEmitter):
|
||||
# Ask what the pairing config should be for this connection
|
||||
pairing_config = self.pairing_config_factory(connection)
|
||||
|
||||
connection.abort_on('disconnection', pairing_config.delegate.display_number(passkey))
|
||||
connection.abort_on(
|
||||
'disconnection', pairing_config.delegate.display_number(passkey)
|
||||
)
|
||||
|
||||
# [Classic only]
|
||||
@host_event_handler
|
||||
@@ -2571,10 +2736,15 @@ class Device(CompositeEventEmitter):
|
||||
@with_connection_from_handle
|
||||
def on_connection_encryption_change(self, connection, encryption):
|
||||
logger.debug(
|
||||
f'*** Connection Encryption Change: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}, encryption={encryption}'
|
||||
f'*** Connection Encryption Change: [0x{connection.handle:04X}] '
|
||||
f'{connection.peer_address} as {connection.role_name}, '
|
||||
f'encryption={encryption}'
|
||||
)
|
||||
connection.encryption = encryption
|
||||
if not connection.authenticated and encryption == HCI_Encryption_Change_Event.AES_CCM:
|
||||
if (
|
||||
not connection.authenticated
|
||||
and encryption == HCI_Encryption_Change_Event.AES_CCM
|
||||
):
|
||||
connection.authenticated = True
|
||||
connection.sc = True
|
||||
connection.emit('connection_encryption_change')
|
||||
@@ -2583,7 +2753,9 @@ class Device(CompositeEventEmitter):
|
||||
@with_connection_from_handle
|
||||
def on_connection_encryption_failure(self, connection, error):
|
||||
logger.debug(
|
||||
f'*** Connection Encryption Failure: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}, error={error}'
|
||||
f'*** Connection Encryption Failure: [0x{connection.handle:04X}] '
|
||||
f'{connection.peer_address} as {connection.role_name}, '
|
||||
f'error={error}'
|
||||
)
|
||||
connection.emit('connection_encryption_failure', error)
|
||||
|
||||
@@ -2591,7 +2763,8 @@ class Device(CompositeEventEmitter):
|
||||
@with_connection_from_handle
|
||||
def on_connection_encryption_key_refresh(self, connection):
|
||||
logger.debug(
|
||||
f'*** Connection Key Refresh: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}'
|
||||
f'*** Connection Key Refresh: [0x{connection.handle:04X}] '
|
||||
f'{connection.peer_address} as {connection.role_name}'
|
||||
)
|
||||
connection.emit('connection_encryption_key_refresh')
|
||||
|
||||
@@ -2599,7 +2772,9 @@ class Device(CompositeEventEmitter):
|
||||
@with_connection_from_handle
|
||||
def on_connection_parameters_update(self, connection, connection_parameters):
|
||||
logger.debug(
|
||||
f'*** Connection Parameters Update: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}, {connection_parameters}'
|
||||
f'*** Connection Parameters Update: [0x{connection.handle:04X}] '
|
||||
f'{connection.peer_address} as {connection.role_name}, '
|
||||
f'{connection_parameters}'
|
||||
)
|
||||
connection.parameters = connection_parameters
|
||||
connection.emit('connection_parameters_update')
|
||||
@@ -2608,7 +2783,9 @@ class Device(CompositeEventEmitter):
|
||||
@with_connection_from_handle
|
||||
def on_connection_parameters_update_failure(self, connection, error):
|
||||
logger.debug(
|
||||
f'*** Connection Parameters Update Failed: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}, error={error}'
|
||||
f'*** Connection Parameters Update Failed: [0x{connection.handle:04X}] '
|
||||
f'{connection.peer_address} as {connection.role_name}, '
|
||||
f'error={error}'
|
||||
)
|
||||
connection.emit('connection_parameters_update_failure', error)
|
||||
|
||||
@@ -2616,7 +2793,9 @@ class Device(CompositeEventEmitter):
|
||||
@with_connection_from_handle
|
||||
def on_connection_phy_update(self, connection, connection_phy):
|
||||
logger.debug(
|
||||
f'*** Connection PHY Update: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}, {connection_phy}'
|
||||
f'*** Connection PHY Update: [0x{connection.handle:04X}] '
|
||||
f'{connection.peer_address} as {connection.role_name}, '
|
||||
f'{connection_phy}'
|
||||
)
|
||||
connection.phy = connection_phy
|
||||
connection.emit('connection_phy_update')
|
||||
@@ -2625,7 +2804,9 @@ class Device(CompositeEventEmitter):
|
||||
@with_connection_from_handle
|
||||
def on_connection_phy_update_failure(self, connection, error):
|
||||
logger.debug(
|
||||
f'*** Connection PHY Update Failed: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}, error={error}'
|
||||
f'*** Connection PHY Update Failed: [0x{connection.handle:04X}] '
|
||||
f'{connection.peer_address} as {connection.role_name}, '
|
||||
f'error={error}'
|
||||
)
|
||||
connection.emit('connection_phy_update_failure', error)
|
||||
|
||||
@@ -2633,7 +2814,9 @@ class Device(CompositeEventEmitter):
|
||||
@with_connection_from_handle
|
||||
def on_connection_att_mtu_update(self, connection, att_mtu):
|
||||
logger.debug(
|
||||
f'*** Connection ATT MTU Update: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}, {att_mtu}'
|
||||
f'*** Connection ATT MTU Update: [0x{connection.handle:04X}] '
|
||||
f'{connection.peer_address} as {connection.role_name}, '
|
||||
f'{att_mtu}'
|
||||
)
|
||||
connection.att_mtu = att_mtu
|
||||
connection.emit('connection_att_mtu_update')
|
||||
@@ -2644,7 +2827,8 @@ class Device(CompositeEventEmitter):
|
||||
self, connection, max_tx_octets, max_tx_time, max_rx_octets, max_rx_time
|
||||
):
|
||||
logger.debug(
|
||||
f'*** Connection Data Length Change: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}'
|
||||
f'*** Connection Data Length Change: [0x{connection.handle:04X}] '
|
||||
f'{connection.peer_address} as {connection.role_name}'
|
||||
)
|
||||
connection.data_length = (
|
||||
max_tx_octets,
|
||||
@@ -2677,14 +2861,14 @@ class Device(CompositeEventEmitter):
|
||||
# odd-numbered ones are server->client
|
||||
if att_pdu.op_code & 1:
|
||||
if connection.gatt_client is None:
|
||||
logger.warn(
|
||||
logger.warning(
|
||||
color('no GATT client for connection 0x{connection_handle:04X}')
|
||||
)
|
||||
return
|
||||
connection.gatt_client.on_gatt_pdu(att_pdu)
|
||||
else:
|
||||
if connection.gatt_server is None:
|
||||
logger.warn(
|
||||
logger.warning(
|
||||
color('no GATT server for connection 0x{connection_handle:04X}')
|
||||
)
|
||||
return
|
||||
@@ -2700,4 +2884,8 @@ class Device(CompositeEventEmitter):
|
||||
self.l2cap_channel_manager.on_pdu(connection, cid, pdu)
|
||||
|
||||
def __str__(self):
|
||||
return f'Device(name="{self.name}", random_address="{self.random_address}"", public_address="{self.public_address}")'
|
||||
return (
|
||||
f'Device(name="{self.name}", '
|
||||
f'random_address="{self.random_address}", '
|
||||
f'public_address="{self.public_address}")'
|
||||
)
|
||||
|
||||
@@ -25,14 +25,15 @@
|
||||
from __future__ import annotations
|
||||
import asyncio
|
||||
import enum
|
||||
import types
|
||||
import functools
|
||||
import logging
|
||||
from pyee import EventEmitter
|
||||
import struct
|
||||
from typing import Sequence
|
||||
from colors import color
|
||||
|
||||
from .core import *
|
||||
from .hci import *
|
||||
from .att import *
|
||||
from .core import UUID, get_dict_key_by_value
|
||||
from .att import Attribute
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -43,6 +44,7 @@ logger = logging.getLogger(__name__)
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
# fmt: off
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
GATT_REQUEST_TIMEOUT = 30 # seconds
|
||||
|
||||
@@ -177,6 +179,7 @@ GATT_BOOT_KEYBOARD_OUTPUT_REPORT_CHARACTERISTIC = UUID.from_16_bi
|
||||
GATT_CENTRAL_ADDRESS_RESOLUTION__CHARACTERISTIC = UUID.from_16_bits(0x2AA6, 'Central Address Resolution')
|
||||
|
||||
# fmt: on
|
||||
# pylint: enable=line-too-long
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -203,7 +206,7 @@ class Service(Attribute):
|
||||
|
||||
def __init__(self, uuid, characteristics: list[Characteristic], primary=True):
|
||||
# Convert the uuid to a UUID object if it isn't already
|
||||
if type(uuid) is str:
|
||||
if isinstance(uuid, str):
|
||||
uuid = UUID(uuid)
|
||||
|
||||
super().__init__(
|
||||
@@ -227,7 +230,12 @@ class Service(Attribute):
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
return f'Service(handle=0x{self.handle:04X}, end=0x{self.end_group_handle:04X}, uuid={self.uuid}){"" if self.primary else "*"}'
|
||||
return (
|
||||
f'Service(handle=0x{self.handle:04X}, '
|
||||
f'end=0x{self.end_group_handle:04X}, '
|
||||
f'uuid={self.uuid})'
|
||||
f'{"" if self.primary else "*"}'
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -271,15 +279,15 @@ class Characteristic(Attribute):
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def property_name(property):
|
||||
return Characteristic.PROPERTY_NAMES.get(property, '')
|
||||
def property_name(property_int):
|
||||
return Characteristic.PROPERTY_NAMES.get(property_int, '')
|
||||
|
||||
@staticmethod
|
||||
def properties_as_string(properties):
|
||||
return ','.join(
|
||||
[
|
||||
Characteristic.property_name(p)
|
||||
for p in Characteristic.PROPERTY_NAMES.keys()
|
||||
for p in Characteristic.PROPERTY_NAMES
|
||||
if properties & p
|
||||
]
|
||||
)
|
||||
@@ -298,11 +306,11 @@ class Characteristic(Attribute):
|
||||
properties,
|
||||
permissions,
|
||||
value=b'',
|
||||
descriptors: list[Descriptor] = [],
|
||||
descriptors: Sequence[Descriptor] = (),
|
||||
):
|
||||
super().__init__(uuid, permissions, value)
|
||||
self.uuid = self.type
|
||||
if type(properties) is str:
|
||||
if isinstance(properties, str):
|
||||
self.properties = Characteristic.string_to_properties(properties)
|
||||
else:
|
||||
self.properties = properties
|
||||
@@ -313,8 +321,15 @@ class Characteristic(Attribute):
|
||||
if descriptor.type == descriptor_type:
|
||||
return descriptor
|
||||
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
return f'Characteristic(handle=0x{self.handle:04X}, end=0x{self.end_group_handle:04X}, uuid={self.uuid}, properties={Characteristic.properties_as_string(self.properties)})'
|
||||
return (
|
||||
f'Characteristic(handle=0x{self.handle:04X}, '
|
||||
f'end=0x{self.end_group_handle:04X}, '
|
||||
f'uuid={self.uuid}, '
|
||||
f'properties={Characteristic.properties_as_string(self.properties)})'
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -335,7 +350,12 @@ class CharacteristicDeclaration(Attribute):
|
||||
self.characteristic = characteristic
|
||||
|
||||
def __str__(self):
|
||||
return f'CharacteristicDeclaration(handle=0x{self.handle:04X}, value_handle=0x{self.value_handle:04X}, uuid={self.characteristic.uuid}, properties={Characteristic.properties_as_string(self.characteristic.properties)})'
|
||||
return (
|
||||
f'CharacteristicDeclaration(handle=0x{self.handle:04X}, '
|
||||
f'value_handle=0x{self.value_handle:04X}, '
|
||||
f'uuid={self.characteristic.uuid}, properties='
|
||||
f'{Characteristic.properties_as_string(self.characteristic.properties)})'
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -395,14 +415,14 @@ class CharacteristicAdapter:
|
||||
return getattr(self.wrapped_characteristic, name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
if name in {
|
||||
if name in (
|
||||
'wrapped_characteristic',
|
||||
'subscribers',
|
||||
'read_value',
|
||||
'write_value',
|
||||
'subscribe',
|
||||
'unsubscribe',
|
||||
}:
|
||||
):
|
||||
super().__setattr__(name, value)
|
||||
else:
|
||||
setattr(self.wrapped_characteristic, name, value)
|
||||
@@ -486,9 +506,9 @@ class PackedCharacteristicAdapter(CharacteristicAdapter):
|
||||
the format.
|
||||
'''
|
||||
|
||||
def __init__(self, characteristic, format):
|
||||
def __init__(self, characteristic, pack_format):
|
||||
super().__init__(characteristic)
|
||||
self.struct = struct.Struct(format)
|
||||
self.struct = struct.Struct(pack_format)
|
||||
|
||||
def pack(self, *values):
|
||||
return self.struct.pack(*values)
|
||||
@@ -497,7 +517,7 @@ class PackedCharacteristicAdapter(CharacteristicAdapter):
|
||||
return self.struct.unpack(buffer)
|
||||
|
||||
def encode_value(self, value):
|
||||
return self.pack(*value if type(value) is tuple else (value,))
|
||||
return self.pack(*value if isinstance(value, tuple) else (value,))
|
||||
|
||||
def decode_value(self, value):
|
||||
unpacked = self.unpack(value)
|
||||
@@ -510,14 +530,15 @@ class MappedCharacteristicAdapter(PackedCharacteristicAdapter):
|
||||
Adapter that packs/unpacks characteristic values according to a standard
|
||||
Python `struct` format.
|
||||
The adapted `read_value` and `write_value` methods return/accept aa dictionary which
|
||||
is packed/unpacked according to format, with the arguments extracted from the dictionary
|
||||
by key, in the same order as they occur in the `keys` parameter.
|
||||
is packed/unpacked according to format, with the arguments extracted from the
|
||||
dictionary by key, in the same order as they occur in the `keys` parameter.
|
||||
'''
|
||||
|
||||
def __init__(self, characteristic, format, keys):
|
||||
super().__init__(characteristic, format)
|
||||
def __init__(self, characteristic, pack_format, keys):
|
||||
super().__init__(characteristic, pack_format)
|
||||
self.keys = keys
|
||||
|
||||
# pylint: disable=arguments-differ
|
||||
def pack(self, values):
|
||||
return super().pack(*(values[key] for key in self.keys))
|
||||
|
||||
@@ -544,16 +565,18 @@ class Descriptor(Attribute):
|
||||
See Vol 3, Part G - 3.3.3 Characteristic Descriptor Declarations
|
||||
'''
|
||||
|
||||
def __init__(self, descriptor_type, permissions, value=b''):
|
||||
super().__init__(descriptor_type, permissions, value)
|
||||
|
||||
def __str__(self):
|
||||
return f'Descriptor(handle=0x{self.handle:04X}, type={self.type}, value={self.read_value(None).hex()})'
|
||||
return (
|
||||
f'Descriptor(handle=0x{self.handle:04X}, '
|
||||
f'type={self.type}, '
|
||||
f'value={self.read_value(None).hex()})'
|
||||
)
|
||||
|
||||
|
||||
class ClientCharacteristicConfigurationBits(enum.IntFlag):
|
||||
'''
|
||||
See Vol 3, Part G - 3.3.3.3 - Table 3.11 Client Characteristic Configuration bit field definition
|
||||
See Vol 3, Part G - 3.3.3.3 - Table 3.11 Client Characteristic Configuration bit
|
||||
field definition
|
||||
'''
|
||||
|
||||
DEFAULT = 0x0000
|
||||
|
||||
@@ -28,9 +28,31 @@ import logging
|
||||
import struct
|
||||
|
||||
from colors import color
|
||||
from pyee import EventEmitter
|
||||
|
||||
from .att import *
|
||||
from .core import InvalidStateError, ProtocolError, TimeoutError
|
||||
from .hci import HCI_Constant
|
||||
from .att import (
|
||||
ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
||||
ATT_ATTRIBUTE_NOT_LONG_ERROR,
|
||||
ATT_CID,
|
||||
ATT_DEFAULT_MTU,
|
||||
ATT_ERROR_RESPONSE,
|
||||
ATT_INVALID_OFFSET_ERROR,
|
||||
ATT_PDU,
|
||||
ATT_RESPONSES,
|
||||
ATT_Exchange_MTU_Request,
|
||||
ATT_Find_By_Type_Value_Request,
|
||||
ATT_Find_Information_Request,
|
||||
ATT_Handle_Value_Confirmation,
|
||||
ATT_Read_Blob_Request,
|
||||
ATT_Read_By_Group_Type_Request,
|
||||
ATT_Read_By_Type_Request,
|
||||
ATT_Read_Request,
|
||||
ATT_Write_Command,
|
||||
ATT_Write_Request,
|
||||
)
|
||||
from . import core
|
||||
from .core import UUID, InvalidStateError, ProtocolError
|
||||
from .gatt import (
|
||||
GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
|
||||
GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
|
||||
@@ -40,7 +62,6 @@ from .gatt import (
|
||||
Characteristic,
|
||||
ClientCharacteristicConfigurationBits,
|
||||
)
|
||||
from .hci import *
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -76,16 +97,17 @@ class AttributeProxy(EventEmitter):
|
||||
return value_bytes
|
||||
|
||||
def __str__(self):
|
||||
return f'Attribute(handle=0x{self.handle:04X}, type={self.uuid})'
|
||||
return f'Attribute(handle=0x{self.handle:04X}, type={self.type})'
|
||||
|
||||
|
||||
class ServiceProxy(AttributeProxy):
|
||||
@staticmethod
|
||||
def from_client(cls, client, service_uuid):
|
||||
# The service and its characteristics are considered to have already been discovered
|
||||
def from_client(service_class, client, service_uuid):
|
||||
# The service and its characteristics are considered to have already been
|
||||
# discovered
|
||||
services = client.get_services_by_uuid(service_uuid)
|
||||
service = services[0] if services else None
|
||||
return cls(service) if service else None
|
||||
return service_class(service) if service else None
|
||||
|
||||
def __init__(self, client, handle, end_group_handle, uuid, primary=True):
|
||||
attribute_type = (
|
||||
@@ -97,7 +119,7 @@ class ServiceProxy(AttributeProxy):
|
||||
self.uuid = uuid
|
||||
self.characteristics = []
|
||||
|
||||
async def discover_characteristics(self, uuids=[]):
|
||||
async def discover_characteristics(self, uuids=()):
|
||||
return await self.client.discover_characteristics(uuids, self)
|
||||
|
||||
def get_characteristics_by_uuid(self, uuid):
|
||||
@@ -121,6 +143,8 @@ class CharacteristicProxy(AttributeProxy):
|
||||
if descriptor.type == descriptor_type:
|
||||
return descriptor
|
||||
|
||||
return None
|
||||
|
||||
async def discover_descriptors(self):
|
||||
return await self.client.discover_descriptors(self)
|
||||
|
||||
@@ -148,7 +172,11 @@ class CharacteristicProxy(AttributeProxy):
|
||||
return await self.client.unsubscribe(self, subscriber)
|
||||
|
||||
def __str__(self):
|
||||
return f'Characteristic(handle=0x{self.handle:04X}, uuid={self.uuid}, properties={Characteristic.properties_as_string(self.properties)})'
|
||||
return (
|
||||
f'Characteristic(handle=0x{self.handle:04X}, '
|
||||
f'uuid={self.uuid}, '
|
||||
f'properties={Characteristic.properties_as_string(self.properties)})'
|
||||
)
|
||||
|
||||
|
||||
class DescriptorProxy(AttributeProxy):
|
||||
@@ -214,9 +242,9 @@ class Client:
|
||||
response = await asyncio.wait_for(
|
||||
self.pending_response, GATT_REQUEST_TIMEOUT
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
except asyncio.TimeoutError as error:
|
||||
logger.warning(color('!!! GATT Request timeout', 'red'))
|
||||
raise TimeoutError(f'GATT timeout for {request.name}')
|
||||
raise core.TimeoutError(f'GATT timeout for {request.name}') from error
|
||||
finally:
|
||||
self.pending_request = None
|
||||
self.pending_response = None
|
||||
@@ -225,7 +253,8 @@ class Client:
|
||||
|
||||
def send_confirmation(self, confirmation):
|
||||
logger.debug(
|
||||
f'GATT Confirmation from client: [0x{self.connection.handle:04X}] {confirmation}'
|
||||
f'GATT Confirmation from client: [0x{self.connection.handle:04X}] '
|
||||
f'{confirmation}'
|
||||
)
|
||||
self.send_gatt_pdu(confirmation.to_bytes())
|
||||
|
||||
@@ -300,7 +329,8 @@ class Client:
|
||||
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
# Unexpected end
|
||||
logger.warning(
|
||||
f'!!! unexpected error while discovering services: {HCI_Constant.error_name(response.error_code)}'
|
||||
'!!! unexpected error while discovering services: '
|
||||
f'{HCI_Constant.error_name(response.error_code)}'
|
||||
)
|
||||
# TODO raise appropriate exception
|
||||
return
|
||||
@@ -352,7 +382,7 @@ class Client:
|
||||
'''
|
||||
|
||||
# Force uuid to be a UUID object
|
||||
if type(uuid) is str:
|
||||
if isinstance(uuid, str):
|
||||
uuid = UUID(uuid)
|
||||
|
||||
starting_handle = 0x0001
|
||||
@@ -375,7 +405,8 @@ class Client:
|
||||
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
# Unexpected end
|
||||
logger.warning(
|
||||
f'!!! unexpected error while discovering services: {HCI_Constant.error_name(response.error_code)}'
|
||||
'!!! unexpected error while discovering services: '
|
||||
f'{HCI_Constant.error_name(response.error_code)}'
|
||||
)
|
||||
# TODO raise appropriate exception
|
||||
return
|
||||
@@ -414,7 +445,7 @@ class Client:
|
||||
|
||||
return services
|
||||
|
||||
async def discover_included_services(self, service):
|
||||
async def discover_included_services(self, _service):
|
||||
'''
|
||||
See Vol 3, Part G - 4.5.1 Find Included Services
|
||||
'''
|
||||
@@ -423,11 +454,12 @@ class Client:
|
||||
|
||||
async def discover_characteristics(self, uuids, service):
|
||||
'''
|
||||
See Vol 3, Part G - 4.6.1 Discover All Characteristics of a Service and 4.6.2 Discover Characteristics by UUID
|
||||
See Vol 3, Part G - 4.6.1 Discover All Characteristics of a Service and 4.6.2
|
||||
Discover Characteristics by UUID
|
||||
'''
|
||||
|
||||
# Cast the UUIDs type from string to object if needed
|
||||
uuids = [UUID(uuid) if type(uuid) is str else uuid for uuid in uuids]
|
||||
uuids = [UUID(uuid) if isinstance(uuid, str) else uuid for uuid in uuids]
|
||||
|
||||
# Decide which services to discover for
|
||||
services = [service] if service else self.services
|
||||
@@ -456,7 +488,8 @@ class Client:
|
||||
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
# Unexpected end
|
||||
logger.warning(
|
||||
f'!!! unexpected error while discovering characteristics: {HCI_Constant.error_name(response.error_code)}'
|
||||
'!!! unexpected error while discovering characteristics: '
|
||||
f'{HCI_Constant.error_name(response.error_code)}'
|
||||
)
|
||||
# TODO raise appropriate exception
|
||||
return
|
||||
@@ -532,7 +565,8 @@ class Client:
|
||||
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
# Unexpected end
|
||||
logger.warning(
|
||||
f'!!! unexpected error while discovering descriptors: {HCI_Constant.error_name(response.error_code)}'
|
||||
'!!! unexpected error while discovering descriptors: '
|
||||
f'{HCI_Constant.error_name(response.error_code)}'
|
||||
)
|
||||
# TODO raise appropriate exception
|
||||
return []
|
||||
@@ -585,7 +619,8 @@ class Client:
|
||||
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
# Unexpected end
|
||||
logger.warning(
|
||||
f'!!! unexpected error while discovering attributes: {HCI_Constant.error_name(response.error_code)}'
|
||||
'!!! unexpected error while discovering attributes: '
|
||||
f'{HCI_Constant.error_name(response.error_code)}'
|
||||
)
|
||||
return []
|
||||
break
|
||||
@@ -607,7 +642,8 @@ class Client:
|
||||
return attributes
|
||||
|
||||
async def subscribe(self, characteristic, subscriber=None, prefer_notify=True):
|
||||
# If we haven't already discovered the descriptors for this characteristic, do it now
|
||||
# If we haven't already discovered the descriptors for this characteristic,
|
||||
# do it now
|
||||
if not characteristic.descriptors_discovered:
|
||||
await self.discover_descriptors(characteristic)
|
||||
|
||||
@@ -642,14 +678,16 @@ class Client:
|
||||
subscriber_set = subscribers.setdefault(characteristic.handle, set())
|
||||
if subscriber is not None:
|
||||
subscriber_set.add(subscriber)
|
||||
# Add the characteristic as a subscriber, which will result in the characteristic
|
||||
# emitting an 'update' event when a notification or indication is received
|
||||
# Add the characteristic as a subscriber, which will result in the
|
||||
# characteristic emitting an 'update' event when a notification or indication
|
||||
# is received
|
||||
subscriber_set.add(characteristic)
|
||||
|
||||
await self.write_value(cccd, struct.pack('<H', bits), with_response=True)
|
||||
|
||||
async def unsubscribe(self, characteristic, subscriber=None):
|
||||
# If we haven't already discovered the descriptors for this characteristic, do it now
|
||||
# If we haven't already discovered the descriptors for this characteristic,
|
||||
# do it now
|
||||
if not characteristic.descriptors_discovered:
|
||||
await self.discover_descriptors(characteristic)
|
||||
|
||||
@@ -673,7 +711,7 @@ class Client:
|
||||
|
||||
# Cleanup if we removed the last one
|
||||
if not subscribers:
|
||||
subscriber_set.remove(characteristic.handle)
|
||||
del subscriber_set[characteristic.handle]
|
||||
else:
|
||||
# Remove all subscribers for this attribute from the sets!
|
||||
self.notification_subscribers.pop(characteristic.handle, None)
|
||||
@@ -691,7 +729,7 @@ class Client:
|
||||
'''
|
||||
|
||||
# Send a request to read
|
||||
attribute_handle = attribute if type(attribute) is int else attribute.handle
|
||||
attribute_handle = attribute if isinstance(attribute, int) else attribute.handle
|
||||
response = await self.send_request(
|
||||
ATT_Read_Request(attribute_handle=attribute_handle)
|
||||
)
|
||||
@@ -720,9 +758,9 @@ class Client:
|
||||
if response is None:
|
||||
raise TimeoutError('read timeout')
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
if (
|
||||
response.error_code == ATT_ATTRIBUTE_NOT_LONG_ERROR
|
||||
or response.error_code == ATT_INVALID_OFFSET_ERROR
|
||||
if response.error_code in (
|
||||
ATT_ATTRIBUTE_NOT_LONG_ERROR,
|
||||
ATT_INVALID_OFFSET_ERROR,
|
||||
):
|
||||
break
|
||||
raise ProtocolError(
|
||||
@@ -773,7 +811,8 @@ class Client:
|
||||
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
# Unexpected end
|
||||
logger.warning(
|
||||
f'!!! unexpected error while reading characteristics: {HCI_Constant.error_name(response.error_code)}'
|
||||
'!!! unexpected error while reading characteristics: '
|
||||
f'{HCI_Constant.error_name(response.error_code)}'
|
||||
)
|
||||
# TODO raise appropriate exception
|
||||
return []
|
||||
@@ -799,13 +838,14 @@ class Client:
|
||||
|
||||
async def write_value(self, attribute, value, with_response=False):
|
||||
'''
|
||||
See Vol 3, Part G - 4.9.1 Write Without Response & 4.9.3 Write Characteristic Value
|
||||
See Vol 3, Part G - 4.9.1 Write Without Response & 4.9.3 Write Characteristic
|
||||
Value
|
||||
|
||||
`attribute` can be an Attribute object, or a handle value
|
||||
'''
|
||||
|
||||
# Send a request or command to write
|
||||
attribute_handle = attribute if type(attribute) is int else attribute.handle
|
||||
attribute_handle = attribute if isinstance(attribute, int) else attribute.handle
|
||||
if with_response:
|
||||
response = await self.send_request(
|
||||
ATT_Write_Request(
|
||||
@@ -836,7 +876,8 @@ class Client:
|
||||
logger.warning('!!! unexpected response, there is no pending request')
|
||||
return
|
||||
|
||||
# Sanity check: the response should match the pending request unless it is an error response
|
||||
# Sanity check: the response should match the pending request unless it is
|
||||
# an error response
|
||||
if att_pdu.op_code != ATT_ERROR_RESPONSE:
|
||||
expected_response_name = self.pending_request.name.replace(
|
||||
'_REQUEST', '_RESPONSE'
|
||||
@@ -856,7 +897,12 @@ class Client:
|
||||
handler(att_pdu)
|
||||
else:
|
||||
logger.warning(
|
||||
f'{color(f"--- Ignoring GATT Response from [0x{self.connection.handle:04X}]:", "red")} {att_pdu}'
|
||||
color(
|
||||
'--- Ignoring GATT Response from '
|
||||
f'[0x{self.connection.handle:04X}]: ',
|
||||
'red',
|
||||
)
|
||||
+ str(att_pdu)
|
||||
)
|
||||
|
||||
def on_att_handle_value_notification(self, notification):
|
||||
|
||||
@@ -26,14 +26,53 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
import struct
|
||||
from typing import Tuple, Optional
|
||||
from pyee import EventEmitter
|
||||
from colors import color
|
||||
|
||||
from .core import *
|
||||
from .hci import *
|
||||
from .att import *
|
||||
from .gatt import *
|
||||
from .core import UUID
|
||||
from .att import (
|
||||
ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
||||
ATT_ATTRIBUTE_NOT_LONG_ERROR,
|
||||
ATT_CID,
|
||||
ATT_DEFAULT_MTU,
|
||||
ATT_INVALID_ATTRIBUTE_LENGTH_ERROR,
|
||||
ATT_INVALID_HANDLE_ERROR,
|
||||
ATT_INVALID_OFFSET_ERROR,
|
||||
ATT_REQUEST_NOT_SUPPORTED_ERROR,
|
||||
ATT_REQUESTS,
|
||||
ATT_UNLIKELY_ERROR_ERROR,
|
||||
ATT_UNSUPPORTED_GROUP_TYPE_ERROR,
|
||||
ATT_Error,
|
||||
ATT_Error_Response,
|
||||
ATT_Exchange_MTU_Response,
|
||||
ATT_Find_By_Type_Value_Response,
|
||||
ATT_Find_Information_Response,
|
||||
ATT_Handle_Value_Indication,
|
||||
ATT_Handle_Value_Notification,
|
||||
ATT_Read_Blob_Response,
|
||||
ATT_Read_By_Group_Type_Response,
|
||||
ATT_Read_By_Type_Response,
|
||||
ATT_Read_Response,
|
||||
ATT_Write_Response,
|
||||
Attribute,
|
||||
)
|
||||
from .gatt import (
|
||||
GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
|
||||
GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
|
||||
GATT_INCLUDE_ATTRIBUTE_TYPE,
|
||||
GATT_MAX_ATTRIBUTE_VALUE_SIZE,
|
||||
GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
|
||||
GATT_REQUEST_TIMEOUT,
|
||||
GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE,
|
||||
Characteristic,
|
||||
CharacteristicDeclaration,
|
||||
CharacteristicValue,
|
||||
Descriptor,
|
||||
Service,
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -194,6 +233,7 @@ class Server(EventEmitter):
|
||||
is None
|
||||
):
|
||||
self.add_attribute(
|
||||
# pylint: disable=line-too-long
|
||||
Descriptor(
|
||||
GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
|
||||
Attribute.READABLE | Attribute.WRITEABLE,
|
||||
@@ -232,12 +272,13 @@ class Server(EventEmitter):
|
||||
|
||||
def write_cccd(self, connection, characteristic, value):
|
||||
logger.debug(
|
||||
f'Subscription update for connection=0x{connection.handle:04X}, handle=0x{characteristic.handle:04X}: {value.hex()}'
|
||||
f'Subscription update for connection=0x{connection.handle:04X}, '
|
||||
f'handle=0x{characteristic.handle:04X}: {value.hex()}'
|
||||
)
|
||||
|
||||
# Sanity check
|
||||
if len(value) != 2:
|
||||
logger.warn('CCCD value not 2 bytes long')
|
||||
logger.warning('CCCD value not 2 bytes long')
|
||||
return
|
||||
|
||||
cccds = self.subscribers.setdefault(connection.handle, {})
|
||||
@@ -349,9 +390,9 @@ class Server(EventEmitter):
|
||||
await asyncio.wait_for(
|
||||
self.pending_confirmations[connection.handle], GATT_REQUEST_TIMEOUT
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
except asyncio.TimeoutError as error:
|
||||
logger.warning(color('!!! GATT Indicate timeout', 'red'))
|
||||
raise TimeoutError(f'GATT timeout for {indication.name}')
|
||||
raise TimeoutError(f'GATT timeout for {indication.name}') from error
|
||||
finally:
|
||||
self.pending_confirmations[connection.handle] = None
|
||||
|
||||
@@ -425,7 +466,11 @@ class Server(EventEmitter):
|
||||
else:
|
||||
# Just ignore
|
||||
logger.warning(
|
||||
f'{color("--- Ignoring GATT Request from [0x{connection.handle:04X}]:", "red")} {att_pdu}'
|
||||
color(
|
||||
f'--- Ignoring GATT Request from [0x{connection.handle:04X}]: ',
|
||||
'red',
|
||||
)
|
||||
+ str(att_pdu)
|
||||
)
|
||||
|
||||
#######################################################
|
||||
@@ -436,7 +481,10 @@ class Server(EventEmitter):
|
||||
Handler for requests without a more specific handler
|
||||
'''
|
||||
logger.warning(
|
||||
f'{color(f"--- Unsupported ATT Request from [0x{connection.handle:04X}]:", "red")} {pdu}'
|
||||
color(
|
||||
f'--- Unsupported ATT Request from [0x{connection.handle:04X}]: ', 'red'
|
||||
)
|
||||
+ str(pdu)
|
||||
)
|
||||
response = ATT_Error_Response(
|
||||
request_opcode_in_error=pdu.op_code,
|
||||
@@ -556,11 +604,11 @@ class Server(EventEmitter):
|
||||
if attributes:
|
||||
handles_information_list = []
|
||||
for attribute in attributes:
|
||||
if attribute.type in {
|
||||
if attribute.type in (
|
||||
GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
|
||||
GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE,
|
||||
GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
|
||||
}:
|
||||
):
|
||||
# Part of a group
|
||||
group_end_handle = attribute.end_group_handle
|
||||
else:
|
||||
@@ -692,11 +740,11 @@ class Server(EventEmitter):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.4.9 Read by Group Type Request
|
||||
'''
|
||||
if request.attribute_group_type not in {
|
||||
if request.attribute_group_type not in (
|
||||
GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
|
||||
GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE,
|
||||
GATT_INCLUDE_ATTRIBUTE_TYPE,
|
||||
}:
|
||||
):
|
||||
response = ATT_Error_Response(
|
||||
request_opcode_in_error=request.op_code,
|
||||
attribute_handle_in_error=request.starting_handle,
|
||||
@@ -814,7 +862,7 @@ class Server(EventEmitter):
|
||||
except Exception as error:
|
||||
logger.warning(f'!!! ignoring exception: {error}')
|
||||
|
||||
def on_att_handle_value_confirmation(self, connection, confirmation):
|
||||
def on_att_handle_value_confirmation(self, connection, _confirmation):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.7.3 Handle Value Confirmation
|
||||
'''
|
||||
|
||||
271
bumble/hci.py
271
bumble/hci.py
@@ -21,7 +21,16 @@ import logging
|
||||
import functools
|
||||
from colors import color
|
||||
|
||||
from .core import *
|
||||
from .core import (
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
AdvertisingData,
|
||||
DeviceClass,
|
||||
ProtocolError,
|
||||
bit_flags_to_strings,
|
||||
name_or_number,
|
||||
padded_bytes,
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -43,8 +52,8 @@ def key_with_value(dictionary, target_value):
|
||||
return None
|
||||
|
||||
|
||||
def indent_lines(str):
|
||||
return '\n'.join([' ' + line for line in str.split('\n')])
|
||||
def indent_lines(string):
|
||||
return '\n'.join([' ' + line for line in string.split('\n')])
|
||||
|
||||
|
||||
def map_null_terminated_utf8_string(utf8_bytes):
|
||||
@@ -63,25 +72,32 @@ def map_class_of_device(class_of_device):
|
||||
major_device_class,
|
||||
minor_device_class,
|
||||
) = DeviceClass.split_class_of_device(class_of_device)
|
||||
return f'[{class_of_device:06X}] Services({",".join(DeviceClass.service_class_labels(service_classes))}),Class({DeviceClass.major_device_class_name(major_device_class)}|{DeviceClass.minor_device_class_name(major_device_class, minor_device_class)})'
|
||||
return (
|
||||
f'[{class_of_device:06X}] Services('
|
||||
f'{",".join(DeviceClass.service_class_labels(service_classes))}),'
|
||||
f'Class({DeviceClass.major_device_class_name(major_device_class)}|'
|
||||
f'{DeviceClass.minor_device_class_name(major_device_class, minor_device_class)}'
|
||||
')'
|
||||
)
|
||||
|
||||
|
||||
def phy_list_to_bits(phys):
|
||||
if phys is None:
|
||||
return 0
|
||||
else:
|
||||
phy_bits = 0
|
||||
for phy in phys:
|
||||
if phy not in HCI_LE_PHY_TYPE_TO_BIT:
|
||||
raise ValueError('invalid PHY')
|
||||
phy_bits |= 1 << HCI_LE_PHY_TYPE_TO_BIT[phy]
|
||||
return phy_bits
|
||||
|
||||
phy_bits = 0
|
||||
for phy in phys:
|
||||
if phy not in HCI_LE_PHY_TYPE_TO_BIT:
|
||||
raise ValueError('invalid PHY')
|
||||
phy_bits |= 1 << HCI_LE_PHY_TYPE_TO_BIT[phy]
|
||||
return phy_bits
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
# fmt: off
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
# HCI Version
|
||||
HCI_VERSION_BLUETOOTH_CORE_1_0B = 0
|
||||
@@ -1355,8 +1371,11 @@ HCI_LE_SUPPORTED_FEATURES_NAMES = {
|
||||
}
|
||||
|
||||
# fmt: on
|
||||
# pylint: enable=line-too-long
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# pylint: disable-next=unnecessary-lambda
|
||||
STATUS_SPEC = {'size': 1, 'mapper': lambda x: HCI_Constant.status_name(x)}
|
||||
|
||||
|
||||
@@ -1418,25 +1437,25 @@ class HCI_StatusError(ProtocolError):
|
||||
# -----------------------------------------------------------------------------
|
||||
class HCI_Object:
|
||||
@staticmethod
|
||||
def init_from_fields(object, fields, values):
|
||||
if type(values) is dict:
|
||||
def init_from_fields(hci_object, fields, values):
|
||||
if isinstance(values, dict):
|
||||
for field_name, _ in fields:
|
||||
setattr(object, field_name, values[field_name])
|
||||
setattr(hci_object, field_name, values[field_name])
|
||||
else:
|
||||
for field_name, field_value in zip(fields, values):
|
||||
setattr(object, field_name, field_value)
|
||||
setattr(hci_object, field_name, field_value)
|
||||
|
||||
@staticmethod
|
||||
def init_from_bytes(object, data, offset, fields):
|
||||
def init_from_bytes(hci_object, data, offset, fields):
|
||||
parsed = HCI_Object.dict_from_bytes(data, offset, fields)
|
||||
HCI_Object.init_from_fields(object, parsed.keys(), parsed.values())
|
||||
HCI_Object.init_from_fields(hci_object, parsed.keys(), parsed.values())
|
||||
|
||||
@staticmethod
|
||||
def dict_from_bytes(data, offset, fields):
|
||||
result = collections.OrderedDict()
|
||||
for (field_name, field_type) in fields:
|
||||
# The field_type may be a dictionary with a mapper, parser, and/or size
|
||||
if type(field_type) is dict:
|
||||
if isinstance(field_type, dict):
|
||||
if 'size' in field_type:
|
||||
field_type = field_type['size']
|
||||
elif 'parser' in field_type:
|
||||
@@ -1480,7 +1499,7 @@ class HCI_Object:
|
||||
# 32-bit unsigned big-endian
|
||||
field_value = struct.unpack_from('>I', data, offset)[0]
|
||||
offset += 4
|
||||
elif type(field_type) is int and field_type > 4 and field_type <= 256:
|
||||
elif isinstance(field_type, int) and 4 < field_type <= 256:
|
||||
# Byte array (from 5 up to 256 bytes)
|
||||
field_value = data[offset : offset + field_type]
|
||||
offset += field_type
|
||||
@@ -1494,19 +1513,20 @@ class HCI_Object:
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def dict_to_bytes(object, fields):
|
||||
def dict_to_bytes(hci_object, fields):
|
||||
result = bytearray()
|
||||
for (field_name, field_type) in fields:
|
||||
# The field_type may be a dictionary with a mapper, parser, serializer, and/or size
|
||||
# The field_type may be a dictionary with a mapper, parser, serializer,
|
||||
# and/or size
|
||||
serializer = None
|
||||
if type(field_type) is dict:
|
||||
if isinstance(field_type, dict):
|
||||
if 'serializer' in field_type:
|
||||
serializer = field_type['serializer']
|
||||
if 'size' in field_type:
|
||||
field_type = field_type['size']
|
||||
|
||||
# Serialize the field
|
||||
field_value = object[field_name]
|
||||
field_value = hci_object[field_name]
|
||||
if serializer:
|
||||
field_bytes = serializer(field_value)
|
||||
elif field_type == 1:
|
||||
@@ -1534,20 +1554,18 @@ class HCI_Object:
|
||||
# 32-bit unsigned big-endian
|
||||
field_bytes = struct.pack('>I', field_value)
|
||||
elif field_type == '*':
|
||||
if type(field_value) is int:
|
||||
if field_value >= 0 and field_value <= 255:
|
||||
if isinstance(field_value, int):
|
||||
if 0 <= field_value <= 255:
|
||||
field_bytes = bytes([field_value])
|
||||
else:
|
||||
raise ValueError('value too large for *-typed field')
|
||||
else:
|
||||
field_bytes = bytes(field_value)
|
||||
elif (
|
||||
type(field_value) is bytes
|
||||
or type(field_value) is bytearray
|
||||
or hasattr(field_value, 'to_bytes')
|
||||
elif isinstance(field_value, (bytes, bytearray)) or hasattr(
|
||||
field_value, 'to_bytes'
|
||||
):
|
||||
field_bytes = bytes(field_value)
|
||||
if type(field_type) is int and field_type > 4 and field_type <= 256:
|
||||
if isinstance(field_type, int) and 4 < field_type <= 256:
|
||||
# Truncate or Pad with zeros if the field is too long or too short
|
||||
if len(field_bytes) < field_type:
|
||||
field_bytes += bytes(field_type - len(field_bytes))
|
||||
@@ -1584,42 +1602,44 @@ class HCI_Object:
|
||||
|
||||
@staticmethod
|
||||
def format_field_value(value, indentation):
|
||||
if type(value) is bytes:
|
||||
if isinstance(value, bytes):
|
||||
return value.hex()
|
||||
elif isinstance(value, HCI_Object):
|
||||
|
||||
if isinstance(value, HCI_Object):
|
||||
return '\n' + value.to_string(indentation)
|
||||
else:
|
||||
return str(value)
|
||||
|
||||
return str(value)
|
||||
|
||||
@staticmethod
|
||||
def format_fields(object, keys, indentation='', value_mappers={}):
|
||||
def format_fields(hci_object, keys, indentation='', value_mappers=None):
|
||||
if not keys:
|
||||
return ''
|
||||
|
||||
# Measure the widest field name
|
||||
max_field_name_length = max(
|
||||
[len(key[0] if type(key) is tuple else key) for key in keys]
|
||||
(len(key[0] if isinstance(key, tuple) else key) for key in keys)
|
||||
)
|
||||
|
||||
# Build array of formatted key:value pairs
|
||||
fields = []
|
||||
for key in keys:
|
||||
value_mapper = None
|
||||
if type(key) is tuple:
|
||||
if isinstance(key, tuple):
|
||||
# The key has an associated specifier
|
||||
key, specifier = key
|
||||
|
||||
# Get the value mapper from the specifier
|
||||
if type(specifier) is dict:
|
||||
if isinstance(specifier, dict):
|
||||
value_mapper = specifier.get('mapper')
|
||||
|
||||
# Get the value for the field
|
||||
value = object[key]
|
||||
value = hci_object[key]
|
||||
|
||||
# Map the value if needed
|
||||
value_mapper = value_mappers.get(key, value_mapper)
|
||||
if value_mapper is not None:
|
||||
value = value_mapper(value)
|
||||
if value_mappers:
|
||||
value_mapper = value_mappers.get(key, value_mapper)
|
||||
if value_mapper is not None:
|
||||
value = value_mapper(value)
|
||||
|
||||
# Get the string representation of the value
|
||||
value_str = HCI_Object.format_field_value(
|
||||
@@ -1639,7 +1659,7 @@ class HCI_Object:
|
||||
self.fields = fields
|
||||
self.init_from_fields(self, fields, kwargs)
|
||||
|
||||
def to_string(self, indentation='', value_mappers={}):
|
||||
def to_string(self, indentation='', value_mappers=None):
|
||||
return HCI_Object.format_fields(
|
||||
self.__dict__, self.fields, indentation, value_mappers
|
||||
)
|
||||
@@ -1670,6 +1690,7 @@ class Address:
|
||||
RANDOM_IDENTITY_ADDRESS: 'RANDOM_IDENTITY_ADDRESS',
|
||||
}
|
||||
|
||||
# pylint: disable-next=unnecessary-lambda
|
||||
ADDRESS_TYPE_SPEC = {'size': 1, 'mapper': lambda x: Address.address_type_name(x)}
|
||||
|
||||
@staticmethod
|
||||
@@ -1686,7 +1707,8 @@ class Address:
|
||||
|
||||
@staticmethod
|
||||
def parse_address(data, offset):
|
||||
# Fix the type to a default value. This is used for parsing type-less Classic addresses
|
||||
# Fix the type to a default value. This is used for parsing type-less Classic
|
||||
# addresses
|
||||
return Address.parse_address_with_type(
|
||||
data, offset, Address.PUBLIC_DEVICE_ADDRESS
|
||||
)
|
||||
@@ -1705,10 +1727,10 @@ class Address:
|
||||
Initialize an instance. `address` may be a byte array in little-endian
|
||||
format, or a hex string in big-endian format (with optional ':'
|
||||
separators between the bytes).
|
||||
If the address is a string suffixed with '/P', `address_type` is ignored and the type
|
||||
is set to PUBLIC_DEVICE_ADDRESS.
|
||||
If the address is a string suffixed with '/P', `address_type` is ignored and
|
||||
the type is set to PUBLIC_DEVICE_ADDRESS.
|
||||
'''
|
||||
if type(address) is bytes:
|
||||
if isinstance(address, bytes):
|
||||
self.address_bytes = address
|
||||
else:
|
||||
# Check if there's a '/P' type specifier
|
||||
@@ -1731,9 +1753,9 @@ class Address:
|
||||
|
||||
@property
|
||||
def is_public(self):
|
||||
return (
|
||||
self.address_type == self.PUBLIC_DEVICE_ADDRESS
|
||||
or self.address_type == self.PUBLIC_IDENTITY_ADDRESS
|
||||
return self.address_type in (
|
||||
self.PUBLIC_DEVICE_ADDRESS,
|
||||
self.PUBLIC_IDENTITY_ADDRESS,
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -1742,9 +1764,9 @@ class Address:
|
||||
|
||||
@property
|
||||
def is_resolved(self):
|
||||
return (
|
||||
self.address_type == self.PUBLIC_IDENTITY_ADDRESS
|
||||
or self.address_type == self.RANDOM_IDENTITY_ADDRESS
|
||||
return self.address_type in (
|
||||
self.PUBLIC_IDENTITY_ADDRESS,
|
||||
self.RANDOM_IDENTITY_ADDRESS,
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -1776,10 +1798,10 @@ class Address:
|
||||
'''
|
||||
String representation of the address, MSB first
|
||||
'''
|
||||
str = ':'.join([f'{x:02X}' for x in reversed(self.address_bytes)])
|
||||
result = ':'.join([f'{x:02X}' for x in reversed(self.address_bytes)])
|
||||
if not self.is_public:
|
||||
return str
|
||||
return str + '/P'
|
||||
return result
|
||||
return result + '/P'
|
||||
|
||||
|
||||
# Predefined address values
|
||||
@@ -1801,9 +1823,10 @@ class OwnAddressType:
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def type_name(type):
|
||||
return name_or_number(OwnAddressType.TYPE_NAMES, type)
|
||||
def type_name(type_id):
|
||||
return name_or_number(OwnAddressType.TYPE_NAMES, type_id)
|
||||
|
||||
# pylint: disable-next=unnecessary-lambda
|
||||
TYPE_SPEC = {'size': 1, 'mapper': lambda x: OwnAddressType.type_name(x)}
|
||||
|
||||
|
||||
@@ -1816,14 +1839,17 @@ class HCI_Packet:
|
||||
@staticmethod
|
||||
def from_bytes(packet):
|
||||
packet_type = packet[0]
|
||||
|
||||
if packet_type == HCI_COMMAND_PACKET:
|
||||
return HCI_Command.from_bytes(packet)
|
||||
elif packet_type == HCI_ACL_DATA_PACKET:
|
||||
|
||||
if packet_type == HCI_ACL_DATA_PACKET:
|
||||
return HCI_AclDataPacket.from_bytes(packet)
|
||||
elif packet_type == HCI_EVENT_PACKET:
|
||||
|
||||
if packet_type == HCI_EVENT_PACKET:
|
||||
return HCI_Event.from_bytes(packet)
|
||||
else:
|
||||
return HCI_CustomPacket(packet)
|
||||
|
||||
return HCI_CustomPacket(packet)
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
@@ -1850,7 +1876,7 @@ class HCI_Command(HCI_Packet):
|
||||
command_classes = {}
|
||||
|
||||
@staticmethod
|
||||
def command(fields=[], return_parameters_fields=[]):
|
||||
def command(fields=(), return_parameters_fields=()):
|
||||
'''
|
||||
Decorator used to declare and register subclasses
|
||||
'''
|
||||
@@ -1897,8 +1923,8 @@ class HCI_Command(HCI_Packet):
|
||||
HCI_Command.__init__(self, op_code, parameters)
|
||||
HCI_Object.init_from_bytes(self, parameters, 0, fields)
|
||||
return self
|
||||
else:
|
||||
return cls.from_parameters(parameters)
|
||||
|
||||
return cls.from_parameters(parameters)
|
||||
|
||||
@staticmethod
|
||||
def command_name(op_code):
|
||||
@@ -2842,6 +2868,7 @@ class HCI_LE_Set_Random_Address_Command(HCI_Command):
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command(
|
||||
# pylint: disable=line-too-long,unnecessary-lambda
|
||||
[
|
||||
('advertising_interval_min', 2),
|
||||
('advertising_interval_max', 2),
|
||||
@@ -3089,7 +3116,8 @@ class HCI_LE_Read_Remote_Features_Command(HCI_Command):
|
||||
class HCI_LE_Enable_Encryption_Command(HCI_Command):
|
||||
'''
|
||||
See Bluetooth spec @ 7.8.24 LE Enable Encryption Command
|
||||
(renamed from "LE Start Encryption Command" in version prior to 5.2 of the specification)
|
||||
(renamed from "LE Start Encryption Command" in version prior to 5.2 of the
|
||||
specification)
|
||||
'''
|
||||
|
||||
|
||||
@@ -3144,7 +3172,8 @@ class HCI_LE_Remote_Connection_Parameter_Request_Reply_Command(HCI_Command):
|
||||
)
|
||||
class HCI_LE_Remote_Connection_Parameter_Request_Negative_Reply_Command(HCI_Command):
|
||||
'''
|
||||
See Bluetooth spec @ 7.8.32 LE Remote Connection Parameter Request Negative Reply Command
|
||||
See Bluetooth spec @ 7.8.32 LE Remote Connection Parameter Request Negative Reply
|
||||
Command
|
||||
'''
|
||||
|
||||
|
||||
@@ -3356,6 +3385,7 @@ class HCI_LE_Set_Advertising_Set_Random_Address_Command(HCI_Command):
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command(
|
||||
# pylint: disable=line-too-long,unnecessary-lambda
|
||||
fields=[
|
||||
('advertising_handle', 1),
|
||||
(
|
||||
@@ -3422,6 +3452,7 @@ class HCI_LE_Set_Extended_Advertising_Parameters_Command(HCI_Command):
|
||||
|
||||
@classmethod
|
||||
def advertising_properties_string(cls, properties):
|
||||
# pylint: disable=line-too-long
|
||||
return f'[{",".join(bit_flags_to_strings(properties, cls.ADVERTISING_PROPERTIES_NAMES))}]'
|
||||
|
||||
@classmethod
|
||||
@@ -3431,6 +3462,7 @@ class HCI_LE_Set_Extended_Advertising_Parameters_Command(HCI_Command):
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command(
|
||||
# pylint: disable=line-too-long,unnecessary-lambda
|
||||
[
|
||||
('advertising_handle', 1),
|
||||
(
|
||||
@@ -3480,6 +3512,7 @@ class HCI_LE_Set_Extended_Advertising_Data_Command(HCI_Command):
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command(
|
||||
# pylint: disable=line-too-long,unnecessary-lambda
|
||||
[
|
||||
('advertising_handle', 1),
|
||||
(
|
||||
@@ -3573,9 +3606,9 @@ class HCI_LE_Set_Extended_Advertising_Enable_Command(HCI_Command):
|
||||
|
||||
def __str__(self):
|
||||
fields = [('enable:', self.enable)]
|
||||
for i in range(len(self.advertising_handles)):
|
||||
for i, advertising_handle in enumerate(self.advertising_handles):
|
||||
fields.append(
|
||||
(f'advertising_handle[{i}]: ', self.advertising_handles[i])
|
||||
(f'advertising_handle[{i}]: ', advertising_handle)
|
||||
)
|
||||
fields.append((f'duration[{i}]: ', self.durations[i]))
|
||||
fields.append(
|
||||
@@ -3736,7 +3769,7 @@ class HCI_LE_Set_Extended_Scan_Parameters_Command(HCI_Command):
|
||||
)
|
||||
fields.append(
|
||||
(f'{scanning_phy_str}.scan_interval:', self.scan_intervals[i])
|
||||
),
|
||||
)
|
||||
fields.append((f'{scanning_phy_str}.scan_window: ', self.scan_windows[i]))
|
||||
|
||||
return (
|
||||
@@ -3871,43 +3904,43 @@ class HCI_LE_Extended_Create_Connection_Command(HCI_Command):
|
||||
f'{initiating_phys_str}.scan_interval: ',
|
||||
self.scan_intervals[i],
|
||||
)
|
||||
),
|
||||
)
|
||||
fields.append(
|
||||
(
|
||||
f'{initiating_phys_str}.scan_window: ',
|
||||
self.scan_windows[i],
|
||||
)
|
||||
),
|
||||
)
|
||||
fields.append(
|
||||
(
|
||||
f'{initiating_phys_str}.connection_interval_min:',
|
||||
self.connection_interval_mins[i],
|
||||
)
|
||||
),
|
||||
)
|
||||
fields.append(
|
||||
(
|
||||
f'{initiating_phys_str}.connection_interval_max:',
|
||||
self.connection_interval_maxs[i],
|
||||
)
|
||||
),
|
||||
)
|
||||
fields.append(
|
||||
(
|
||||
f'{initiating_phys_str}.max_latency: ',
|
||||
self.max_latencies[i],
|
||||
)
|
||||
),
|
||||
)
|
||||
fields.append(
|
||||
(
|
||||
f'{initiating_phys_str}.supervision_timeout: ',
|
||||
self.supervision_timeouts[i],
|
||||
)
|
||||
),
|
||||
)
|
||||
fields.append(
|
||||
(
|
||||
f'{initiating_phys_str}.min_ce_length: ',
|
||||
self.min_ce_lengths[i],
|
||||
)
|
||||
),
|
||||
)
|
||||
fields.append(
|
||||
(
|
||||
f'{initiating_phys_str}.max_ce_length: ',
|
||||
@@ -3933,6 +3966,7 @@ class HCI_LE_Extended_Create_Connection_Command(HCI_Command):
|
||||
'privacy_mode',
|
||||
{
|
||||
'size': 1,
|
||||
# pylint: disable-next=unnecessary-lambda
|
||||
'mapper': lambda x: HCI_LE_Set_Privacy_Mode_Command.privacy_mode_name(
|
||||
x
|
||||
),
|
||||
@@ -3979,7 +4013,7 @@ class HCI_Event(HCI_Packet):
|
||||
meta_event_classes = {}
|
||||
|
||||
@staticmethod
|
||||
def event(fields=[]):
|
||||
def event(fields=()):
|
||||
'''
|
||||
Decorator used to declare and register subclasses
|
||||
'''
|
||||
@@ -4005,16 +4039,16 @@ class HCI_Event(HCI_Packet):
|
||||
return inner
|
||||
|
||||
@staticmethod
|
||||
def registered(cls):
|
||||
cls.name = cls.__name__.upper()
|
||||
cls.event_code = key_with_value(HCI_EVENT_NAMES, cls.name)
|
||||
if cls.event_code is None:
|
||||
def registered(event_class):
|
||||
event_class.name = event_class.__name__.upper()
|
||||
event_class.event_code = key_with_value(HCI_EVENT_NAMES, event_class.name)
|
||||
if event_class.event_code is None:
|
||||
raise KeyError('event not found in HCI_EVENT_NAMES')
|
||||
|
||||
# Register a factory for this class
|
||||
HCI_Event.event_classes[cls.event_code] = cls
|
||||
HCI_Event.event_classes[event_class.event_code] = event_class
|
||||
|
||||
return cls
|
||||
return event_class
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(packet):
|
||||
@@ -4025,7 +4059,8 @@ class HCI_Event(HCI_Packet):
|
||||
raise ValueError('invalid packet length')
|
||||
|
||||
if event_code == HCI_LE_META_EVENT:
|
||||
# We do this dispatch here and not in the subclass in order to avoid call loops
|
||||
# We do this dispatch here and not in the subclass in order to avoid call
|
||||
# loops
|
||||
subevent_code = parameters[0]
|
||||
cls = HCI_Event.meta_event_classes.get(subevent_code)
|
||||
if cls is None:
|
||||
@@ -4086,7 +4121,7 @@ class HCI_LE_Meta_Event(HCI_Event):
|
||||
'''
|
||||
|
||||
@staticmethod
|
||||
def event(fields=[]):
|
||||
def event(fields=()):
|
||||
'''
|
||||
Decorator used to declare and register subclasses
|
||||
'''
|
||||
@@ -4214,9 +4249,9 @@ class HCI_LE_Advertising_Report_Event(HCI_LE_Meta_Event):
|
||||
def event_type_string(self):
|
||||
return HCI_LE_Advertising_Report_Event.event_type_name(self.event_type)
|
||||
|
||||
def to_string(self, prefix):
|
||||
def to_string(self, indentation='', _=None):
|
||||
return super().to_string(
|
||||
prefix,
|
||||
indentation,
|
||||
{
|
||||
'event_type': HCI_LE_Advertising_Report_Event.event_type_name,
|
||||
'address_type': Address.address_type_name,
|
||||
@@ -4443,9 +4478,10 @@ class HCI_LE_Extended_Advertising_Report_Event(HCI_LE_Meta_Event):
|
||||
self.event_type
|
||||
)
|
||||
|
||||
def to_string(self, prefix):
|
||||
def to_string(self, indentation='', _=None):
|
||||
# pylint: disable=line-too-long
|
||||
return super().to_string(
|
||||
prefix,
|
||||
indentation,
|
||||
{
|
||||
'event_type': HCI_LE_Extended_Advertising_Report_Event.event_type_string,
|
||||
'address_type': Address.address_type_name,
|
||||
@@ -4472,6 +4508,7 @@ class HCI_LE_Extended_Advertising_Report_Event(HCI_LE_Meta_Event):
|
||||
)
|
||||
)
|
||||
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 = ''
|
||||
@@ -4587,6 +4624,7 @@ class HCI_Inquiry_Result_Event(HCI_Event):
|
||||
'link_type',
|
||||
{
|
||||
'size': 1,
|
||||
# pylint: disable-next=unnecessary-lambda
|
||||
'mapper': lambda x: HCI_Connection_Complete_Event.link_type_name(x),
|
||||
},
|
||||
),
|
||||
@@ -4622,6 +4660,7 @@ class HCI_Connection_Complete_Event(HCI_Event):
|
||||
'link_type',
|
||||
{
|
||||
'size': 1,
|
||||
# pylint: disable-next=unnecessary-lambda
|
||||
'mapper': lambda x: HCI_Connection_Complete_Event.link_type_name(x),
|
||||
},
|
||||
),
|
||||
@@ -4678,6 +4717,7 @@ class HCI_Remote_Name_Request_Complete_Event(HCI_Event):
|
||||
'encryption_enabled',
|
||||
{
|
||||
'size': 1,
|
||||
# pylint: disable-next=unnecessary-lambda
|
||||
'mapper': lambda x: HCI_Encryption_Change_Event.encryption_enabled_name(
|
||||
x
|
||||
),
|
||||
@@ -4746,16 +4786,20 @@ class HCI_Command_Complete_Event(HCI_Event):
|
||||
See Bluetooth spec @ 7.7.14 Command Complete Event
|
||||
'''
|
||||
|
||||
return_parameters = b''
|
||||
|
||||
def map_return_parameters(self, return_parameters):
|
||||
# Map simple 'status' return parameters to their named constant form
|
||||
if type(return_parameters) is bytes and len(return_parameters) == 1:
|
||||
'''Map simple 'status' return parameters to their named constant form'''
|
||||
|
||||
if isinstance(return_parameters, bytes) and len(return_parameters) == 1:
|
||||
# Byte-array form
|
||||
return HCI_Constant.status_name(return_parameters[0])
|
||||
elif type(return_parameters) is int:
|
||||
|
||||
if isinstance(return_parameters, int):
|
||||
# Already converted to an integer status code
|
||||
return HCI_Constant.status_name(return_parameters)
|
||||
else:
|
||||
return return_parameters
|
||||
|
||||
return return_parameters
|
||||
|
||||
@staticmethod
|
||||
def from_parameters(parameters):
|
||||
@@ -4766,8 +4810,12 @@ class HCI_Command_Complete_Event(HCI_Event):
|
||||
)
|
||||
|
||||
# Parse the return parameters
|
||||
if type(self.return_parameters) is bytes and len(self.return_parameters) == 1:
|
||||
# All commands with 1-byte return parameters return a 'status' field, convert it to an integer
|
||||
if (
|
||||
isinstance(self.return_parameters, bytes)
|
||||
and len(self.return_parameters) == 1
|
||||
):
|
||||
# All commands with 1-byte return parameters return a 'status' field,
|
||||
# convert it to an integer
|
||||
self.return_parameters = self.return_parameters[0]
|
||||
else:
|
||||
cls = HCI_Command.command_classes.get(self.command_opcode)
|
||||
@@ -4793,6 +4841,7 @@ class HCI_Command_Complete_Event(HCI_Event):
|
||||
[
|
||||
(
|
||||
'status',
|
||||
# pylint: disable-next=unnecessary-lambda
|
||||
{'size': 1, 'mapper': lambda x: HCI_Command_Status_Event.status_name(x)},
|
||||
),
|
||||
('num_hci_command_packets', 1),
|
||||
@@ -4810,8 +4859,8 @@ class HCI_Command_Status_Event(HCI_Event):
|
||||
def status_name(status):
|
||||
if status == HCI_Command_Status_Event.PENDING:
|
||||
return 'PENDING'
|
||||
else:
|
||||
return HCI_Constant.error_name(status)
|
||||
|
||||
return HCI_Constant.error_name(status)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -4869,10 +4918,10 @@ class HCI_Number_Of_Completed_Packets_Event(HCI_Event):
|
||||
color(' number_of_handles: ', 'cyan')
|
||||
+ f'{len(self.connection_handles)}',
|
||||
]
|
||||
for i in range(len(self.connection_handles)):
|
||||
for i, connection_handle in enumerate(self.connection_handles):
|
||||
lines.append(
|
||||
color(f' connection_handle[{i}]: ', 'cyan')
|
||||
+ f'{self.connection_handles[i]}'
|
||||
+ f'{connection_handle}'
|
||||
)
|
||||
lines.append(
|
||||
color(f' num_completed_packets[{i}]: ', 'cyan')
|
||||
@@ -4888,6 +4937,7 @@ class HCI_Number_Of_Completed_Packets_Event(HCI_Event):
|
||||
('connection_handle', 2),
|
||||
(
|
||||
'current_mode',
|
||||
# pylint: disable-next=unnecessary-lambda
|
||||
{'size': 1, 'mapper': lambda x: HCI_Mode_Change_Event.mode_name(x)},
|
||||
),
|
||||
('interval', 2),
|
||||
@@ -5044,6 +5094,7 @@ class HCI_Read_Remote_Extended_Features_Complete_Event(HCI_Event):
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Event.event(
|
||||
# pylint: disable=line-too-long
|
||||
[
|
||||
('status', STATUS_SPEC),
|
||||
('connection_handle', 2),
|
||||
@@ -5052,6 +5103,7 @@ class HCI_Read_Remote_Extended_Features_Complete_Event(HCI_Event):
|
||||
'link_type',
|
||||
{
|
||||
'size': 1,
|
||||
# pylint: disable-next=unnecessary-lambda
|
||||
'mapper': lambda x: HCI_Synchronous_Connection_Complete_Event.link_type_name(
|
||||
x
|
||||
),
|
||||
@@ -5065,6 +5117,7 @@ class HCI_Read_Remote_Extended_Features_Complete_Event(HCI_Event):
|
||||
'air_mode',
|
||||
{
|
||||
'size': 1,
|
||||
# pylint: disable-next=unnecessary-lambda
|
||||
'mapper': lambda x: HCI_Synchronous_Connection_Complete_Event.air_mode_name(
|
||||
x
|
||||
),
|
||||
@@ -5229,7 +5282,7 @@ class HCI_Remote_Host_Supported_Features_Notification_Event(HCI_Event):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class HCI_AclDataPacket(HCI_Packet):
|
||||
class HCI_AclDataPacket:
|
||||
'''
|
||||
See Bluetooth spec @ 5.4.2 HCI ACL Data Packets
|
||||
'''
|
||||
@@ -5268,7 +5321,13 @@ class HCI_AclDataPacket(HCI_Packet):
|
||||
return self.to_bytes()
|
||||
|
||||
def __str__(self):
|
||||
return f'{color("ACL", "blue")}: handle=0x{self.connection_handle:04x}, pb={self.pb_flag}, bc={self.bc_flag}, data_total_length={self.data_total_length}, data={self.data.hex()}'
|
||||
return (
|
||||
f'{color("ACL", "blue")}: '
|
||||
f'handle=0x{self.connection_handle:04x}'
|
||||
f'pb={self.pb_flag}, bc={self.bc_flag}, '
|
||||
f'data_total_length={self.data_total_length}, '
|
||||
f'data={self.data.hex()}'
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -5279,9 +5338,9 @@ class HCI_AclDataPacketAssembler:
|
||||
self.l2cap_pdu_length = 0
|
||||
|
||||
def feed_packet(self, packet):
|
||||
if (
|
||||
packet.pb_flag == HCI_ACL_PB_FIRST_NON_FLUSHABLE
|
||||
or packet.pb_flag == HCI_ACL_PB_FIRST_FLUSHABLE
|
||||
if packet.pb_flag in (
|
||||
HCI_ACL_PB_FIRST_NON_FLUSHABLE,
|
||||
HCI_ACL_PB_FIRST_FLUSHABLE,
|
||||
):
|
||||
(l2cap_pdu_length,) = struct.unpack_from('<H', packet.data, 0)
|
||||
self.current_data = packet.data
|
||||
|
||||
@@ -18,10 +18,9 @@
|
||||
import logging
|
||||
from colors import color
|
||||
|
||||
from bumble.smp import SMP_CID, SMP_Command
|
||||
|
||||
from .att import ATT_CID, ATT_PDU
|
||||
from .smp import SMP_CID, SMP_Command
|
||||
from .core import name_or_number
|
||||
from .gatt import ATT_PDU, ATT_CID
|
||||
from .l2cap import (
|
||||
L2CAP_PDU,
|
||||
L2CAP_CONNECTION_REQUEST,
|
||||
@@ -66,6 +65,7 @@ class PacketTracer:
|
||||
self.psms = {} # PSM, by source_cid
|
||||
self.peer = None # ACL stream in the other direction
|
||||
|
||||
# pylint: disable=too-many-nested-blocks
|
||||
def on_acl_pdu(self, pdu):
|
||||
l2cap_pdu = L2CAP_PDU.from_bytes(pdu)
|
||||
|
||||
@@ -75,10 +75,7 @@ class PacketTracer:
|
||||
elif l2cap_pdu.cid == SMP_CID:
|
||||
smp_command = SMP_Command.from_bytes(l2cap_pdu.payload)
|
||||
self.analyzer.emit(smp_command)
|
||||
elif (
|
||||
l2cap_pdu.cid == L2CAP_SIGNALING_CID
|
||||
or l2cap_pdu.cid == L2CAP_LE_SIGNALING_CID
|
||||
):
|
||||
elif l2cap_pdu.cid in (L2CAP_SIGNALING_CID, L2CAP_LE_SIGNALING_CID):
|
||||
control_frame = L2CAP_Control_Frame.from_bytes(l2cap_pdu.payload)
|
||||
self.analyzer.emit(control_frame)
|
||||
|
||||
@@ -95,7 +92,8 @@ class PacketTracer:
|
||||
# Found a pending connection
|
||||
self.psms[control_frame.destination_cid] = psm
|
||||
|
||||
# For AVDTP connections, create a packet assembler for each direction
|
||||
# For AVDTP connections, create a packet assembler for
|
||||
# each direction
|
||||
if psm == AVDTP_PSM:
|
||||
self.avdtp_assemblers[
|
||||
control_frame.source_cid
|
||||
@@ -117,7 +115,8 @@ class PacketTracer:
|
||||
self.analyzer.emit(rfcomm_frame)
|
||||
elif psm == AVDTP_PSM:
|
||||
self.analyzer.emit(
|
||||
f'{color("L2CAP", "green")} [CID={l2cap_pdu.cid}, PSM=AVDTP]: {l2cap_pdu.payload.hex()}'
|
||||
f'{color("L2CAP", "green")} [CID={l2cap_pdu.cid}, '
|
||||
f'PSM=AVDTP]: {l2cap_pdu.payload.hex()}'
|
||||
)
|
||||
assembler = self.avdtp_assemblers.get(l2cap_pdu.cid)
|
||||
if assembler:
|
||||
@@ -125,7 +124,8 @@ class PacketTracer:
|
||||
else:
|
||||
psm_string = name_or_number(PSM_NAMES, psm)
|
||||
self.analyzer.emit(
|
||||
f'{color("L2CAP", "green")} [CID={l2cap_pdu.cid}, PSM={psm_string}]: {l2cap_pdu.payload.hex()}'
|
||||
f'{color("L2CAP", "green")} [CID={l2cap_pdu.cid}, '
|
||||
f'PSM={psm_string}]: {l2cap_pdu.payload.hex()}'
|
||||
)
|
||||
else:
|
||||
self.analyzer.emit(l2cap_pdu)
|
||||
@@ -147,7 +147,8 @@ class PacketTracer:
|
||||
|
||||
def start_acl_stream(self, connection_handle):
|
||||
logger.info(
|
||||
f'[{self.label}] +++ Creating ACL stream for connection 0x{connection_handle:04X}'
|
||||
f'[{self.label}] +++ Creating ACL stream for connection '
|
||||
f'0x{connection_handle:04X}'
|
||||
)
|
||||
stream = PacketTracer.AclStream(self)
|
||||
self.acl_streams[connection_handle] = stream
|
||||
@@ -162,7 +163,8 @@ class PacketTracer:
|
||||
def end_acl_stream(self, connection_handle):
|
||||
if connection_handle in self.acl_streams:
|
||||
logger.info(
|
||||
f'[{self.label}] --- Removing ACL stream for connection 0x{connection_handle:04X}'
|
||||
f'[{self.label}] --- Removing ACL stream for connection '
|
||||
f'0x{connection_handle:04X}'
|
||||
)
|
||||
del self.acl_streams[connection_handle]
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ class HfpProtocol:
|
||||
|
||||
def feed(self, data):
|
||||
# Convert the data to a string if needed
|
||||
if type(data) == bytes:
|
||||
if isinstance(data, bytes):
|
||||
data = data.decode('utf-8')
|
||||
|
||||
logger.debug(f'<<< Data received: {data}')
|
||||
@@ -79,16 +79,16 @@ class HfpProtocol:
|
||||
async def initialize_service(self):
|
||||
# Perform Service Level Connection Initialization
|
||||
self.send_command_line('AT+BRSF=2072') # Retrieve Supported Features
|
||||
line = await (self.next_line())
|
||||
line = await (self.next_line())
|
||||
await (self.next_line())
|
||||
await (self.next_line())
|
||||
|
||||
self.send_command_line('AT+CIND=?')
|
||||
line = await (self.next_line())
|
||||
line = await (self.next_line())
|
||||
await (self.next_line())
|
||||
await (self.next_line())
|
||||
|
||||
self.send_command_line('AT+CIND?')
|
||||
line = await (self.next_line())
|
||||
line = await (self.next_line())
|
||||
await (self.next_line())
|
||||
await (self.next_line())
|
||||
|
||||
self.send_command_line('AT+CMER=3,0,0,1')
|
||||
line = await (self.next_line())
|
||||
await (self.next_line())
|
||||
|
||||
135
bumble/host.py
135
bumble/host.py
@@ -16,17 +16,61 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import collections
|
||||
import logging
|
||||
import struct
|
||||
|
||||
from colors import color
|
||||
|
||||
from .hci import *
|
||||
from .l2cap import *
|
||||
from .att import *
|
||||
from .gatt import *
|
||||
from .smp import *
|
||||
from .core import ConnectionParameters
|
||||
from bumble.l2cap import L2CAP_PDU
|
||||
|
||||
from .hci import (
|
||||
HCI_ACL_DATA_PACKET,
|
||||
HCI_COMMAND_COMPLETE_EVENT,
|
||||
HCI_COMMAND_PACKET,
|
||||
HCI_EVENT_PACKET,
|
||||
HCI_LE_READ_BUFFER_SIZE_COMMAND,
|
||||
HCI_LE_READ_LOCAL_SUPPORTED_FEATURES_COMMAND,
|
||||
HCI_LE_READ_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND,
|
||||
HCI_LE_WRITE_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND,
|
||||
HCI_READ_BUFFER_SIZE_COMMAND,
|
||||
HCI_READ_LOCAL_VERSION_INFORMATION_COMMAND,
|
||||
HCI_RESET_COMMAND,
|
||||
HCI_SUCCESS,
|
||||
HCI_SUPPORTED_COMMANDS_FLAGS,
|
||||
HCI_VERSION_BLUETOOTH_CORE_4_0,
|
||||
HCI_AclDataPacket,
|
||||
HCI_AclDataPacketAssembler,
|
||||
HCI_Constant,
|
||||
HCI_Error,
|
||||
HCI_LE_Long_Term_Key_Request_Negative_Reply_Command,
|
||||
HCI_LE_Long_Term_Key_Request_Reply_Command,
|
||||
HCI_LE_Read_Buffer_Size_Command,
|
||||
HCI_LE_Read_Local_Supported_Features_Command,
|
||||
HCI_LE_Read_Suggested_Default_Data_Length_Command,
|
||||
HCI_LE_Remote_Connection_Parameter_Request_Reply_Command,
|
||||
HCI_LE_Set_Event_Mask_Command,
|
||||
HCI_LE_Write_Suggested_Default_Data_Length_Command,
|
||||
HCI_Link_Key_Request_Negative_Reply_Command,
|
||||
HCI_Link_Key_Request_Reply_Command,
|
||||
HCI_PIN_Code_Request_Negative_Reply_Command,
|
||||
HCI_Packet,
|
||||
HCI_Read_Buffer_Size_Command,
|
||||
HCI_Read_Local_Supported_Commands_Command,
|
||||
HCI_Read_Local_Version_Information_Command,
|
||||
HCI_Reset_Command,
|
||||
HCI_Set_Event_Mask_Command,
|
||||
)
|
||||
from .core import (
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
BT_CENTRAL_ROLE,
|
||||
BT_LE_TRANSPORT,
|
||||
ConnectionPHY,
|
||||
ConnectionParameters,
|
||||
)
|
||||
from .utils import AbortableEventEmitter
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -71,6 +115,7 @@ class Host(AbortableEventEmitter):
|
||||
|
||||
self.hci_sink = None
|
||||
self.ready = False # True when we can accept incoming packets
|
||||
self.reset_done = False
|
||||
self.connections = {} # Connections, by connection handle
|
||||
self.pending_command = None
|
||||
self.pending_response = None
|
||||
@@ -139,10 +184,12 @@ class Host(AbortableEventEmitter):
|
||||
self.local_version is not None
|
||||
and self.local_version.hci_version <= HCI_VERSION_BLUETOOTH_CORE_4_0
|
||||
):
|
||||
# Some older controllers don't like event masks with bits they don't understand
|
||||
# Some older controllers don't like event masks with bits they don't
|
||||
# understand
|
||||
le_event_mask = bytes.fromhex('1F00000000000000')
|
||||
else:
|
||||
le_event_mask = bytes.fromhex('FFFFF00000000000')
|
||||
|
||||
await self.send_command(
|
||||
HCI_LE_Set_Event_Mask_Command(le_event_mask=le_event_mask)
|
||||
)
|
||||
@@ -159,7 +206,8 @@ class Host(AbortableEventEmitter):
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f'HCI ACL flow control: hc_acl_data_packet_length={self.hc_acl_data_packet_length},'
|
||||
'HCI ACL flow control: '
|
||||
f'hc_acl_data_packet_length={self.hc_acl_data_packet_length},'
|
||||
f'hc_total_num_acl_data_packets={self.hc_total_num_acl_data_packets}'
|
||||
)
|
||||
|
||||
@@ -175,8 +223,10 @@ class Host(AbortableEventEmitter):
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f'HCI LE ACL flow control: hc_le_acl_data_packet_length={self.hc_le_acl_data_packet_length},'
|
||||
f'hc_total_num_le_acl_data_packets={self.hc_total_num_le_acl_data_packets}'
|
||||
'HCI LE ACL flow control: '
|
||||
f'hc_le_acl_data_packet_length={self.hc_le_acl_data_packet_length},'
|
||||
'hc_total_num_le_acl_data_packets='
|
||||
f'{self.hc_total_num_le_acl_data_packets}'
|
||||
)
|
||||
|
||||
if (
|
||||
@@ -244,9 +294,9 @@ class Host(AbortableEventEmitter):
|
||||
|
||||
# Check the return parameters if required
|
||||
if check_result:
|
||||
if type(response.return_parameters) is int:
|
||||
if isinstance(response.return_parameters, int):
|
||||
status = response.return_parameters
|
||||
elif type(response.return_parameters) is bytes:
|
||||
elif isinstance(response.return_parameters, bytes):
|
||||
# return parameters first field is a one byte status code
|
||||
status = response.return_parameters[0]
|
||||
else:
|
||||
@@ -306,7 +356,8 @@ class Host(AbortableEventEmitter):
|
||||
|
||||
if len(self.acl_packet_queue):
|
||||
logger.debug(
|
||||
f'{self.acl_packets_in_flight} ACL packets in flight, {len(self.acl_packet_queue)} in queue'
|
||||
f'{self.acl_packets_in_flight} ACL packets in flight, '
|
||||
f'{len(self.acl_packet_queue)} in queue'
|
||||
)
|
||||
|
||||
def check_acl_packet_queue(self):
|
||||
@@ -400,7 +451,9 @@ class Host(AbortableEventEmitter):
|
||||
# Check that it is what we were expecting
|
||||
if self.pending_command.op_code != event.command_opcode:
|
||||
logger.warning(
|
||||
f'!!! command result mismatch, expected 0x{self.pending_command.op_code:X} but got 0x{event.command_opcode:X}'
|
||||
'!!! command result mismatch, expected '
|
||||
f'0x{self.pending_command.op_code:X} but got '
|
||||
f'0x{event.command_opcode:X}'
|
||||
)
|
||||
|
||||
self.pending_response.set_result(event)
|
||||
@@ -415,10 +468,12 @@ class Host(AbortableEventEmitter):
|
||||
|
||||
def on_hci_command_complete_event(self, event):
|
||||
if event.command_opcode == 0:
|
||||
# This is used just for the Num_HCI_Command_Packets field, not related to an actual command
|
||||
# This is used just for the Num_HCI_Command_Packets field, not related to
|
||||
# an actual command
|
||||
logger.debug('no-command event')
|
||||
else:
|
||||
return self.on_command_processed(event)
|
||||
return None
|
||||
|
||||
return self.on_command_processed(event)
|
||||
|
||||
def on_hci_command_status_event(self, event):
|
||||
return self.on_command_processed(event)
|
||||
@@ -431,7 +486,8 @@ class Host(AbortableEventEmitter):
|
||||
else:
|
||||
logger.warning(
|
||||
color(
|
||||
f'!!! {total_packets} completed but only {self.acl_packets_in_flight} in flight'
|
||||
'!!! {total_packets} completed but only '
|
||||
f'{self.acl_packets_in_flight} in flight'
|
||||
)
|
||||
)
|
||||
self.acl_packets_in_flight = 0
|
||||
@@ -451,7 +507,8 @@ class Host(AbortableEventEmitter):
|
||||
if event.status == HCI_SUCCESS:
|
||||
# Create/update the connection
|
||||
logger.debug(
|
||||
f'### CONNECTION: [0x{event.connection_handle:04X}] {event.peer_address} as {HCI_Constant.role_name(event.role)}'
|
||||
f'### CONNECTION: [0x{event.connection_handle:04X}] '
|
||||
f'{event.peer_address} as {HCI_Constant.role_name(event.role)}'
|
||||
)
|
||||
|
||||
connection = self.connections.get(event.connection_handle)
|
||||
@@ -496,7 +553,8 @@ class Host(AbortableEventEmitter):
|
||||
if event.status == HCI_SUCCESS:
|
||||
# Create/update the connection
|
||||
logger.debug(
|
||||
f'### BR/EDR CONNECTION: [0x{event.connection_handle:04X}] {event.bd_addr}'
|
||||
f'### BR/EDR CONNECTION: [0x{event.connection_handle:04X}] '
|
||||
f'{event.bd_addr}'
|
||||
)
|
||||
|
||||
connection = self.connections.get(event.connection_handle)
|
||||
@@ -536,7 +594,10 @@ class Host(AbortableEventEmitter):
|
||||
|
||||
if event.status == HCI_SUCCESS:
|
||||
logger.debug(
|
||||
f'### DISCONNECTION: [0x{event.connection_handle:04X}] {connection.peer_address} as {HCI_Constant.role_name(connection.role)}, reason={event.reason}'
|
||||
f'### DISCONNECTION: [0x{event.connection_handle:04X}] '
|
||||
f'{connection.peer_address} as '
|
||||
f'{HCI_Constant.role_name(connection.role)}, '
|
||||
f'reason={event.reason}'
|
||||
)
|
||||
del self.connections[event.connection_handle]
|
||||
|
||||
@@ -616,9 +677,15 @@ class Host(AbortableEventEmitter):
|
||||
logger.debug('no long term key provider')
|
||||
long_term_key = None
|
||||
else:
|
||||
long_term_key = await self.abort_on('flush', self.long_term_key_provider(
|
||||
connection.handle, event.random_number, event.encryption_diversifier
|
||||
))
|
||||
long_term_key = await self.abort_on(
|
||||
'flush',
|
||||
# pylint: disable-next=not-callable
|
||||
self.long_term_key_provider(
|
||||
connection.handle,
|
||||
event.random_number,
|
||||
event.encryption_diversifier,
|
||||
),
|
||||
)
|
||||
if long_term_key:
|
||||
response = HCI_LE_Long_Term_Key_Request_Reply_Command(
|
||||
connection_handle=event.connection_handle,
|
||||
@@ -642,12 +709,14 @@ class Host(AbortableEventEmitter):
|
||||
def on_hci_role_change_event(self, event):
|
||||
if event.status == HCI_SUCCESS:
|
||||
logger.debug(
|
||||
f'role change for {event.bd_addr}: {HCI_Constant.role_name(event.new_role)}'
|
||||
f'role change for {event.bd_addr}: '
|
||||
f'{HCI_Constant.role_name(event.new_role)}'
|
||||
)
|
||||
# TODO: lookup the connection and update the role
|
||||
else:
|
||||
logger.debug(
|
||||
f'role change for {event.bd_addr} failed: {HCI_Constant.error_name(event.status)}'
|
||||
f'role change for {event.bd_addr} failed: '
|
||||
f'{HCI_Constant.error_name(event.status)}'
|
||||
)
|
||||
|
||||
def on_hci_le_data_length_change_event(self, event):
|
||||
@@ -706,13 +775,15 @@ class Host(AbortableEventEmitter):
|
||||
|
||||
def on_hci_link_key_notification_event(self, event):
|
||||
logger.debug(
|
||||
f'link key for {event.bd_addr}: {event.link_key.hex()}, type={HCI_Constant.link_key_type_name(event.key_type)}'
|
||||
f'link key for {event.bd_addr}: {event.link_key.hex()}, '
|
||||
f'type={HCI_Constant.link_key_type_name(event.key_type)}'
|
||||
)
|
||||
self.emit('link_key', event.bd_addr, event.link_key, event.key_type)
|
||||
|
||||
def on_hci_simple_pairing_complete_event(self, event):
|
||||
logger.debug(
|
||||
f'simple pairing complete for {event.bd_addr}: status={HCI_Constant.status_name(event.status)}'
|
||||
f'simple pairing complete for {event.bd_addr}: '
|
||||
f'status={HCI_Constant.status_name(event.status)}'
|
||||
)
|
||||
|
||||
def on_hci_pin_code_request_event(self, event):
|
||||
@@ -728,7 +799,11 @@ class Host(AbortableEventEmitter):
|
||||
logger.debug('no link key provider')
|
||||
link_key = None
|
||||
else:
|
||||
link_key = await self.abort_on('flush', self.link_key_provider(event.bd_addr))
|
||||
link_key = await self.abort_on(
|
||||
'flush',
|
||||
# pylint: disable-next=not-callable
|
||||
self.link_key_provider(event.bd_addr),
|
||||
)
|
||||
if link_key:
|
||||
response = HCI_Link_Key_Request_Reply_Command(
|
||||
bd_addr=event.bd_addr, link_key=link_key
|
||||
@@ -763,7 +838,7 @@ class Host(AbortableEventEmitter):
|
||||
'authentication_user_passkey_notification', event.bd_addr, event.passkey
|
||||
)
|
||||
|
||||
def on_hci_inquiry_complete_event(self, event):
|
||||
def on_hci_inquiry_complete_event(self, _event):
|
||||
self.emit('inquiry_complete')
|
||||
|
||||
def on_hci_inquiry_result_with_rssi_event(self, event):
|
||||
|
||||
@@ -76,8 +76,10 @@ class PairingKeys:
|
||||
@staticmethod
|
||||
def key_from_dict(keys_dict, key_name):
|
||||
key_dict = keys_dict.get(key_name)
|
||||
if key_dict is not None:
|
||||
return PairingKeys.Key.from_dict(key_dict)
|
||||
if key_dict is None:
|
||||
return None
|
||||
|
||||
return PairingKeys.Key.from_dict(key_dict)
|
||||
|
||||
@staticmethod
|
||||
def from_dict(keys_dict):
|
||||
@@ -121,9 +123,9 @@ class PairingKeys:
|
||||
|
||||
def print(self, prefix=''):
|
||||
keys_dict = self.to_dict()
|
||||
for (property, value) in keys_dict.items():
|
||||
if type(value) is dict:
|
||||
print(f'{prefix}{color(property, "cyan")}:')
|
||||
for (container_property, value) in keys_dict.items():
|
||||
if isinstance(value, dict):
|
||||
print(f'{prefix}{color(container_property, "cyan")}:')
|
||||
for (key_property, key_value) in value.items():
|
||||
print(f'{prefix} {color(key_property, "green")}: {key_value}')
|
||||
else:
|
||||
@@ -138,7 +140,7 @@ class KeyStore:
|
||||
async def update(self, name, keys):
|
||||
pass
|
||||
|
||||
async def get(self, name):
|
||||
async def get(self, _name):
|
||||
return PairingKeys()
|
||||
|
||||
async def get_all(self):
|
||||
@@ -193,6 +195,9 @@ class JsonKeyStore(KeyStore):
|
||||
|
||||
if filename is None:
|
||||
# Use a default for the current user
|
||||
|
||||
# Import here because this may not exist on all platforms
|
||||
# pylint: disable=import-outside-toplevel
|
||||
import appdirs
|
||||
|
||||
self.directory_name = os.path.join(
|
||||
@@ -219,7 +224,7 @@ class JsonKeyStore(KeyStore):
|
||||
|
||||
async def load(self):
|
||||
try:
|
||||
with open(self.filename, 'r') as json_file:
|
||||
with open(self.filename, 'r', encoding='utf-8') as json_file:
|
||||
return json.load(json_file)
|
||||
except FileNotFoundError:
|
||||
return {}
|
||||
@@ -231,7 +236,7 @@ class JsonKeyStore(KeyStore):
|
||||
|
||||
# Save to a temporary file
|
||||
temp_filename = self.filename + '.tmp'
|
||||
with open(temp_filename, 'w') as output:
|
||||
with open(temp_filename, 'w', encoding='utf-8') as output:
|
||||
json.dump(db, output, sort_keys=True, indent=4)
|
||||
|
||||
# Atomically replace the previous file
|
||||
|
||||
160
bumble/l2cap.py
160
bumble/l2cap.py
@@ -41,6 +41,7 @@ logger = logging.getLogger(__name__)
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
# fmt: off
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
L2CAP_SIGNALING_CID = 0x01
|
||||
L2CAP_LE_SIGNALING_CID = 0x05
|
||||
@@ -137,11 +138,15 @@ L2CAP_MAXIMUM_TRANSMISSION_UNIT_CONFIGURATION_OPTION_TYPE = 0x01
|
||||
L2CAP_MTU_CONFIGURATION_PARAMETER_TYPE = 0x01
|
||||
|
||||
# fmt: on
|
||||
# pylint: enable=line-too-long
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Classes
|
||||
# -----------------------------------------------------------------------------
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
|
||||
class L2CAP_PDU:
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part A - 3 DATA PACKET FORMAT
|
||||
@@ -181,6 +186,7 @@ class L2CAP_Control_Frame:
|
||||
|
||||
classes = {}
|
||||
code = 0
|
||||
name = None
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(pdu):
|
||||
@@ -215,11 +221,11 @@ class L2CAP_Control_Frame:
|
||||
def decode_configuration_options(data):
|
||||
options = []
|
||||
while len(data) >= 2:
|
||||
type = data[0]
|
||||
value_type = data[0]
|
||||
length = data[1]
|
||||
value = data[2 : 2 + length]
|
||||
data = data[2 + length :]
|
||||
options.append((type, value))
|
||||
options.append((value_type, value))
|
||||
|
||||
return options
|
||||
|
||||
@@ -236,7 +242,8 @@ class L2CAP_Control_Frame:
|
||||
cls.code = key_with_value(L2CAP_CONTROL_FRAME_NAMES, cls.name)
|
||||
if cls.code is None:
|
||||
raise KeyError(
|
||||
f'Control Frame name {cls.name} not found in L2CAP_CONTROL_FRAME_NAMES'
|
||||
f'Control Frame name {cls.name} '
|
||||
'not found in L2CAP_CONTROL_FRAME_NAMES'
|
||||
)
|
||||
cls.fields = fields
|
||||
|
||||
@@ -281,6 +288,7 @@ class L2CAP_Control_Frame:
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@L2CAP_Control_Frame.subclass(
|
||||
# pylint: disable=unnecessary-lambda
|
||||
[
|
||||
(
|
||||
'reason',
|
||||
@@ -311,6 +319,7 @@ class L2CAP_Command_Reject(L2CAP_Control_Frame):
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@L2CAP_Control_Frame.subclass(
|
||||
# pylint: disable=unnecessary-lambda
|
||||
[
|
||||
(
|
||||
'psm',
|
||||
@@ -356,6 +365,7 @@ class L2CAP_Connection_Request(L2CAP_Control_Frame):
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@L2CAP_Control_Frame.subclass(
|
||||
# pylint: disable=unnecessary-lambda
|
||||
[
|
||||
('destination_cid', 2),
|
||||
('source_cid', 2),
|
||||
@@ -380,6 +390,7 @@ class L2CAP_Connection_Response(L2CAP_Control_Frame):
|
||||
CONNECTION_REFUSED_SOURCE_CID_ALREADY_ALLOCATED = 0x0007
|
||||
CONNECTION_REFUSED_UNACCEPTABLE_PARAMETERS = 0x000B
|
||||
|
||||
# pylint: disable=line-too-long
|
||||
RESULT_NAMES = {
|
||||
CONNECTION_SUCCESSFUL: 'CONNECTION_SUCCESSFUL',
|
||||
CONNECTION_PENDING: 'CONNECTION_PENDING',
|
||||
@@ -406,6 +417,7 @@ class L2CAP_Configure_Request(L2CAP_Control_Frame):
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@L2CAP_Control_Frame.subclass(
|
||||
# pylint: disable=unnecessary-lambda
|
||||
[
|
||||
('source_cid', 2),
|
||||
('flags', 2),
|
||||
@@ -481,6 +493,7 @@ class L2CAP_Echo_Response(L2CAP_Control_Frame):
|
||||
'info_type',
|
||||
{
|
||||
'size': 2,
|
||||
# pylint: disable-next=unnecessary-lambda
|
||||
'mapper': lambda x: L2CAP_Information_Request.info_type_name(x),
|
||||
},
|
||||
)
|
||||
@@ -524,6 +537,7 @@ class L2CAP_Information_Request(L2CAP_Control_Frame):
|
||||
('info_type', {'size': 2, 'mapper': L2CAP_Information_Request.info_type_name}),
|
||||
(
|
||||
'result',
|
||||
# pylint: disable-next=unnecessary-lambda
|
||||
{'size': 2, 'mapper': lambda x: L2CAP_Information_Response.result_name(x)},
|
||||
),
|
||||
('data', '*'),
|
||||
@@ -568,12 +582,14 @@ class L2CAP_Connection_Parameter_Update_Response(L2CAP_Control_Frame):
|
||||
)
|
||||
class L2CAP_LE_Credit_Based_Connection_Request(L2CAP_Control_Frame):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part A - 4.22 LE CREDIT BASED CONNECTION REQUEST (CODE 0x14)
|
||||
See Bluetooth spec @ Vol 3, Part A - 4.22 LE CREDIT BASED CONNECTION REQUEST
|
||||
(CODE 0x14)
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@L2CAP_Control_Frame.subclass(
|
||||
# pylint: disable=unnecessary-lambda,line-too-long
|
||||
[
|
||||
('destination_cid', 2),
|
||||
('mtu', 2),
|
||||
@@ -592,7 +608,8 @@ class L2CAP_LE_Credit_Based_Connection_Request(L2CAP_Control_Frame):
|
||||
)
|
||||
class L2CAP_LE_Credit_Based_Connection_Response(L2CAP_Control_Frame):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part A - 4.23 LE CREDIT BASED CONNECTION RESPONSE (CODE 0x15)
|
||||
See Bluetooth spec @ Vol 3, Part A - 4.23 LE CREDIT BASED CONNECTION RESPONSE
|
||||
(CODE 0x15)
|
||||
'''
|
||||
|
||||
CONNECTION_SUCCESSFUL = 0x0000
|
||||
@@ -606,6 +623,7 @@ class L2CAP_LE_Credit_Based_Connection_Response(L2CAP_Control_Frame):
|
||||
CONNECTION_REFUSED_SOURCE_CID_ALREADY_ALLOCATED = 0x000A
|
||||
CONNECTION_REFUSED_UNACCEPTABLE_PARAMETERS = 0x000B
|
||||
|
||||
# pylint: disable=line-too-long
|
||||
RESULT_NAMES = {
|
||||
CONNECTION_SUCCESSFUL: 'CONNECTION_SUCCESSFUL',
|
||||
CONNECTION_REFUSED_LE_PSM_NOT_SUPPORTED: 'CONNECTION_REFUSED_LE_PSM_NOT_SUPPORTED',
|
||||
@@ -693,6 +711,7 @@ class Channel(EventEmitter):
|
||||
self.destination_cid = 0
|
||||
self.response = None
|
||||
self.connection_result = None
|
||||
self.disconnection_result = None
|
||||
self.sink = None
|
||||
|
||||
def change_state(self, new_state):
|
||||
@@ -723,6 +742,7 @@ class Channel(EventEmitter):
|
||||
self.response.set_result(pdu)
|
||||
self.response = None
|
||||
elif self.sink:
|
||||
# pylint: disable=not-callable
|
||||
self.sink(pdu)
|
||||
else:
|
||||
logger.warning(
|
||||
@@ -746,7 +766,8 @@ class Channel(EventEmitter):
|
||||
)
|
||||
)
|
||||
|
||||
# Create a future to wait for the state machine to get to a success or error state
|
||||
# Create a future to wait for the state machine to get to a success or error
|
||||
# state
|
||||
self.connection_result = asyncio.get_running_loop().create_future()
|
||||
|
||||
# Wait for the connection to succeed or fail
|
||||
@@ -768,7 +789,8 @@ class Channel(EventEmitter):
|
||||
)
|
||||
)
|
||||
|
||||
# Create a future to wait for the state machine to get to a success or error state
|
||||
# Create a future to wait for the state machine to get to a success or error
|
||||
# state
|
||||
self.disconnection_result = asyncio.get_running_loop().create_future()
|
||||
return await self.disconnection_result
|
||||
|
||||
@@ -830,10 +852,10 @@ class Channel(EventEmitter):
|
||||
self.connection_result = None
|
||||
|
||||
def on_configure_request(self, request):
|
||||
if (
|
||||
self.state != Channel.WAIT_CONFIG
|
||||
and self.state != Channel.WAIT_CONFIG_REQ
|
||||
and self.state != Channel.WAIT_CONFIG_REQ_RSP
|
||||
if self.state not in (
|
||||
Channel.WAIT_CONFIG,
|
||||
Channel.WAIT_CONFIG_REQ,
|
||||
Channel.WAIT_CONFIG_REQ_RSP,
|
||||
):
|
||||
logger.warning(color('invalid state', 'red'))
|
||||
return
|
||||
@@ -871,10 +893,7 @@ class Channel(EventEmitter):
|
||||
if response.result == L2CAP_Configure_Response.SUCCESS:
|
||||
if self.state == Channel.WAIT_CONFIG_REQ_RSP:
|
||||
self.change_state(Channel.WAIT_CONFIG_REQ)
|
||||
elif (
|
||||
self.state == Channel.WAIT_CONFIG_RSP
|
||||
or self.state == Channel.WAIT_CONTROL_IND
|
||||
):
|
||||
elif self.state in (Channel.WAIT_CONFIG_RSP, Channel.WAIT_CONTROL_IND):
|
||||
self.change_state(Channel.OPEN)
|
||||
if self.connection_result:
|
||||
self.connection_result.set_result(None)
|
||||
@@ -897,14 +916,15 @@ class Channel(EventEmitter):
|
||||
else:
|
||||
logger.warning(
|
||||
color(
|
||||
f'!!! configuration rejected: {L2CAP_Configure_Response.result_name(response.result)}',
|
||||
'!!! configuration rejected: '
|
||||
f'{L2CAP_Configure_Response.result_name(response.result)}',
|
||||
'red',
|
||||
)
|
||||
)
|
||||
# TODO: decide how to fail gracefully
|
||||
|
||||
def on_disconnection_request(self, request):
|
||||
if self.state == Channel.OPEN or self.state == Channel.WAIT_DISCONNECT:
|
||||
if self.state in (Channel.OPEN, Channel.WAIT_DISCONNECT):
|
||||
self.send_control_frame(
|
||||
L2CAP_Disconnection_Response(
|
||||
identifier=request.identifier,
|
||||
@@ -938,7 +958,12 @@ class Channel(EventEmitter):
|
||||
self.manager.on_channel_closed(self)
|
||||
|
||||
def __str__(self):
|
||||
return f'Channel({self.source_cid}->{self.destination_cid}, PSM={self.psm}, MTU={self.mtu}, state={Channel.STATE_NAMES[self.state]})'
|
||||
return (
|
||||
f'Channel({self.source_cid}->{self.destination_cid}, '
|
||||
f'PSM={self.psm}, '
|
||||
f'MTU={self.mtu}, '
|
||||
f'state={Channel.STATE_NAMES[self.state]})'
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -976,7 +1001,7 @@ class LeConnectionOrientedChannel(EventEmitter):
|
||||
destination_cid,
|
||||
mtu,
|
||||
mps,
|
||||
credits,
|
||||
credits, # pylint: disable=redefined-builtin
|
||||
peer_mtu,
|
||||
peer_mps,
|
||||
peer_credits,
|
||||
@@ -1001,6 +1026,7 @@ class LeConnectionOrientedChannel(EventEmitter):
|
||||
self.out_queue = deque()
|
||||
self.out_sdu = None
|
||||
self.sink = None
|
||||
self.connected = False
|
||||
self.connection_result = None
|
||||
self.disconnection_result = None
|
||||
self.drained = asyncio.Event()
|
||||
@@ -1072,7 +1098,8 @@ class LeConnectionOrientedChannel(EventEmitter):
|
||||
)
|
||||
)
|
||||
|
||||
# Create a future to wait for the state machine to get to a success or error state
|
||||
# Create a future to wait for the state machine to get to a success or error
|
||||
# state
|
||||
self.disconnection_result = asyncio.get_running_loop().create_future()
|
||||
return await self.disconnection_result
|
||||
|
||||
@@ -1110,7 +1137,8 @@ class LeConnectionOrientedChannel(EventEmitter):
|
||||
|
||||
# Check if the SDU is complete
|
||||
if self.in_sdu_length == 0:
|
||||
# We don't know the size yet, check if we have received the header to compute it
|
||||
# We don't know the size yet, check if we have received the header to
|
||||
# compute it
|
||||
if len(self.in_sdu) >= 2:
|
||||
self.in_sdu_length = struct.unpack_from('<H', self.in_sdu, 0)[0]
|
||||
if self.in_sdu_length == 0:
|
||||
@@ -1125,7 +1153,8 @@ class LeConnectionOrientedChannel(EventEmitter):
|
||||
if len(self.in_sdu) != 2 + self.in_sdu_length:
|
||||
# Overflow
|
||||
logger.warning(
|
||||
f'SDU overflow: sdu_length={self.in_sdu_length}, received {len(self.in_sdu) - 2}'
|
||||
f'SDU overflow: sdu_length={self.in_sdu_length}, '
|
||||
f'received {len(self.in_sdu) - 2}'
|
||||
)
|
||||
# TODO: we should disconnect
|
||||
self.in_sdu = None
|
||||
@@ -1134,7 +1163,7 @@ class LeConnectionOrientedChannel(EventEmitter):
|
||||
|
||||
# Send the SDU to the sink
|
||||
logger.debug(f'SDU complete: 2+{len(self.in_sdu) - 2} bytes')
|
||||
self.sink(self.in_sdu[2:])
|
||||
self.sink(self.in_sdu[2:]) # pylint: disable=not-callable
|
||||
|
||||
# Prepare for a new SDU
|
||||
self.in_sdu = None
|
||||
@@ -1174,7 +1203,7 @@ class LeConnectionOrientedChannel(EventEmitter):
|
||||
# Cleanup
|
||||
self.connection_result = None
|
||||
|
||||
def on_credits(self, credits):
|
||||
def on_credits(self, credits): # pylint: disable=redefined-builtin
|
||||
self.credits += credits
|
||||
logger.debug(f'received {credits} credits, total = {self.credits}')
|
||||
|
||||
@@ -1228,7 +1257,8 @@ class LeConnectionOrientedChannel(EventEmitter):
|
||||
# Keep what's still left to send
|
||||
self.out_sdu = self.out_sdu[len(packet) :]
|
||||
continue
|
||||
elif self.out_queue:
|
||||
|
||||
if self.out_queue:
|
||||
# Create the next SDU (2 bytes header plus up to MTU bytes payload)
|
||||
logger.debug(
|
||||
f'assembling SDU from {len(self.out_queue)} packets in output queue'
|
||||
@@ -1282,13 +1312,20 @@ class LeConnectionOrientedChannel(EventEmitter):
|
||||
pass
|
||||
|
||||
def __str__(self):
|
||||
return f'CoC({self.source_cid}->{self.destination_cid}, State={self.state_name(self.state)}, PSM={self.le_psm}, MTU={self.mtu}/{self.peer_mtu}, MPS={self.mps}/{self.peer_mps}, credits={self.credits}/{self.peer_credits})'
|
||||
return (
|
||||
f'CoC({self.source_cid}->{self.destination_cid}, '
|
||||
f'State={self.state_name(self.state)}, '
|
||||
f'PSM={self.le_psm}, '
|
||||
f'MTU={self.mtu}/{self.peer_mtu}, '
|
||||
f'MPS={self.mps}/{self.peer_mps}, '
|
||||
f'credits={self.credits}/{self.peer_credits})'
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class ChannelManager:
|
||||
def __init__(
|
||||
self, extended_features=[], connectionless_mtu=L2CAP_DEFAULT_CONNECTIONLESS_MTU
|
||||
self, extended_features=(), connectionless_mtu=L2CAP_DEFAULT_CONNECTIONLESS_MTU
|
||||
):
|
||||
self._host = None
|
||||
self.identifiers = {} # Incrementing identifier values by connection
|
||||
@@ -1322,10 +1359,14 @@ class ChannelManager:
|
||||
if connection_channels := self.channels.get(connection_handle):
|
||||
return connection_channels.get(cid)
|
||||
|
||||
return None
|
||||
|
||||
def find_le_coc_channel(self, connection_handle, cid):
|
||||
if connection_channels := self.le_coc_channels.get(connection_handle):
|
||||
return connection_channels.get(cid)
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def find_free_br_edr_cid(channels):
|
||||
# Pick the smallest valid CID that's not already in the list
|
||||
@@ -1337,6 +1378,8 @@ class ChannelManager:
|
||||
if cid not in channels:
|
||||
return cid
|
||||
|
||||
raise RuntimeError('no free CID available')
|
||||
|
||||
@staticmethod
|
||||
def find_free_le_cid(channels):
|
||||
# Pick the smallest valid CID that's not already in the list
|
||||
@@ -1348,6 +1391,8 @@ class ChannelManager:
|
||||
if cid not in channels:
|
||||
return cid
|
||||
|
||||
raise RuntimeError('no free CID')
|
||||
|
||||
@staticmethod
|
||||
def check_le_coc_parameters(max_credits, mtu, mps):
|
||||
if (
|
||||
@@ -1442,7 +1487,7 @@ class ChannelManager:
|
||||
|
||||
return psm
|
||||
|
||||
def on_disconnection(self, connection_handle, reason):
|
||||
def on_disconnection(self, connection_handle, _reason):
|
||||
logger.debug(f'disconnection from {connection_handle}, cleaning up channels')
|
||||
if connection_handle in self.channels:
|
||||
del self.channels[connection_handle]
|
||||
@@ -1452,14 +1497,16 @@ class ChannelManager:
|
||||
del self.identifiers[connection_handle]
|
||||
|
||||
def send_pdu(self, connection, cid, pdu):
|
||||
pdu_str = pdu.hex() if type(pdu) is bytes else str(pdu)
|
||||
pdu_str = pdu.hex() if isinstance(pdu, bytes) else str(pdu)
|
||||
logger.debug(
|
||||
f'{color(">>> Sending L2CAP PDU", "blue")} on connection [0x{connection.handle:04X}] (CID={cid}) {connection.peer_address}: {pdu_str}'
|
||||
f'{color(">>> Sending L2CAP PDU", "blue")} '
|
||||
f'on connection [0x{connection.handle:04X}] (CID={cid}) '
|
||||
f'{connection.peer_address}: {pdu_str}'
|
||||
)
|
||||
self.host.send_l2cap_pdu(connection.handle, cid, bytes(pdu))
|
||||
|
||||
def on_pdu(self, connection, cid, pdu):
|
||||
if cid == L2CAP_SIGNALING_CID or cid == L2CAP_LE_SIGNALING_CID:
|
||||
if cid in (L2CAP_SIGNALING_CID, L2CAP_LE_SIGNALING_CID):
|
||||
# Parse the L2CAP payload into a Control Frame object
|
||||
control_frame = L2CAP_Control_Frame.from_bytes(pdu)
|
||||
|
||||
@@ -1479,13 +1526,17 @@ class ChannelManager:
|
||||
|
||||
def send_control_frame(self, connection, cid, control_frame):
|
||||
logger.debug(
|
||||
f'{color(">>> Sending L2CAP Signaling Control Frame", "blue")} on connection [0x{connection.handle:04X}] (CID={cid}) {connection.peer_address}:\n{control_frame}'
|
||||
f'{color(">>> Sending L2CAP Signaling Control Frame", "blue")} '
|
||||
f'on connection [0x{connection.handle:04X}] (CID={cid}) '
|
||||
f'{connection.peer_address}:\n{control_frame}'
|
||||
)
|
||||
self.host.send_l2cap_pdu(connection.handle, cid, bytes(control_frame))
|
||||
|
||||
def on_control_frame(self, connection, cid, control_frame):
|
||||
logger.debug(
|
||||
f'{color("<<< Received L2CAP Signaling Control Frame", "green")} on connection [0x{connection.handle:04X}] (CID={cid}) {connection.peer_address}:\n{control_frame}'
|
||||
f'{color("<<< Received L2CAP Signaling Control Frame", "green")} '
|
||||
f'on connection [0x{connection.handle:04X}] (CID={cid}) '
|
||||
f'{connection.peer_address}:\n{control_frame}'
|
||||
)
|
||||
|
||||
# Find the handler method
|
||||
@@ -1518,7 +1569,7 @@ class ChannelManager:
|
||||
),
|
||||
)
|
||||
|
||||
def on_l2cap_command_reject(self, connection, cid, packet):
|
||||
def on_l2cap_command_reject(self, _connection, _cid, packet):
|
||||
logger.warning(f'{color("!!! Command rejected:", "red")} {packet.reason}')
|
||||
|
||||
def on_l2cap_connection_request(self, connection, cid, request):
|
||||
@@ -1536,6 +1587,7 @@ class ChannelManager:
|
||||
identifier=request.identifier,
|
||||
destination_cid=request.source_cid,
|
||||
source_cid=0,
|
||||
# pylint: disable=line-too-long
|
||||
result=L2CAP_Connection_Response.CONNECTION_REFUSED_NO_RESOURCES_AVAILABLE,
|
||||
status=0x0000,
|
||||
),
|
||||
@@ -1556,7 +1608,8 @@ class ChannelManager:
|
||||
channel.on_connection_request(request)
|
||||
else:
|
||||
logger.warning(
|
||||
f'No server for connection 0x{connection.handle:04X} on PSM {request.psm}'
|
||||
f'No server for connection 0x{connection.handle:04X} '
|
||||
f'on PSM {request.psm}'
|
||||
)
|
||||
self.send_control_frame(
|
||||
connection,
|
||||
@@ -1565,6 +1618,7 @@ class ChannelManager:
|
||||
identifier=request.identifier,
|
||||
destination_cid=request.source_cid,
|
||||
source_cid=0,
|
||||
# pylint: disable=line-too-long
|
||||
result=L2CAP_Connection_Response.CONNECTION_REFUSED_LE_PSM_NOT_SUPPORTED,
|
||||
status=0x0000,
|
||||
),
|
||||
@@ -1576,7 +1630,8 @@ class ChannelManager:
|
||||
) is None:
|
||||
logger.warning(
|
||||
color(
|
||||
f'channel {response.source_cid} not found for 0x{connection.handle:04X}:{cid}',
|
||||
f'channel {response.source_cid} not found for '
|
||||
f'0x{connection.handle:04X}:{cid}',
|
||||
'red',
|
||||
)
|
||||
)
|
||||
@@ -1590,7 +1645,8 @@ class ChannelManager:
|
||||
) is None:
|
||||
logger.warning(
|
||||
color(
|
||||
f'channel {request.destination_cid} not found for 0x{connection.handle:04X}:{cid}',
|
||||
f'channel {request.destination_cid} not found for '
|
||||
f'0x{connection.handle:04X}:{cid}',
|
||||
'red',
|
||||
)
|
||||
)
|
||||
@@ -1604,7 +1660,8 @@ class ChannelManager:
|
||||
) is None:
|
||||
logger.warning(
|
||||
color(
|
||||
f'channel {response.source_cid} not found for 0x{connection.handle:04X}:{cid}',
|
||||
f'channel {response.source_cid} not found for '
|
||||
f'0x{connection.handle:04X}:{cid}',
|
||||
'red',
|
||||
)
|
||||
)
|
||||
@@ -1618,7 +1675,8 @@ class ChannelManager:
|
||||
) is None:
|
||||
logger.warning(
|
||||
color(
|
||||
f'channel {request.destination_cid} not found for 0x{connection.handle:04X}:{cid}',
|
||||
f'channel {request.destination_cid} not found for '
|
||||
f'0x{connection.handle:04X}:{cid}',
|
||||
'red',
|
||||
)
|
||||
)
|
||||
@@ -1632,7 +1690,8 @@ class ChannelManager:
|
||||
) is None:
|
||||
logger.warning(
|
||||
color(
|
||||
f'channel {response.source_cid} not found for 0x{connection.handle:04X}:{cid}',
|
||||
f'channel {response.source_cid} not found for '
|
||||
f'0x{connection.handle:04X}:{cid}',
|
||||
'red',
|
||||
)
|
||||
)
|
||||
@@ -1648,7 +1707,7 @@ class ChannelManager:
|
||||
L2CAP_Echo_Response(identifier=request.identifier, data=request.data),
|
||||
)
|
||||
|
||||
def on_l2cap_echo_response(self, connection, cid, response):
|
||||
def on_l2cap_echo_response(self, _connection, _cid, response):
|
||||
logger.debug(f'<<< Echo response: data={response.data.hex()}')
|
||||
# TODO notify listeners
|
||||
|
||||
@@ -1663,7 +1722,7 @@ class ChannelManager:
|
||||
result = L2CAP_Information_Response.SUCCESS
|
||||
data = sum(1 << cid for cid in self.fixed_channels).to_bytes(8, 'little')
|
||||
else:
|
||||
result = L2CAP_Information_Request.NO_SUPPORTED
|
||||
result = L2CAP_Information_Response.NOT_SUPPORTED
|
||||
|
||||
self.send_control_frame(
|
||||
connection,
|
||||
@@ -1730,6 +1789,7 @@ class ChannelManager:
|
||||
mtu=mtu,
|
||||
mps=mps,
|
||||
initial_credits=0,
|
||||
# pylint: disable=line-too-long
|
||||
result=L2CAP_LE_Credit_Based_Connection_Response.CONNECTION_REFUSED_SOURCE_CID_ALREADY_ALLOCATED,
|
||||
),
|
||||
)
|
||||
@@ -1748,6 +1808,7 @@ class ChannelManager:
|
||||
mtu=mtu,
|
||||
mps=mps,
|
||||
initial_credits=0,
|
||||
# pylint: disable=line-too-long
|
||||
result=L2CAP_LE_Credit_Based_Connection_Response.CONNECTION_REFUSED_NO_RESOURCES_AVAILABLE,
|
||||
),
|
||||
)
|
||||
@@ -1755,7 +1816,8 @@ class ChannelManager:
|
||||
|
||||
# Create a new channel
|
||||
logger.debug(
|
||||
f'creating LE CoC server channel with cid={source_cid} for psm {request.le_psm}'
|
||||
f'creating LE CoC server channel with cid={source_cid} for psm '
|
||||
f'{request.le_psm}'
|
||||
)
|
||||
channel = LeConnectionOrientedChannel(
|
||||
self,
|
||||
@@ -1784,6 +1846,7 @@ class ChannelManager:
|
||||
mtu=mtu,
|
||||
mps=mps,
|
||||
initial_credits=max_credits,
|
||||
# pylint: disable=line-too-long
|
||||
result=L2CAP_LE_Credit_Based_Connection_Response.CONNECTION_SUCCESSFUL,
|
||||
),
|
||||
)
|
||||
@@ -1792,7 +1855,8 @@ class ChannelManager:
|
||||
server(channel)
|
||||
else:
|
||||
logger.info(
|
||||
f'No LE server for connection 0x{connection.handle:04X} on PSM {request.le_psm}'
|
||||
f'No LE server for connection 0x{connection.handle:04X} '
|
||||
f'on PSM {request.le_psm}'
|
||||
)
|
||||
self.send_control_frame(
|
||||
connection,
|
||||
@@ -1803,11 +1867,12 @@ class ChannelManager:
|
||||
mtu=L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MTU,
|
||||
mps=L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MPS,
|
||||
initial_credits=0,
|
||||
# pylint: disable=line-too-long
|
||||
result=L2CAP_LE_Credit_Based_Connection_Response.CONNECTION_REFUSED_LE_PSM_NOT_SUPPORTED,
|
||||
),
|
||||
)
|
||||
|
||||
def on_l2cap_le_credit_based_connection_response(self, connection, cid, response):
|
||||
def on_l2cap_le_credit_based_connection_response(self, connection, _cid, response):
|
||||
# Find the pending request by identifier
|
||||
request = self.le_coc_requests.get(response.identifier)
|
||||
if request is None:
|
||||
@@ -1820,7 +1885,8 @@ class ChannelManager:
|
||||
if channel is None:
|
||||
logger.warning(
|
||||
color(
|
||||
f'received connection response for an unknown channel (cid={request.source_cid})',
|
||||
'received connection response for an unknown channel '
|
||||
f'(cid={request.source_cid})',
|
||||
'red',
|
||||
)
|
||||
)
|
||||
@@ -1829,7 +1895,7 @@ class ChannelManager:
|
||||
# Process the response
|
||||
channel.on_connection_response(response)
|
||||
|
||||
def on_l2cap_le_flow_control_credit(self, connection, cid, credit):
|
||||
def on_l2cap_le_flow_control_credit(self, connection, _cid, credit):
|
||||
channel = self.find_le_coc_channel(connection.handle, credit.cid)
|
||||
if channel is None:
|
||||
logger.warning(f'received credits for an unknown channel (cid={credit.cid}')
|
||||
|
||||
@@ -17,9 +17,10 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
import logging
|
||||
import asyncio
|
||||
import websockets
|
||||
from functools import partial
|
||||
|
||||
from colors import color
|
||||
import websockets
|
||||
|
||||
from bumble.hci import (
|
||||
Address,
|
||||
@@ -47,7 +48,8 @@ def parse_parameters(params_str):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# TODO: add more support for various LL exchanges (see Vol 6, Part B - 2.4 DATA CHANNEL PDU)
|
||||
# TODO: add more support for various LL exchanges
|
||||
# (see Vol 6, Part B - 2.4 DATA CHANNEL PDU)
|
||||
# -----------------------------------------------------------------------------
|
||||
class LocalLink:
|
||||
'''
|
||||
@@ -119,7 +121,8 @@ class LocalLink:
|
||||
|
||||
def connect(self, central_address, le_create_connection_command):
|
||||
logger.debug(
|
||||
f'$$$ CONNECTION {central_address} -> {le_create_connection_command.peer_address}'
|
||||
f'$$$ CONNECTION {central_address} -> '
|
||||
f'{le_create_connection_command.peer_address}'
|
||||
)
|
||||
self.pending_connection = (central_address, le_create_connection_command)
|
||||
asyncio.get_running_loop().call_soon(self.on_connection_complete)
|
||||
@@ -144,11 +147,13 @@ class LocalLink:
|
||||
|
||||
def disconnect(self, central_address, peripheral_address, disconnect_command):
|
||||
logger.debug(
|
||||
f'$$$ DISCONNECTION {central_address} -> {peripheral_address}: reason = {disconnect_command.reason}'
|
||||
f'$$$ DISCONNECTION {central_address} -> '
|
||||
f'{peripheral_address}: reason = {disconnect_command.reason}'
|
||||
)
|
||||
args = [central_address, peripheral_address, disconnect_command]
|
||||
asyncio.get_running_loop().call_soon(self.on_disconnection_complete, *args)
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def on_connection_encrypted(
|
||||
self, central_address, peripheral_address, rand, ediv, ltk
|
||||
):
|
||||
@@ -217,6 +222,7 @@ class RemoteLink:
|
||||
async def run_connection(self):
|
||||
# Connect to the relay
|
||||
logger.debug(f'connecting to {self.uri}')
|
||||
# pylint: disable-next=no-member
|
||||
websocket = await websockets.connect(self.uri)
|
||||
self.websocket.set_result(websocket)
|
||||
logger.debug(f'connected to {self.uri}')
|
||||
@@ -287,11 +293,11 @@ class RemoteLink:
|
||||
self.controller.on_link_central_connected(Address(sender))
|
||||
|
||||
# Accept the connection by responding to it
|
||||
await self.send_targetted_message(sender, 'connected')
|
||||
await self.send_targeted_message(sender, 'connected')
|
||||
|
||||
async def on_connected_message_received(self, sender, _):
|
||||
if not self.pending_connection:
|
||||
logger.warn('received a connection ack, but no connection is pending')
|
||||
logger.warning('received a connection ack, but no connection is pending')
|
||||
return
|
||||
|
||||
# Remember the connection
|
||||
@@ -313,7 +319,7 @@ class RemoteLink:
|
||||
if sender in self.peripheral_connections:
|
||||
self.peripheral_connections.remove(sender)
|
||||
|
||||
async def on_encrypted_message_received(self, sender, message):
|
||||
async def on_encrypted_message_received(self, sender, _):
|
||||
# TODO parse params to get real args
|
||||
self.controller.on_link_encrypted(Address(sender), bytes(8), 0, bytes(16))
|
||||
|
||||
@@ -335,7 +341,7 @@ class RemoteLink:
|
||||
|
||||
# TODO: parse the result
|
||||
|
||||
async def send_targetted_message(self, target, message):
|
||||
async def send_targeted_message(self, target, message):
|
||||
# Ensure we have a connection
|
||||
websocket = await self.websocket
|
||||
|
||||
@@ -352,23 +358,23 @@ class RemoteLink:
|
||||
self.execute(self.notify_address_changed)
|
||||
|
||||
async def send_advertising_data_to_relay(self, data):
|
||||
await self.send_targetted_message('*', f'advertisement:{data.hex()}')
|
||||
await self.send_targeted_message('*', f'advertisement:{data.hex()}')
|
||||
|
||||
def send_advertising_data(self, sender_address, data):
|
||||
def send_advertising_data(self, _, data):
|
||||
self.execute(partial(self.send_advertising_data_to_relay, data))
|
||||
|
||||
async def send_acl_data_to_relay(self, peer_address, data):
|
||||
await self.send_targetted_message(peer_address, f'acl:{data.hex()}')
|
||||
await self.send_targeted_message(peer_address, f'acl:{data.hex()}')
|
||||
|
||||
def send_acl_data(self, sender_address, peer_address, data):
|
||||
def send_acl_data(self, _, peer_address, data):
|
||||
self.execute(partial(self.send_acl_data_to_relay, peer_address, data))
|
||||
|
||||
async def send_connection_request_to_relay(self, peer_address):
|
||||
await self.send_targetted_message(peer_address, 'connect')
|
||||
await self.send_targeted_message(peer_address, 'connect')
|
||||
|
||||
def connect(self, central_address, le_create_connection_command):
|
||||
def connect(self, _, le_create_connection_command):
|
||||
if self.pending_connection:
|
||||
logger.warn('connection already pending')
|
||||
logger.warning('connection already pending')
|
||||
return
|
||||
self.pending_connection = le_create_connection_command
|
||||
self.execute(
|
||||
@@ -385,11 +391,12 @@ class RemoteLink:
|
||||
|
||||
def disconnect(self, central_address, peripheral_address, disconnect_command):
|
||||
logger.debug(
|
||||
f'disconnect {central_address} -> {peripheral_address}: reason = {disconnect_command.reason}'
|
||||
f'disconnect {central_address} -> '
|
||||
f'{peripheral_address}: reason = {disconnect_command.reason}'
|
||||
)
|
||||
self.execute(
|
||||
partial(
|
||||
self.send_targetted_message,
|
||||
self.send_targeted_message,
|
||||
peripheral_address,
|
||||
f'disconnect:reason={disconnect_command.reason}',
|
||||
)
|
||||
@@ -398,15 +405,13 @@ class RemoteLink:
|
||||
self.on_disconnection_complete, disconnect_command
|
||||
)
|
||||
|
||||
def on_connection_encrypted(
|
||||
self, central_address, peripheral_address, rand, ediv, ltk
|
||||
):
|
||||
def on_connection_encrypted(self, _, peripheral_address, rand, ediv, ltk):
|
||||
asyncio.get_running_loop().call_soon(
|
||||
self.controller.on_link_encrypted, peripheral_address, rand, ediv, ltk
|
||||
)
|
||||
self.execute(
|
||||
partial(
|
||||
self.send_targetted_message,
|
||||
self.send_targeted_message,
|
||||
peripheral_address,
|
||||
f'encrypted:ltk={ltk.hex()}',
|
||||
)
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
import struct
|
||||
import logging
|
||||
from typing import List
|
||||
from ..core import AdvertisingData
|
||||
from ..gatt import (
|
||||
GATT_ASHA_SERVICE,
|
||||
@@ -29,7 +30,6 @@ from ..gatt import (
|
||||
TemplateService,
|
||||
Characteristic,
|
||||
CharacteristicValue,
|
||||
PackedCharacteristicAdapter,
|
||||
)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -50,23 +50,26 @@ class AshaService(TemplateService):
|
||||
SUPPORTED_CODEC_ID = [0x02, 0x01] # Codec IDs [G.722 at 16 kHz]
|
||||
RENDER_DELAY = [00, 00]
|
||||
|
||||
def __init__(self, capability: int, hisyncid: [int]):
|
||||
def __init__(self, capability: int, hisyncid: List[int]):
|
||||
self.hisyncid = hisyncid
|
||||
self.capability = capability # Device Capabilities [Left, Monaural]
|
||||
|
||||
# Handler for volume control
|
||||
def on_volume_write(connection, value):
|
||||
def on_volume_write(_connection, value):
|
||||
logger.info(f'--- VOLUME Write:{value[0]}')
|
||||
|
||||
# Handler for audio control commands
|
||||
def on_audio_control_point_write(connection, value):
|
||||
def on_audio_control_point_write(_connection, value):
|
||||
logger.info(f'--- AUDIO CONTROL POINT Write:{value.hex()}')
|
||||
opcode = value[0]
|
||||
if opcode == AshaService.OPCODE_START:
|
||||
# Start
|
||||
audio_type = ('Unknown', 'Ringtone', 'Phone Call', 'Media')[value[2]]
|
||||
logger.info(
|
||||
f'### START: codec={value[1]}, audio_type={audio_type}, volume={value[3]}, otherstate={value[4]}'
|
||||
f'### START: codec={value[1]}, '
|
||||
f'audio_type={audio_type}, '
|
||||
f'volume={value[3]}, '
|
||||
f'otherstate={value[4]}'
|
||||
)
|
||||
elif opcode == AshaService.OPCODE_STOP:
|
||||
logger.info('### STOP')
|
||||
@@ -74,7 +77,8 @@ class AshaService(TemplateService):
|
||||
logger.info(f'### STATUS: connected={value[1]}')
|
||||
|
||||
# TODO Respond with a status
|
||||
# asyncio.create_task(device.notify_subscribers(audio_status_characteristic, force=True))
|
||||
# asyncio.create_task(device.notify_subscribers(audio_status_characteristic,
|
||||
# force=True))
|
||||
|
||||
self.read_only_properties_characteristic = Characteristic(
|
||||
GATT_ASHA_READ_ONLY_PROPERTIES_CHARACTERISTIC,
|
||||
|
||||
@@ -40,7 +40,7 @@ class BatteryService(TemplateService):
|
||||
Characteristic.READABLE,
|
||||
CharacteristicValue(read=read_battery_level),
|
||||
),
|
||||
format=BatteryService.BATTERY_LEVEL_FORMAT,
|
||||
pack_format=BatteryService.BATTERY_LEVEL_FORMAT,
|
||||
)
|
||||
super().__init__([self.battery_level_characteristic])
|
||||
|
||||
@@ -56,7 +56,7 @@ class BatteryServiceProxy(ProfileServiceProxy):
|
||||
GATT_BATTERY_LEVEL_CHARACTERISTIC
|
||||
):
|
||||
self.battery_level = PackedCharacteristicAdapter(
|
||||
characteristics[0], format=BatteryService.BATTERY_LEVEL_FORMAT
|
||||
characteristics[0], pack_format=BatteryService.BATTERY_LEVEL_FORMAT
|
||||
)
|
||||
else:
|
||||
self.battery_level = None
|
||||
|
||||
@@ -156,6 +156,7 @@ class HeartRateService(TemplateService):
|
||||
0,
|
||||
CharacteristicValue(read=read_heart_rate_measurement),
|
||||
),
|
||||
# pylint: disable=unnecessary-lambda
|
||||
encode=lambda value: bytes(value),
|
||||
)
|
||||
characteristics = [self.heart_rate_measurement_characteristic]
|
||||
@@ -185,7 +186,7 @@ class HeartRateService(TemplateService):
|
||||
Characteristic.WRITEABLE,
|
||||
CharacteristicValue(write=write_heart_rate_control_point_value),
|
||||
),
|
||||
format=HeartRateService.HEART_RATE_CONTROL_POINT_FORMAT,
|
||||
pack_format=HeartRateService.HEART_RATE_CONTROL_POINT_FORMAT,
|
||||
)
|
||||
characteristics.append(self.heart_rate_control_point_characteristic)
|
||||
|
||||
@@ -224,7 +225,7 @@ class HeartRateServiceProxy(ProfileServiceProxy):
|
||||
):
|
||||
self.heart_rate_control_point = PackedCharacteristicAdapter(
|
||||
characteristics[0],
|
||||
format=HeartRateService.HEART_RATE_CONTROL_POINT_FORMAT,
|
||||
pack_format=HeartRateService.HEART_RATE_CONTROL_POINT_FORMAT,
|
||||
)
|
||||
else:
|
||||
self.heart_rate_control_point = None
|
||||
|
||||
167
bumble/rfcomm.py
167
bumble/rfcomm.py
@@ -21,7 +21,8 @@ import asyncio
|
||||
from colors import color
|
||||
from pyee import EventEmitter
|
||||
|
||||
from .core import BT_BR_EDR_TRANSPORT, InvalidStateError, ProtocolError, ConnectionError
|
||||
from . import core
|
||||
from .core import BT_BR_EDR_TRANSPORT, InvalidStateError, ProtocolError
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -104,17 +105,17 @@ RFCOMM_DYNAMIC_CHANNEL_NUMBER_END = 30
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def fcs(buffer):
|
||||
fcs = 0xFF
|
||||
def compute_fcs(buffer):
|
||||
result = 0xFF
|
||||
for byte in buffer:
|
||||
fcs = CRC_TABLE[fcs ^ byte]
|
||||
return 0xFF - fcs
|
||||
result = CRC_TABLE[result ^ byte]
|
||||
return 0xFF - result
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class RFCOMM_Frame:
|
||||
def __init__(self, type, c_r, dlci, p_f, information=b'', with_credits=False):
|
||||
self.type = type
|
||||
def __init__(self, frame_type, c_r, dlci, p_f, information=b'', with_credits=False):
|
||||
self.type = frame_type
|
||||
self.c_r = c_r
|
||||
self.dlci = dlci
|
||||
self.p_f = p_f
|
||||
@@ -129,18 +130,18 @@ class RFCOMM_Frame:
|
||||
# 1-byte length indicator
|
||||
self.length = bytes([(length << 1) | 1])
|
||||
self.address = (dlci << 2) | (c_r << 1) | 1
|
||||
self.control = type | (p_f << 4)
|
||||
if type == RFCOMM_UIH_FRAME:
|
||||
self.fcs = fcs(bytes([self.address, self.control]))
|
||||
self.control = frame_type | (p_f << 4)
|
||||
if frame_type == RFCOMM_UIH_FRAME:
|
||||
self.fcs = compute_fcs(bytes([self.address, self.control]))
|
||||
else:
|
||||
self.fcs = fcs(bytes([self.address, self.control]) + self.length)
|
||||
self.fcs = compute_fcs(bytes([self.address, self.control]) + self.length)
|
||||
|
||||
def type_name(self):
|
||||
return RFCOMM_FRAME_TYPE_NAMES[self.type]
|
||||
|
||||
@staticmethod
|
||||
def parse_mcc(data):
|
||||
type = data[0] >> 2
|
||||
mcc_type = data[0] >> 2
|
||||
c_r = (data[0] >> 1) & 1
|
||||
length = data[1]
|
||||
if data[1] & 1:
|
||||
@@ -150,12 +151,12 @@ class RFCOMM_Frame:
|
||||
length = (data[3] << 7) & (length >> 1)
|
||||
value = data[3 : 3 + length]
|
||||
|
||||
return (type, c_r, value)
|
||||
return (mcc_type, c_r, value)
|
||||
|
||||
@staticmethod
|
||||
def make_mcc(type, c_r, data):
|
||||
def make_mcc(mcc_type, c_r, data):
|
||||
return (
|
||||
bytes([(type << 2 | c_r << 1 | 1) & 0xFF, (len(data) & 0x7F) << 1 | 1])
|
||||
bytes([(mcc_type << 2 | c_r << 1 | 1) & 0xFF, (len(data) & 0x7F) << 1 | 1])
|
||||
+ data
|
||||
)
|
||||
|
||||
@@ -186,7 +187,7 @@ class RFCOMM_Frame:
|
||||
# Extract fields
|
||||
dlci = (data[0] >> 2) & 0x3F
|
||||
c_r = (data[0] >> 1) & 0x01
|
||||
type = data[1] & 0xEF
|
||||
frame_type = data[1] & 0xEF
|
||||
p_f = (data[1] >> 4) & 0x01
|
||||
length = data[2]
|
||||
if length & 0x01:
|
||||
@@ -198,9 +199,9 @@ class RFCOMM_Frame:
|
||||
fcs = data[-1]
|
||||
|
||||
# Construct the frame and check the CRC
|
||||
frame = RFCOMM_Frame(type, c_r, dlci, p_f, information)
|
||||
frame = RFCOMM_Frame(frame_type, c_r, dlci, p_f, information)
|
||||
if frame.fcs != fcs:
|
||||
logger.warn(f'FCS mismatch: got {fcs:02X}, expected {frame.fcs:02X}')
|
||||
logger.warning(f'FCS mismatch: got {fcs:02X}, expected {frame.fcs:02X}')
|
||||
raise ValueError('fcs mismatch')
|
||||
|
||||
return frame
|
||||
@@ -214,7 +215,14 @@ class RFCOMM_Frame:
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f'{color(self.type_name(), "yellow")}(c/r={self.c_r},dlci={self.dlci},p/f={self.p_f},length={len(self.information)},fcs=0x{self.fcs:02X})'
|
||||
return (
|
||||
f'{color(self.type_name(), "yellow")}'
|
||||
f'(c/r={self.c_r},'
|
||||
f'dlci={self.dlci},'
|
||||
f'p/f={self.p_f},'
|
||||
f'length={len(self.information)},'
|
||||
f'fcs=0x{self.fcs:02X})'
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -264,7 +272,15 @@ class RFCOMM_MCC_PN:
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f'PN(dlci={self.dlci},cl={self.cl},priority={self.priority},ack_timer={self.ack_timer},max_frame_size={self.max_frame_size},max_retransmissions={self.max_retransmissions},window_size={self.window_size})'
|
||||
return (
|
||||
f'PN(dlci={self.dlci},'
|
||||
f'cl={self.cl},'
|
||||
f'priority={self.priority},'
|
||||
f'ack_timer={self.ack_timer},'
|
||||
f'max_frame_size={self.max_frame_size},'
|
||||
f'max_retransmissions={self.max_retransmissions},'
|
||||
f'window_size={self.window_size})'
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -302,7 +318,14 @@ class RFCOMM_MCC_MSC:
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f'MSC(dlci={self.dlci},fc={self.fc},rtc={self.rtc},rtr={self.rtr},ic={self.ic},dv={self.dv})'
|
||||
return (
|
||||
f'MSC(dlci={self.dlci},'
|
||||
f'fc={self.fc},'
|
||||
f'rtc={self.rtc},'
|
||||
f'rtr={self.rtr},'
|
||||
f'ic={self.ic},'
|
||||
f'dv={self.dv})'
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -336,6 +359,7 @@ class DLC(EventEmitter):
|
||||
self.role = multiplexer.role
|
||||
self.c_r = 1 if self.role == Multiplexer.INITIATOR else 0
|
||||
self.sink = None
|
||||
self.connection_result = None
|
||||
|
||||
# Compute the MTU
|
||||
max_overhead = 4 + 1 # header with 2-byte length + fcs
|
||||
@@ -360,30 +384,38 @@ class DLC(EventEmitter):
|
||||
handler = getattr(self, f'on_{frame.type_name()}_frame'.lower())
|
||||
handler(frame)
|
||||
|
||||
def on_sabm_frame(self, frame):
|
||||
def on_sabm_frame(self, _frame):
|
||||
if self.state != DLC.CONNECTING:
|
||||
logger.warn(color('!!! received SABM when not in CONNECTING state', 'red'))
|
||||
logger.warning(
|
||||
color('!!! received SABM when not in CONNECTING state', 'red')
|
||||
)
|
||||
return
|
||||
|
||||
self.send_frame(RFCOMM_Frame.ua(c_r=1 - self.c_r, dlci=self.dlci))
|
||||
|
||||
# Exchange the modem status with the peer
|
||||
msc = RFCOMM_MCC_MSC(dlci=self.dlci, fc=0, rtc=1, rtr=1, ic=0, dv=1)
|
||||
mcc = RFCOMM_Frame.make_mcc(type=RFCOMM_MCC_MSC_TYPE, c_r=1, data=bytes(msc))
|
||||
mcc = RFCOMM_Frame.make_mcc(
|
||||
mcc_type=RFCOMM_MCC_MSC_TYPE, c_r=1, data=bytes(msc)
|
||||
)
|
||||
logger.debug(f'>>> MCC MSC Command: {msc}')
|
||||
self.send_frame(RFCOMM_Frame.uih(c_r=self.c_r, dlci=0, information=mcc))
|
||||
|
||||
self.change_state(DLC.CONNECTED)
|
||||
self.emit('open')
|
||||
|
||||
def on_ua_frame(self, frame):
|
||||
def on_ua_frame(self, _frame):
|
||||
if self.state != DLC.CONNECTING:
|
||||
logger.warn(color('!!! received SABM when not in CONNECTING state', 'red'))
|
||||
logger.warning(
|
||||
color('!!! received SABM when not in CONNECTING state', 'red')
|
||||
)
|
||||
return
|
||||
|
||||
# Exchange the modem status with the peer
|
||||
msc = RFCOMM_MCC_MSC(dlci=self.dlci, fc=0, rtc=1, rtr=1, ic=0, dv=1)
|
||||
mcc = RFCOMM_Frame.make_mcc(type=RFCOMM_MCC_MSC_TYPE, c_r=1, data=bytes(msc))
|
||||
mcc = RFCOMM_Frame.make_mcc(
|
||||
mcc_type=RFCOMM_MCC_MSC_TYPE, c_r=1, data=bytes(msc)
|
||||
)
|
||||
logger.debug(f'>>> MCC MSC Command: {msc}')
|
||||
self.send_frame(RFCOMM_Frame.uih(c_r=self.c_r, dlci=0, information=mcc))
|
||||
|
||||
@@ -394,7 +426,7 @@ class DLC(EventEmitter):
|
||||
# TODO: handle all states
|
||||
pass
|
||||
|
||||
def on_disc_frame(self, frame):
|
||||
def on_disc_frame(self, _frame):
|
||||
# TODO: handle all states
|
||||
self.send_frame(RFCOMM_Frame.ua(c_r=1 - self.c_r, dlci=self.dlci))
|
||||
|
||||
@@ -402,25 +434,28 @@ class DLC(EventEmitter):
|
||||
data = frame.information
|
||||
if frame.p_f == 1:
|
||||
# With credits
|
||||
credits = frame.information[0]
|
||||
self.tx_credits += credits
|
||||
received_credits = frame.information[0]
|
||||
self.tx_credits += received_credits
|
||||
|
||||
logger.debug(
|
||||
f'<<< Credits [{self.dlci}]: received {credits}, total={self.tx_credits}'
|
||||
f'<<< Credits [{self.dlci}]: '
|
||||
f'received {credits}, total={self.tx_credits}'
|
||||
)
|
||||
data = data[1:]
|
||||
|
||||
logger.debug(
|
||||
f'{color("<<< Data", "yellow")} [{self.dlci}] {len(data)} bytes, rx_credits={self.rx_credits}: {data.hex()}'
|
||||
f'{color("<<< Data", "yellow")} '
|
||||
f'[{self.dlci}] {len(data)} bytes, '
|
||||
f'rx_credits={self.rx_credits}: {data.hex()}'
|
||||
)
|
||||
if len(data) and self.sink:
|
||||
self.sink(data)
|
||||
self.sink(data) # pylint: disable=not-callable
|
||||
|
||||
# Update the credits
|
||||
if self.rx_credits > 0:
|
||||
self.rx_credits -= 1
|
||||
else:
|
||||
logger.warn(color('!!! received frame with no rx credits', 'red'))
|
||||
logger.warning(color('!!! received frame with no rx credits', 'red'))
|
||||
|
||||
# Check if there's anything to send (including credits)
|
||||
self.process_tx()
|
||||
@@ -434,7 +469,7 @@ class DLC(EventEmitter):
|
||||
logger.debug(f'<<< MCC MSC Command: {msc}')
|
||||
msc = RFCOMM_MCC_MSC(dlci=self.dlci, fc=0, rtc=1, rtr=1, ic=0, dv=1)
|
||||
mcc = RFCOMM_Frame.make_mcc(
|
||||
type=RFCOMM_MCC_MSC_TYPE, c_r=0, data=bytes(msc)
|
||||
mcc_type=RFCOMM_MCC_MSC_TYPE, c_r=0, data=bytes(msc)
|
||||
)
|
||||
logger.debug(f'>>> MCC MSC Response: {msc}')
|
||||
self.send_frame(RFCOMM_Frame.uih(c_r=self.c_r, dlci=0, information=mcc))
|
||||
@@ -443,7 +478,7 @@ class DLC(EventEmitter):
|
||||
logger.debug(f'<<< MCC MSC Response: {msc}')
|
||||
|
||||
def connect(self):
|
||||
if not self.state == DLC.INIT:
|
||||
if self.state != DLC.INIT:
|
||||
raise InvalidStateError('invalid state')
|
||||
|
||||
self.change_state(DLC.CONNECTING)
|
||||
@@ -451,7 +486,7 @@ class DLC(EventEmitter):
|
||||
self.send_frame(RFCOMM_Frame.sabm(c_r=self.c_r, dlci=self.dlci))
|
||||
|
||||
def accept(self):
|
||||
if not self.state == DLC.INIT:
|
||||
if self.state != DLC.INIT:
|
||||
raise InvalidStateError('invalid state')
|
||||
|
||||
pn = RFCOMM_MCC_PN(
|
||||
@@ -463,7 +498,7 @@ class DLC(EventEmitter):
|
||||
max_retransmissions=0,
|
||||
window_size=RFCOMM_DEFAULT_INITIAL_RX_CREDITS,
|
||||
)
|
||||
mcc = RFCOMM_Frame.make_mcc(type=RFCOMM_MCC_PN_TYPE, c_r=0, data=bytes(pn))
|
||||
mcc = RFCOMM_Frame.make_mcc(mcc_type=RFCOMM_MCC_PN_TYPE, c_r=0, data=bytes(pn))
|
||||
logger.debug(f'>>> PN Response: {pn}')
|
||||
self.send_frame(RFCOMM_Frame.uih(c_r=self.c_r, dlci=0, information=mcc))
|
||||
self.change_state(DLC.CONNECTING)
|
||||
@@ -471,8 +506,8 @@ class DLC(EventEmitter):
|
||||
def rx_credits_needed(self):
|
||||
if self.rx_credits <= self.rx_threshold:
|
||||
return RFCOMM_DEFAULT_INITIAL_RX_CREDITS - self.rx_credits
|
||||
else:
|
||||
return 0
|
||||
|
||||
return 0
|
||||
|
||||
def process_tx(self):
|
||||
# Send anything we can (or an empty frame if we need to send rx credits)
|
||||
@@ -496,7 +531,9 @@ class DLC(EventEmitter):
|
||||
|
||||
# Send the frame
|
||||
logger.debug(
|
||||
f'>>> sending {len(chunk)} bytes with {rx_credits_needed} credits, rx_credits={self.rx_credits}, tx_credits={self.tx_credits}'
|
||||
f'>>> sending {len(chunk)} bytes with {rx_credits_needed} credits, '
|
||||
f'rx_credits={self.rx_credits}, '
|
||||
f'tx_credits={self.tx_credits}'
|
||||
)
|
||||
self.send_frame(
|
||||
RFCOMM_Frame.uih(
|
||||
@@ -512,8 +549,8 @@ class DLC(EventEmitter):
|
||||
# Stream protocol
|
||||
def write(self, data):
|
||||
# We can only send bytes
|
||||
if type(data) != bytes:
|
||||
if type(data) == str:
|
||||
if not isinstance(data, bytes):
|
||||
if isinstance(data, str):
|
||||
# Automatically convert strings to bytes using UTF-8
|
||||
data = data.encode('utf-8')
|
||||
else:
|
||||
@@ -592,14 +629,14 @@ class Multiplexer(EventEmitter):
|
||||
self.on_frame(frame)
|
||||
else:
|
||||
if frame.type == RFCOMM_DM_FRAME:
|
||||
# DM responses are for a DLCI, but since we only create the dlc when we receive
|
||||
# a PN response (because we need the parameters), we handle DM frames at the Multiplexer
|
||||
# level
|
||||
# DM responses are for a DLCI, but since we only create the dlc when we
|
||||
# receive a PN response (because we need the parameters), we handle DM
|
||||
# frames at the Multiplexer level
|
||||
self.on_dm_frame(frame)
|
||||
else:
|
||||
dlc = self.dlcs.get(frame.dlci)
|
||||
if dlc is None:
|
||||
logger.warn(f'no dlc for DLCI {frame.dlci}')
|
||||
logger.warning(f'no dlc for DLCI {frame.dlci}')
|
||||
return
|
||||
dlc.on_frame(frame)
|
||||
|
||||
@@ -607,14 +644,14 @@ class Multiplexer(EventEmitter):
|
||||
handler = getattr(self, f'on_{frame.type_name()}_frame'.lower())
|
||||
handler(frame)
|
||||
|
||||
def on_sabm_frame(self, frame):
|
||||
def on_sabm_frame(self, _frame):
|
||||
if self.state != Multiplexer.INIT:
|
||||
logger.debug('not in INIT state, ignoring SABM')
|
||||
return
|
||||
self.change_state(Multiplexer.CONNECTED)
|
||||
self.send_frame(RFCOMM_Frame.ua(c_r=1, dlci=0))
|
||||
|
||||
def on_ua_frame(self, frame):
|
||||
def on_ua_frame(self, _frame):
|
||||
if self.state == Multiplexer.CONNECTING:
|
||||
self.change_state(Multiplexer.CONNECTED)
|
||||
if self.connection_result:
|
||||
@@ -626,34 +663,34 @@ class Multiplexer(EventEmitter):
|
||||
self.disconnection_result.set_result(None)
|
||||
self.disconnection_result = None
|
||||
|
||||
def on_dm_frame(self, frame):
|
||||
def on_dm_frame(self, _frame):
|
||||
if self.state == Multiplexer.OPENING:
|
||||
self.change_state(Multiplexer.CONNECTED)
|
||||
if self.open_result:
|
||||
self.open_result.set_exception(
|
||||
ConnectionError(
|
||||
ConnectionError.CONNECTION_REFUSED,
|
||||
core.ConnectionError(
|
||||
core.ConnectionError.CONNECTION_REFUSED,
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
self.l2cap_channel.connection.peer_address,
|
||||
'rfcomm',
|
||||
)
|
||||
)
|
||||
else:
|
||||
logger.warn(f'unexpected state for DM: {self}')
|
||||
logger.warning(f'unexpected state for DM: {self}')
|
||||
|
||||
def on_disc_frame(self, frame):
|
||||
def on_disc_frame(self, _frame):
|
||||
self.change_state(Multiplexer.DISCONNECTED)
|
||||
self.send_frame(
|
||||
RFCOMM_Frame.ua(c_r=0 if self.role == Multiplexer.INITIATOR else 1, dlci=0)
|
||||
)
|
||||
|
||||
def on_uih_frame(self, frame):
|
||||
(type, c_r, value) = RFCOMM_Frame.parse_mcc(frame.information)
|
||||
(mcc_type, c_r, value) = RFCOMM_Frame.parse_mcc(frame.information)
|
||||
|
||||
if type == RFCOMM_MCC_PN_TYPE:
|
||||
if mcc_type == RFCOMM_MCC_PN_TYPE:
|
||||
pn = RFCOMM_MCC_PN.from_bytes(value)
|
||||
self.on_mcc_pn(c_r, pn)
|
||||
elif type == RFCOMM_MCC_MSC_TYPE:
|
||||
elif mcc_type == RFCOMM_MCC_MSC_TYPE:
|
||||
mcs = RFCOMM_MCC_MSC.from_bytes(value)
|
||||
self.on_mcc_msc(c_r, mcs)
|
||||
|
||||
@@ -669,7 +706,7 @@ class Multiplexer(EventEmitter):
|
||||
if pn.dlci & 1:
|
||||
# Not expected, this is an initiator-side number
|
||||
# TODO: error out
|
||||
logger.warn(f'invalid DLCI: {pn.dlci}')
|
||||
logger.warning(f'invalid DLCI: {pn.dlci}')
|
||||
else:
|
||||
if self.acceptor:
|
||||
channel_number = pn.dlci >> 1
|
||||
@@ -688,7 +725,7 @@ class Multiplexer(EventEmitter):
|
||||
self.send_frame(RFCOMM_Frame.dm(c_r=1, dlci=pn.dlci))
|
||||
else:
|
||||
# No acceptor?? shouldn't happen
|
||||
logger.warn(color('!!! no acceptor registered', 'red'))
|
||||
logger.warning(color('!!! no acceptor registered', 'red'))
|
||||
else:
|
||||
# Response
|
||||
logger.debug(f'>>> PN Response: {pn}')
|
||||
@@ -697,12 +734,12 @@ class Multiplexer(EventEmitter):
|
||||
self.dlcs[pn.dlci] = dlc
|
||||
dlc.connect()
|
||||
else:
|
||||
logger.warn('ignoring PN response')
|
||||
logger.warning('ignoring PN response')
|
||||
|
||||
def on_mcc_msc(self, c_r, msc):
|
||||
dlc = self.dlcs.get(msc.dlci)
|
||||
if dlc is None:
|
||||
logger.warn(f'no dlc for DLCI {msc.dlci}')
|
||||
logger.warning(f'no dlc for DLCI {msc.dlci}')
|
||||
return
|
||||
dlc.on_mcc_msc(c_r, msc)
|
||||
|
||||
@@ -732,8 +769,8 @@ class Multiplexer(EventEmitter):
|
||||
if self.state != Multiplexer.CONNECTED:
|
||||
if self.state == Multiplexer.OPENING:
|
||||
raise InvalidStateError('open already in progress')
|
||||
else:
|
||||
raise InvalidStateError('not connected')
|
||||
|
||||
raise InvalidStateError('not connected')
|
||||
|
||||
pn = RFCOMM_MCC_PN(
|
||||
dlci=channel << 1,
|
||||
@@ -744,7 +781,7 @@ class Multiplexer(EventEmitter):
|
||||
max_retransmissions=0,
|
||||
window_size=RFCOMM_DEFAULT_INITIAL_RX_CREDITS,
|
||||
)
|
||||
mcc = RFCOMM_Frame.make_mcc(type=RFCOMM_MCC_PN_TYPE, c_r=1, data=bytes(pn))
|
||||
mcc = RFCOMM_Frame.make_mcc(mcc_type=RFCOMM_MCC_PN_TYPE, c_r=1, data=bytes(pn))
|
||||
logger.debug(f'>>> Sending MCC: {pn}')
|
||||
self.open_result = asyncio.get_running_loop().create_future()
|
||||
self.change_state(Multiplexer.OPENING)
|
||||
@@ -784,7 +821,7 @@ class Client:
|
||||
self.connection, RFCOMM_PSM
|
||||
)
|
||||
except ProtocolError as error:
|
||||
logger.warn(f'L2CAP connection failed: {error}')
|
||||
logger.warning(f'L2CAP connection failed: {error}')
|
||||
raise
|
||||
|
||||
# Create a mutliplexer to manage DLCs with the server
|
||||
|
||||
141
bumble/sdp.py
141
bumble/sdp.py
@@ -34,6 +34,7 @@ logger = logging.getLogger(__name__)
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
# fmt: off
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
SDP_CONTINUATION_WATCHDOG = 64 # Maximum number of continuations we're willing to do
|
||||
|
||||
@@ -115,6 +116,8 @@ SDP_PUBLIC_BROWSE_ROOT = core.UUID.from_16_bits(0x1002, 'PublicBrowseRoot')
|
||||
SDP_ALL_ATTRIBUTES_RANGE = (0x0000FFFF, 4) # Express this as tuple so we can convey the desired encoding size
|
||||
|
||||
# fmt: on
|
||||
# pylint: enable=line-too-long
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -167,12 +170,13 @@ class DataElement:
|
||||
URL: lambda x: DataElement(DataElement.URL, x.decode('utf8')),
|
||||
}
|
||||
|
||||
def __init__(self, type, value, value_size=None):
|
||||
self.type = type
|
||||
def __init__(self, element_type, value, value_size=None):
|
||||
self.type = element_type
|
||||
self.value = value
|
||||
self.value_size = value_size
|
||||
self.bytes = None # Used a cache when parsing from bytes so we can emit a byte-for-byte replica
|
||||
if type == DataElement.UNSIGNED_INTEGER or type == DataElement.SIGNED_INTEGER:
|
||||
# Used as a cache when parsing from bytes so we can emit a byte-for-byte replica
|
||||
self.bytes = None
|
||||
if element_type in (DataElement.UNSIGNED_INTEGER, DataElement.SIGNED_INTEGER):
|
||||
if value_size is None:
|
||||
raise ValueError('integer types must have a value size specified')
|
||||
|
||||
@@ -240,27 +244,33 @@ class DataElement:
|
||||
def unsigned_integer_from_bytes(data):
|
||||
if len(data) == 1:
|
||||
return data[0]
|
||||
elif len(data) == 2:
|
||||
|
||||
if len(data) == 2:
|
||||
return struct.unpack('>H', data)[0]
|
||||
elif len(data) == 4:
|
||||
|
||||
if len(data) == 4:
|
||||
return struct.unpack('>I', data)[0]
|
||||
elif len(data) == 8:
|
||||
|
||||
if len(data) == 8:
|
||||
return struct.unpack('>Q', data)[0]
|
||||
else:
|
||||
raise ValueError(f'invalid integer length {len(data)}')
|
||||
|
||||
raise ValueError(f'invalid integer length {len(data)}')
|
||||
|
||||
@staticmethod
|
||||
def signed_integer_from_bytes(data):
|
||||
if len(data) == 1:
|
||||
return struct.unpack('b', data)[0]
|
||||
elif len(data) == 2:
|
||||
|
||||
if len(data) == 2:
|
||||
return struct.unpack('>h', data)[0]
|
||||
elif len(data) == 4:
|
||||
|
||||
if len(data) == 4:
|
||||
return struct.unpack('>i', data)[0]
|
||||
elif len(data) == 8:
|
||||
|
||||
if len(data) == 8:
|
||||
return struct.unpack('>q', data)[0]
|
||||
else:
|
||||
raise ValueError(f'invalid integer length {len(data)}')
|
||||
|
||||
raise ValueError(f'invalid integer length {len(data)}')
|
||||
|
||||
@staticmethod
|
||||
def list_from_bytes(data):
|
||||
@@ -278,11 +288,11 @@ class DataElement:
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(data):
|
||||
type = data[0] >> 3
|
||||
element_type = data[0] >> 3
|
||||
size_index = data[0] & 7
|
||||
value_offset = 0
|
||||
if size_index == 0:
|
||||
if type == DataElement.NIL:
|
||||
if element_type == DataElement.NIL:
|
||||
value_size = 0
|
||||
else:
|
||||
value_size = 1
|
||||
@@ -305,17 +315,17 @@ class DataElement:
|
||||
value_offset = 4
|
||||
|
||||
value_data = data[1 + value_offset : 1 + value_offset + value_size]
|
||||
constructor = DataElement.type_constructors.get(type)
|
||||
constructor = DataElement.type_constructors.get(element_type)
|
||||
if constructor:
|
||||
if (
|
||||
type == DataElement.UNSIGNED_INTEGER
|
||||
or type == DataElement.SIGNED_INTEGER
|
||||
if element_type in (
|
||||
DataElement.UNSIGNED_INTEGER,
|
||||
DataElement.SIGNED_INTEGER,
|
||||
):
|
||||
result = constructor(value_data, value_size)
|
||||
else:
|
||||
result = constructor(value_data)
|
||||
else:
|
||||
result = DataElement(type, value_data)
|
||||
result = DataElement(element_type, value_data)
|
||||
result.bytes = data[
|
||||
: 1 + value_offset + value_size
|
||||
] # Keep a copy so we can re-serialize to an exact replica
|
||||
@@ -334,7 +344,8 @@ class DataElement:
|
||||
elif self.type == DataElement.UNSIGNED_INTEGER:
|
||||
if self.value < 0:
|
||||
raise ValueError('UNSIGNED_INTEGER cannot be negative')
|
||||
elif self.value_size == 1:
|
||||
|
||||
if self.value_size == 1:
|
||||
data = struct.pack('B', self.value)
|
||||
elif self.value_size == 2:
|
||||
data = struct.pack('>H', self.value)
|
||||
@@ -357,11 +368,11 @@ class DataElement:
|
||||
raise ValueError('invalid value_size')
|
||||
elif self.type == DataElement.UUID:
|
||||
data = bytes(reversed(bytes(self.value)))
|
||||
elif self.type == DataElement.TEXT_STRING or self.type == DataElement.URL:
|
||||
elif self.type in (DataElement.TEXT_STRING, DataElement.URL):
|
||||
data = self.value.encode('utf8')
|
||||
elif self.type == DataElement.BOOLEAN:
|
||||
data = bytes([1 if self.value else 0])
|
||||
elif self.type == DataElement.SEQUENCE or self.type == DataElement.ALTERNATIVE:
|
||||
elif self.type in (DataElement.SEQUENCE, DataElement.ALTERNATIVE):
|
||||
data = b''.join([bytes(element) for element in self.value])
|
||||
else:
|
||||
data = self.value
|
||||
@@ -372,10 +383,10 @@ class DataElement:
|
||||
if size != 0:
|
||||
raise ValueError('NIL must be empty')
|
||||
size_index = 0
|
||||
elif (
|
||||
self.type == DataElement.UNSIGNED_INTEGER
|
||||
or self.type == DataElement.SIGNED_INTEGER
|
||||
or self.type == DataElement.UUID
|
||||
elif self.type in (
|
||||
DataElement.UNSIGNED_INTEGER,
|
||||
DataElement.SIGNED_INTEGER,
|
||||
DataElement.UUID,
|
||||
):
|
||||
if size <= 1:
|
||||
size_index = 0
|
||||
@@ -389,11 +400,11 @@ class DataElement:
|
||||
size_index = 4
|
||||
else:
|
||||
raise ValueError('invalid data size')
|
||||
elif (
|
||||
self.type == DataElement.TEXT_STRING
|
||||
or self.type == DataElement.SEQUENCE
|
||||
or self.type == DataElement.ALTERNATIVE
|
||||
or self.type == DataElement.URL
|
||||
elif self.type in (
|
||||
DataElement.TEXT_STRING,
|
||||
DataElement.SEQUENCE,
|
||||
DataElement.ALTERNATIVE,
|
||||
DataElement.URL,
|
||||
):
|
||||
if size <= 0xFF:
|
||||
size_index = 5
|
||||
@@ -419,14 +430,19 @@ class DataElement:
|
||||
type_name = name_or_number(self.TYPE_NAMES, self.type)
|
||||
if self.type == DataElement.NIL:
|
||||
value_string = ''
|
||||
elif self.type == DataElement.SEQUENCE or self.type == DataElement.ALTERNATIVE:
|
||||
elif self.type in (DataElement.SEQUENCE, DataElement.ALTERNATIVE):
|
||||
container_separator = '\n' if pretty else ''
|
||||
element_separator = '\n' if pretty else ','
|
||||
value_string = f'[{container_separator}{element_separator.join([element.to_string(pretty, indentation + 1 if pretty else 0) for element in self.value])}{container_separator}{prefix}]'
|
||||
elif (
|
||||
self.type == DataElement.UNSIGNED_INTEGER
|
||||
or self.type == DataElement.SIGNED_INTEGER
|
||||
):
|
||||
elements = [
|
||||
element.to_string(pretty, indentation + 1 if pretty else 0)
|
||||
for element in self.value
|
||||
]
|
||||
value_string = (
|
||||
f'[{container_separator}'
|
||||
f'{element_separator.join(elements)}'
|
||||
f'{container_separator}{prefix}]'
|
||||
)
|
||||
elif self.type in (DataElement.UNSIGNED_INTEGER, DataElement.SIGNED_INTEGER):
|
||||
value_string = f'{self.value}#{self.value_size}'
|
||||
elif isinstance(self.value, DataElement):
|
||||
value_string = self.value.to_string(pretty, indentation)
|
||||
@@ -440,8 +456,8 @@ class DataElement:
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class ServiceAttribute:
|
||||
def __init__(self, id, value):
|
||||
self.id = id
|
||||
def __init__(self, attribute_id, value):
|
||||
self.id = attribute_id
|
||||
self.value = value
|
||||
|
||||
@staticmethod
|
||||
@@ -450,7 +466,7 @@ class ServiceAttribute:
|
||||
for i in range(0, len(elements) // 2):
|
||||
attribute_id, attribute_value = elements[2 * i : 2 * (i + 1)]
|
||||
if attribute_id.type != DataElement.UNSIGNED_INTEGER:
|
||||
logger.warn('attribute ID element is not an integer')
|
||||
logger.warning('attribute ID element is not an integer')
|
||||
continue
|
||||
attribute_list.append(ServiceAttribute(attribute_id.value, attribute_value))
|
||||
|
||||
@@ -468,27 +484,31 @@ class ServiceAttribute:
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def id_name(id):
|
||||
return name_or_number(SDP_ATTRIBUTE_ID_NAMES, id)
|
||||
def id_name(id_code):
|
||||
return name_or_number(SDP_ATTRIBUTE_ID_NAMES, id_code)
|
||||
|
||||
@staticmethod
|
||||
def is_uuid_in_value(uuid, value):
|
||||
# Find if a uuid matches a value, either directly or recursing into sequences
|
||||
if value.type == DataElement.UUID:
|
||||
return value.value == uuid
|
||||
elif value.type == DataElement.SEQUENCE:
|
||||
|
||||
if value.type == DataElement.SEQUENCE:
|
||||
for element in value.value:
|
||||
if ServiceAttribute.is_uuid_in_value(uuid, element):
|
||||
return True
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
def to_string(self, color=False):
|
||||
if color:
|
||||
return f'Attribute(id={colors.color(self.id_name(self.id),"magenta")},value={self.value})'
|
||||
else:
|
||||
return f'Attribute(id={self.id_name(self.id)},value={self.value})'
|
||||
return False
|
||||
|
||||
def to_string(self, with_colors=False):
|
||||
if with_colors:
|
||||
return (
|
||||
f'Attribute(id={colors.color(self.id_name(self.id),"magenta")},'
|
||||
f'value={self.value})'
|
||||
)
|
||||
|
||||
return f'Attribute(id={self.id_name(self.id)},value={self.value})'
|
||||
|
||||
def __str__(self):
|
||||
return self.to_string()
|
||||
@@ -501,10 +521,12 @@ class SDP_PDU:
|
||||
'''
|
||||
|
||||
sdp_pdu_classes = {}
|
||||
name = None
|
||||
pdu_id = 0
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(pdu):
|
||||
pdu_id, transaction_id, parameters_length = struct.unpack_from('>BHH', pdu, 0)
|
||||
pdu_id, transaction_id, _parameters_length = struct.unpack_from('>BHH', pdu, 0)
|
||||
|
||||
cls = SDP_PDU.sdp_pdu_classes.get(pdu_id)
|
||||
if cls is None:
|
||||
@@ -755,7 +777,7 @@ class Client:
|
||||
DataElement.unsigned_integer(
|
||||
attribute_id[0], value_size=attribute_id[1]
|
||||
)
|
||||
if type(attribute_id) is tuple
|
||||
if isinstance(attribute_id, tuple)
|
||||
else DataElement.unsigned_integer_16(attribute_id)
|
||||
for attribute_id in attribute_ids
|
||||
]
|
||||
@@ -787,7 +809,7 @@ class Client:
|
||||
# Parse the result into attribute lists
|
||||
attribute_lists_sequences = DataElement.from_bytes(accumulator)
|
||||
if attribute_lists_sequences.type != DataElement.SEQUENCE:
|
||||
logger.warn('unexpected data type')
|
||||
logger.warning('unexpected data type')
|
||||
return []
|
||||
|
||||
return [
|
||||
@@ -805,7 +827,7 @@ class Client:
|
||||
DataElement.unsigned_integer(
|
||||
attribute_id[0], value_size=attribute_id[1]
|
||||
)
|
||||
if type(attribute_id) is tuple
|
||||
if isinstance(attribute_id, tuple)
|
||||
else DataElement.unsigned_integer_16(attribute_id)
|
||||
for attribute_id in attribute_ids
|
||||
]
|
||||
@@ -837,7 +859,7 @@ class Client:
|
||||
# Parse the result into a list of attributes
|
||||
attribute_list_sequence = DataElement.from_bytes(accumulator)
|
||||
if attribute_list_sequence.type != DataElement.SEQUENCE:
|
||||
logger.warn('unexpected data type')
|
||||
logger.warning('unexpected data type')
|
||||
return []
|
||||
|
||||
return ServiceAttribute.list_from_data_elements(attribute_list_sequence.value)
|
||||
@@ -850,6 +872,7 @@ class Server:
|
||||
def __init__(self, device):
|
||||
self.device = device
|
||||
self.service_records = {} # Service records maps, by record handle
|
||||
self.channel = None
|
||||
self.current_response = None
|
||||
|
||||
def register(self, l2cap_channel_manager):
|
||||
@@ -884,7 +907,7 @@ class Server:
|
||||
try:
|
||||
sdp_pdu = SDP_PDU.from_bytes(pdu)
|
||||
except Exception as error:
|
||||
logger.warn(color(f'failed to parse SDP Request PDU: {error}', 'red'))
|
||||
logger.warning(color(f'failed to parse SDP Request PDU: {error}', 'red'))
|
||||
self.send_response(
|
||||
SDP_ErrorResponse(
|
||||
transaction_id=0, error_code=SDP_INVALID_REQUEST_SYNTAX_ERROR
|
||||
@@ -945,7 +968,7 @@ class Server:
|
||||
if attribute.id >= id_range_start and attribute.id <= id_range_end
|
||||
]
|
||||
|
||||
# Return the maching attributes, sorted by attribute id
|
||||
# Return the matching attributes, sorted by attribute id
|
||||
attributes.sort(key=lambda x: x.id)
|
||||
attribute_list = DataElement.sequence([])
|
||||
for attribute in attributes:
|
||||
|
||||
160
bumble/smp.py
160
bumble/smp.py
@@ -28,8 +28,14 @@ import secrets
|
||||
from pyee import EventEmitter
|
||||
from colors import color
|
||||
|
||||
from .core import *
|
||||
from .hci import *
|
||||
from .hci import Address, HCI_LE_Enable_Encryption_Command, HCI_Object, key_with_value
|
||||
from .core import (
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
BT_CENTRAL_ROLE,
|
||||
BT_LE_TRANSPORT,
|
||||
ProtocolError,
|
||||
name_or_number,
|
||||
)
|
||||
from .keys import PairingKeys
|
||||
from . import crypto
|
||||
|
||||
@@ -44,6 +50,7 @@ logger = logging.getLogger(__name__)
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
# fmt: off
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
SMP_CID = 0x06
|
||||
SMP_BR_CID = 0x07
|
||||
@@ -158,6 +165,8 @@ SMP_CTKD_H7_LEBR_SALT = bytes.fromhex('00000000000000000000000000000000746D7031'
|
||||
SMP_CTKD_H7_BRLE_SALT = bytes.fromhex('00000000000000000000000000000000746D7032')
|
||||
|
||||
# fmt: on
|
||||
# pylint: enable=line-too-long
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -177,6 +186,7 @@ class SMP_Command:
|
||||
|
||||
smp_classes = {}
|
||||
code = 0
|
||||
name = ''
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(pdu):
|
||||
@@ -206,7 +216,10 @@ class SMP_Command:
|
||||
keypress = (value >> 4) & 1
|
||||
ct2 = (value >> 5) & 1
|
||||
|
||||
return f'bonding_flags={bonding_flags}, MITM={mitm}, sc={sc}, keypress={keypress}, ct2={ct2}'
|
||||
return (
|
||||
f'bonding_flags={bonding_flags}, '
|
||||
f'MITM={mitm}, sc={sc}, keypress={keypress}, ct2={ct2}'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def io_capability_name(io_capability):
|
||||
@@ -458,11 +471,11 @@ class AddressResolver:
|
||||
|
||||
def resolve(self, address):
|
||||
address_bytes = bytes(address)
|
||||
hash = address_bytes[0:3]
|
||||
hash_part = address_bytes[0:3]
|
||||
prand = address_bytes[3:6]
|
||||
for (irk, resolved_address) in self.resolving_keys:
|
||||
local_hash = crypto.ah(irk, prand)
|
||||
if local_hash == hash:
|
||||
if local_hash == hash_part:
|
||||
# Match!
|
||||
if resolved_address.address_type == Address.PUBLIC_DEVICE_ADDRESS:
|
||||
resolved_address_type = Address.PUBLIC_IDENTITY_ADDRESS
|
||||
@@ -472,6 +485,8 @@ class AddressResolver:
|
||||
address=str(resolved_address), address_type=resolved_address_type
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class PairingDelegate:
|
||||
@@ -500,13 +515,13 @@ class PairingDelegate:
|
||||
async def confirm(self):
|
||||
return True
|
||||
|
||||
async def compare_numbers(self, number, digits=6):
|
||||
async def compare_numbers(self, _number, _digits=6):
|
||||
return True
|
||||
|
||||
async def get_number(self):
|
||||
return 0
|
||||
|
||||
async def display_number(self, number, digits=6):
|
||||
async def display_number(self, _number, _digits=6):
|
||||
pass
|
||||
|
||||
async def key_distribution_response(
|
||||
@@ -528,7 +543,11 @@ class PairingConfig:
|
||||
|
||||
def __str__(self):
|
||||
io_capability_str = SMP_Command.io_capability_name(self.delegate.io_capability)
|
||||
return f'PairingConfig(sc={self.sc}, mitm={self.mitm}, bonding={self.bonding}, delegate[{io_capability_str}])'
|
||||
return (
|
||||
f'PairingConfig(sc={self.sc}, '
|
||||
f'mitm={self.mitm}, bonding={self.bonding}, '
|
||||
f'delegate[{io_capability_str}])'
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -548,14 +567,16 @@ class Session:
|
||||
|
||||
# I/O Capability to pairing method decision matrix
|
||||
#
|
||||
# See Bluetooth spec @ Vol 3, part H - Table 2.8: Mapping of IO Capabilities to Key Generation Method
|
||||
# See Bluetooth spec @ Vol 3, part H - Table 2.8: Mapping of IO Capabilities to Key
|
||||
# Generation Method
|
||||
#
|
||||
# Map: initiator -> responder -> <method>
|
||||
# where <method> may be a simple entry or a 2-element tuple, with the first element for legacy
|
||||
# pairing and the second for secure connections, when the two are different.
|
||||
# Each entry is either a method name, or, for PASSKEY, a tuple:
|
||||
# where <method> may be a simple entry or a 2-element tuple, with the first element
|
||||
# for legacy pairing and the second for secure connections, when the two are
|
||||
# different. Each entry is either a method name, or, for PASSKEY, a tuple:
|
||||
# (method, initiator_displays, responder_displays)
|
||||
# to specify if the initiator and responder should display (True) or input a code (False).
|
||||
# to specify if the initiator and responder should display (True) or input a code
|
||||
# (False).
|
||||
PAIRING_METHODS = {
|
||||
SMP_DISPLAY_ONLY_IO_CAPABILITY: {
|
||||
SMP_DISPLAY_ONLY_IO_CAPABILITY: JUST_WORKS,
|
||||
@@ -606,6 +627,10 @@ class Session:
|
||||
def __init__(self, manager, connection, pairing_config):
|
||||
self.manager = manager
|
||||
self.connection = connection
|
||||
self.preq = None
|
||||
self.pres = None
|
||||
self.ea = None
|
||||
self.eb = None
|
||||
self.tk = bytes(16)
|
||||
self.r = bytes(16)
|
||||
self.stk = None
|
||||
@@ -626,6 +651,7 @@ class Session:
|
||||
self.peer_signature_key = None
|
||||
self.peer_expected_distributions = []
|
||||
self.dh_key = None
|
||||
self.confirm_value = None
|
||||
self.passkey = 0
|
||||
self.passkey_step = 0
|
||||
self.passkey_display = False
|
||||
@@ -726,6 +752,8 @@ class Session:
|
||||
else:
|
||||
return self.ltk
|
||||
|
||||
return None
|
||||
|
||||
def decide_pairing_method(
|
||||
self, auth_req, initiator_io_capability, responder_io_capability
|
||||
):
|
||||
@@ -734,10 +762,10 @@ class Session:
|
||||
return
|
||||
|
||||
details = self.PAIRING_METHODS[initiator_io_capability][responder_io_capability]
|
||||
if type(details) is tuple and len(details) == 2:
|
||||
if isinstance(details, tuple) and len(details) == 2:
|
||||
# One entry for legacy pairing and one for secure connections
|
||||
details = details[1 if self.sc else 0]
|
||||
if type(details) is int:
|
||||
if isinstance(details, int):
|
||||
# Just a method ID
|
||||
self.pairing_method = details
|
||||
else:
|
||||
@@ -762,7 +790,7 @@ class Session:
|
||||
next_steps()
|
||||
return
|
||||
except Exception as error:
|
||||
logger.warn(f'exception while confirm: {error}')
|
||||
logger.warning(f'exception while confirm: {error}')
|
||||
|
||||
self.send_pairing_failed(SMP_CONFIRM_VALUE_FAILED_ERROR)
|
||||
|
||||
@@ -779,7 +807,7 @@ class Session:
|
||||
next_steps()
|
||||
return
|
||||
except Exception as error:
|
||||
logger.warn(f'exception while prompting: {error}')
|
||||
logger.warning(f'exception while prompting: {error}')
|
||||
|
||||
self.send_pairing_failed(SMP_CONFIRM_VALUE_FAILED_ERROR)
|
||||
|
||||
@@ -793,7 +821,7 @@ class Session:
|
||||
logger.debug(f'user input: {passkey}')
|
||||
next_steps(passkey)
|
||||
except Exception as error:
|
||||
logger.warn(f'exception while prompting: {error}')
|
||||
logger.warning(f'exception while prompting: {error}')
|
||||
self.send_pairing_failed(SMP_PASSKEY_ENTRY_FAILED_ERROR)
|
||||
|
||||
self.connection.abort_on('disconnection', prompt())
|
||||
@@ -808,8 +836,9 @@ class Session:
|
||||
self.tk = self.passkey.to_bytes(16, byteorder='little')
|
||||
logger.debug(f'TK from passkey = {self.tk.hex()}')
|
||||
|
||||
self.connection.abort_on('disconnection',
|
||||
self.pairing_config.delegate.display_number(self.passkey, digits=6)
|
||||
self.connection.abort_on(
|
||||
'disconnection',
|
||||
self.pairing_config.delegate.display_number(self.passkey, digits=6),
|
||||
)
|
||||
|
||||
def input_passkey(self, next_steps=None):
|
||||
@@ -872,10 +901,7 @@ class Session:
|
||||
logger.debug(f'generated random: {self.r.hex()}')
|
||||
|
||||
if self.sc:
|
||||
if (
|
||||
self.pairing_method == self.JUST_WORKS
|
||||
or self.pairing_method == self.NUMERIC_COMPARISON
|
||||
):
|
||||
if self.pairing_method in (self.JUST_WORKS, self.NUMERIC_COMPARISON):
|
||||
z = 0
|
||||
elif self.pairing_method == self.PASSKEY:
|
||||
z = 0x80 + ((self.passkey >> self.passkey_step) & 1)
|
||||
@@ -926,7 +952,7 @@ class Session:
|
||||
connection_handle=self.connection.handle,
|
||||
random_number=bytes(8),
|
||||
encrypted_diversifier=0,
|
||||
long_term_key=key
|
||||
long_term_key=key,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -948,7 +974,9 @@ class Session:
|
||||
self.connection.transport == BT_BR_EDR_TRANSPORT
|
||||
and self.initiator_key_distribution & SMP_ENC_KEY_DISTRIBUTION_FLAG
|
||||
):
|
||||
self.ctkd_task = self.connection.abort_on('disconnection', self.derive_ltk())
|
||||
self.ctkd_task = self.connection.abort_on(
|
||||
'disconnection', self.derive_ltk()
|
||||
)
|
||||
elif not self.sc:
|
||||
# Distribute the LTK, EDIV and RAND
|
||||
if self.initiator_key_distribution & SMP_ENC_KEY_DISTRIBUTION_FLAG:
|
||||
@@ -995,7 +1023,9 @@ class Session:
|
||||
self.connection.transport == BT_BR_EDR_TRANSPORT
|
||||
and self.responder_key_distribution & SMP_ENC_KEY_DISTRIBUTION_FLAG
|
||||
):
|
||||
self.ctkd_task = self.connection.abort_on('disconnection', self.derive_ltk())
|
||||
self.ctkd_task = self.connection.abort_on(
|
||||
'disconnection', self.derive_ltk()
|
||||
)
|
||||
# Distribute the LTK, EDIV and RAND
|
||||
elif not self.sc:
|
||||
if self.responder_key_distribution & SMP_ENC_KEY_DISTRIBUTION_FLAG:
|
||||
@@ -1055,13 +1085,14 @@ class Session:
|
||||
if key_distribution_flags & SMP_SIGN_KEY_DISTRIBUTION_FLAG != 0:
|
||||
self.peer_expected_distributions.append(SMP_Signing_Information_Command)
|
||||
logger.debug(
|
||||
f'expecting distributions: {[c.__name__ for c in self.peer_expected_distributions]}'
|
||||
'expecting distributions: '
|
||||
f'{[c.__name__ for c in self.peer_expected_distributions]}'
|
||||
)
|
||||
|
||||
def check_key_distribution(self, command_class):
|
||||
# First, check that the connection is encrypted
|
||||
if not self.connection.is_encrypted:
|
||||
logger.warn(
|
||||
logger.warning(
|
||||
color('received key distribution on a non-encrypted connection', 'red')
|
||||
)
|
||||
self.send_pairing_failed(SMP_UNSPECIFIED_REASON_ERROR)
|
||||
@@ -1071,14 +1102,16 @@ class Session:
|
||||
if command_class in self.peer_expected_distributions:
|
||||
self.peer_expected_distributions.remove(command_class)
|
||||
logger.debug(
|
||||
f'remaining distributions: {[c.__name__ for c in self.peer_expected_distributions]}'
|
||||
'remaining distributions: '
|
||||
f'{[c.__name__ for c in self.peer_expected_distributions]}'
|
||||
)
|
||||
if not self.peer_expected_distributions:
|
||||
self.on_peer_key_distribution_complete()
|
||||
else:
|
||||
logger.warn(
|
||||
logger.warning(
|
||||
color(
|
||||
f'!!! unexpected key distribution command: {command_class.__name__}',
|
||||
'!!! unexpected key distribution command: '
|
||||
f'{command_class.__name__}',
|
||||
'red',
|
||||
)
|
||||
)
|
||||
@@ -1094,7 +1127,7 @@ class Session:
|
||||
# Wait for the pairing process to finish
|
||||
await self.connection.abort_on('disconnection', self.pairing_result)
|
||||
|
||||
def on_disconnection(self, reason):
|
||||
def on_disconnection(self, _):
|
||||
self.connection.remove_listener('disconnection', self.on_disconnection)
|
||||
self.connection.remove_listener(
|
||||
'connection_encryption_change', self.on_connection_encryption_change
|
||||
@@ -1131,8 +1164,8 @@ class Session:
|
||||
|
||||
if self.completed:
|
||||
return
|
||||
else:
|
||||
self.completed = True
|
||||
|
||||
self.completed = True
|
||||
|
||||
if self.pairing_result is not None and not self.pairing_result.done():
|
||||
self.pairing_result.set_result(None)
|
||||
@@ -1192,8 +1225,8 @@ class Session:
|
||||
|
||||
if self.completed:
|
||||
return
|
||||
else:
|
||||
self.completed = True
|
||||
|
||||
self.completed = True
|
||||
|
||||
error = ProtocolError(reason, 'smp', error_name(reason))
|
||||
if self.pairing_result is not None and not self.pairing_result.done():
|
||||
@@ -1217,7 +1250,9 @@ class Session:
|
||||
logger.error(color('SMP command not handled???', 'red'))
|
||||
|
||||
def on_smp_pairing_request_command(self, command):
|
||||
self.connection.abort_on('disconnection', self.on_smp_pairing_request_command_async(command))
|
||||
self.connection.abort_on(
|
||||
'disconnection', self.on_smp_pairing_request_command_async(command)
|
||||
)
|
||||
|
||||
async def on_smp_pairing_request_command_async(self, command):
|
||||
# Check if the request should proceed
|
||||
@@ -1237,7 +1272,7 @@ class Session:
|
||||
|
||||
# Check for OOB
|
||||
if command.oob_data_flag != 0:
|
||||
self.terminate(SMP_OOB_NOT_AVAILABLE_ERROR)
|
||||
self.send_pairing_failed(SMP_OOB_NOT_AVAILABLE_ERROR)
|
||||
return
|
||||
|
||||
# Decide which pairing method to use
|
||||
@@ -1281,7 +1316,7 @@ class Session:
|
||||
|
||||
def on_smp_pairing_response_command(self, command):
|
||||
if self.is_responder:
|
||||
logger.warn(color('received pairing response as a responder', 'red'))
|
||||
logger.warning(color('received pairing response as a responder', 'red'))
|
||||
return
|
||||
|
||||
# Save the response
|
||||
@@ -1330,7 +1365,7 @@ class Session:
|
||||
else:
|
||||
self.send_pairing_confirm_command()
|
||||
|
||||
def on_smp_pairing_confirm_command_legacy(self, command):
|
||||
def on_smp_pairing_confirm_command_legacy(self, _):
|
||||
if self.is_initiator:
|
||||
self.send_pairing_random_command()
|
||||
else:
|
||||
@@ -1340,11 +1375,8 @@ class Session:
|
||||
else:
|
||||
self.send_pairing_confirm_command()
|
||||
|
||||
def on_smp_pairing_confirm_command_secure_connections(self, command):
|
||||
if (
|
||||
self.pairing_method == self.JUST_WORKS
|
||||
or self.pairing_method == self.NUMERIC_COMPARISON
|
||||
):
|
||||
def on_smp_pairing_confirm_command_secure_connections(self, _):
|
||||
if self.pairing_method in (self.JUST_WORKS, self.NUMERIC_COMPARISON):
|
||||
if self.is_initiator:
|
||||
self.r = crypto.r()
|
||||
self.send_pairing_random_command()
|
||||
@@ -1397,11 +1429,9 @@ class Session:
|
||||
self.send_pairing_random_command()
|
||||
|
||||
def on_smp_pairing_random_command_secure_connections(self, command):
|
||||
# pylint: disable=too-many-return-statements
|
||||
if self.is_initiator:
|
||||
if (
|
||||
self.pairing_method == self.JUST_WORKS
|
||||
or self.pairing_method == self.NUMERIC_COMPARISON
|
||||
):
|
||||
if self.pairing_method in (self.JUST_WORKS, self.NUMERIC_COMPARISON):
|
||||
# Check that the random value matches what was committed to earlier
|
||||
confirm_verifier = crypto.f4(
|
||||
self.pkb, self.pka, command.random_value, bytes([0])
|
||||
@@ -1432,10 +1462,7 @@ class Session:
|
||||
else:
|
||||
return
|
||||
else:
|
||||
if (
|
||||
self.pairing_method == self.JUST_WORKS
|
||||
or self.pairing_method == self.NUMERIC_COMPARISON
|
||||
):
|
||||
if self.pairing_method in (self.JUST_WORKS, self.NUMERIC_COMPARISON):
|
||||
self.send_pairing_random_command()
|
||||
elif self.pairing_method == self.PASSKEY:
|
||||
# Check that the random value matches what was committed to earlier
|
||||
@@ -1467,10 +1494,7 @@ class Session:
|
||||
(mac_key, self.ltk) = crypto.f5(self.dh_key, self.na, self.nb, a, b)
|
||||
|
||||
# Compute the DH Key checks
|
||||
if (
|
||||
self.pairing_method == self.JUST_WORKS
|
||||
or self.pairing_method == self.NUMERIC_COMPARISON
|
||||
):
|
||||
if self.pairing_method in (self.JUST_WORKS, self.NUMERIC_COMPARISON):
|
||||
ra = bytes(16)
|
||||
rb = ra
|
||||
elif self.pairing_method == self.PASSKEY:
|
||||
@@ -1495,10 +1519,7 @@ class Session:
|
||||
self.wait_before_continuing.set_result(None)
|
||||
|
||||
# Prompt the user for confirmation if needed
|
||||
if (
|
||||
self.pairing_method == self.JUST_WORKS
|
||||
or self.pairing_method == self.NUMERIC_COMPARISON
|
||||
):
|
||||
if self.pairing_method in (self.JUST_WORKS, self.NUMERIC_COMPARISON):
|
||||
# Compute the 6-digit code
|
||||
code = crypto.g2(self.pka, self.pkb, self.na, self.nb) % 1000000
|
||||
|
||||
@@ -1547,10 +1568,7 @@ class Session:
|
||||
else:
|
||||
self.send_public_key_command()
|
||||
|
||||
if (
|
||||
self.pairing_method == self.JUST_WORKS
|
||||
or self.pairing_method == self.NUMERIC_COMPARISON
|
||||
):
|
||||
if self.pairing_method in (self.JUST_WORKS, self.NUMERIC_COMPARISON):
|
||||
# We can now send the confirmation value
|
||||
self.send_pairing_confirm_command()
|
||||
|
||||
@@ -1616,7 +1634,8 @@ class Manager(EventEmitter):
|
||||
|
||||
def send_command(self, connection, command):
|
||||
logger.debug(
|
||||
f'>>> Sending SMP Command on connection [0x{connection.handle:04X}] {connection.peer_address}: {command}'
|
||||
f'>>> Sending SMP Command on connection [0x{connection.handle:04X}] '
|
||||
f'{connection.peer_address}: {command}'
|
||||
)
|
||||
cid = SMP_BR_CID if connection.transport == BT_BR_EDR_TRANSPORT else SMP_CID
|
||||
connection.send_l2cap_pdu(cid, command.to_bytes())
|
||||
@@ -1638,7 +1657,8 @@ class Manager(EventEmitter):
|
||||
# Parse the L2CAP payload into an SMP Command object
|
||||
command = SMP_Command.from_bytes(pdu)
|
||||
logger.debug(
|
||||
f'<<< Received SMP Command on connection [0x{connection.handle:04X}] {connection.peer_address}: {command}'
|
||||
f'<<< Received SMP Command on connection [0x{connection.handle:04X}] '
|
||||
f'{connection.peer_address}: {command}'
|
||||
)
|
||||
|
||||
# Delegate the handling of the command to the session
|
||||
@@ -1684,7 +1704,7 @@ class Manager(EventEmitter):
|
||||
try:
|
||||
await self.device.keystore.update(str(identity_address), keys)
|
||||
except Exception as error:
|
||||
logger.warn(f'!!! error while storing keys: {error}')
|
||||
logger.warning(f'!!! error while storing keys: {error}')
|
||||
|
||||
self.device.abort_on('flush', store_keys())
|
||||
|
||||
@@ -1702,3 +1722,5 @@ class Manager(EventEmitter):
|
||||
def get_long_term_key(self, connection, rand, ediv):
|
||||
if session := self.sessions.get(connection.handle):
|
||||
return session.get_long_term_key(rand, ediv)
|
||||
|
||||
return None
|
||||
|
||||
@@ -35,61 +35,76 @@ async def open_transport(name):
|
||||
Where <parameters> depend on the type (and may be empty for some types).
|
||||
The supported types are: serial,udp,tcp,pty,usb
|
||||
'''
|
||||
# pylint: disable=import-outside-toplevel
|
||||
# pylint: disable=too-many-return-statements
|
||||
|
||||
scheme, *spec = name.split(':', 1)
|
||||
if scheme == 'serial' and spec:
|
||||
from .serial import open_serial_transport
|
||||
|
||||
return await open_serial_transport(spec[0])
|
||||
elif scheme == 'udp' and spec:
|
||||
|
||||
if scheme == 'udp' and spec:
|
||||
from .udp import open_udp_transport
|
||||
|
||||
return await open_udp_transport(spec[0])
|
||||
elif scheme == 'tcp-client' and spec:
|
||||
|
||||
if scheme == 'tcp-client' and spec:
|
||||
from .tcp_client import open_tcp_client_transport
|
||||
|
||||
return await open_tcp_client_transport(spec[0])
|
||||
elif scheme == 'tcp-server' and spec:
|
||||
|
||||
if scheme == 'tcp-server' and spec:
|
||||
from .tcp_server import open_tcp_server_transport
|
||||
|
||||
return await open_tcp_server_transport(spec[0])
|
||||
elif scheme == 'ws-client' and spec:
|
||||
|
||||
if scheme == 'ws-client' and spec:
|
||||
from .ws_client import open_ws_client_transport
|
||||
|
||||
return await open_ws_client_transport(spec[0])
|
||||
elif scheme == 'ws-server' and spec:
|
||||
|
||||
if scheme == 'ws-server' and spec:
|
||||
from .ws_server import open_ws_server_transport
|
||||
|
||||
return await open_ws_server_transport(spec[0])
|
||||
elif scheme == 'pty':
|
||||
|
||||
if scheme == 'pty':
|
||||
from .pty import open_pty_transport
|
||||
|
||||
return await open_pty_transport(spec[0] if spec else None)
|
||||
elif scheme == 'file':
|
||||
|
||||
if scheme == 'file':
|
||||
from .file import open_file_transport
|
||||
|
||||
return await open_file_transport(spec[0] if spec else None)
|
||||
elif scheme == 'vhci':
|
||||
|
||||
if scheme == 'vhci':
|
||||
from .vhci import open_vhci_transport
|
||||
|
||||
return await open_vhci_transport(spec[0] if spec else None)
|
||||
elif scheme == 'hci-socket':
|
||||
|
||||
if scheme == 'hci-socket':
|
||||
from .hci_socket import open_hci_socket_transport
|
||||
|
||||
return await open_hci_socket_transport(spec[0] if spec else None)
|
||||
elif scheme == 'usb':
|
||||
|
||||
if scheme == 'usb':
|
||||
from .usb import open_usb_transport
|
||||
|
||||
return await open_usb_transport(spec[0] if spec else None)
|
||||
elif scheme == 'pyusb':
|
||||
|
||||
if scheme == 'pyusb':
|
||||
from .pyusb import open_pyusb_transport
|
||||
|
||||
return await open_pyusb_transport(spec[0] if spec else None)
|
||||
elif scheme == 'android-emulator':
|
||||
|
||||
if scheme == 'android-emulator':
|
||||
from .android_emulator import open_android_emulator_transport
|
||||
|
||||
return await open_android_emulator_transport(spec[0] if spec else None)
|
||||
else:
|
||||
raise ValueError('unknown transport scheme')
|
||||
|
||||
raise ValueError('unknown transport scheme')
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -104,5 +119,5 @@ async def open_transport_or_link(name):
|
||||
link.close()
|
||||
|
||||
return LinkTransport(controller, AsyncPipeSink(controller))
|
||||
else:
|
||||
return await open_transport(name)
|
||||
|
||||
return await open_transport(name)
|
||||
|
||||
@@ -65,9 +65,12 @@ class PacketPump:
|
||||
# -----------------------------------------------------------------------------
|
||||
class PacketParser:
|
||||
'''
|
||||
In-line parser that accepts data and emits 'on_packet' when a full packet has been parsed
|
||||
In-line parser that accepts data and emits 'on_packet' when a full packet has been
|
||||
parsed
|
||||
'''
|
||||
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
|
||||
NEED_TYPE = 0
|
||||
NEED_LENGTH = 1
|
||||
NEED_BODY = 2
|
||||
@@ -278,7 +281,7 @@ class PumpedPacketSource(ParserSource):
|
||||
logger.debug('source pump task done')
|
||||
break
|
||||
except Exception as error:
|
||||
logger.warn(f'exception while waiting for packet: {error}')
|
||||
logger.warning(f'exception while waiting for packet: {error}')
|
||||
self.terminated.set_result(error)
|
||||
break
|
||||
|
||||
@@ -309,7 +312,7 @@ class PumpedPacketSink:
|
||||
logger.debug('sink pump task done')
|
||||
break
|
||||
except Exception as error:
|
||||
logger.warn(f'exception while sending packet: {error}')
|
||||
logger.warning(f'exception while sending packet: {error}')
|
||||
break
|
||||
|
||||
self.pump_task = asyncio.create_task(pump_packets())
|
||||
|
||||
@@ -30,8 +30,9 @@ logger = logging.getLogger(__name__)
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_file_transport(spec):
|
||||
'''
|
||||
Open a File transport (typically not for a real file, but for a PTY or other unix virtual files).
|
||||
The parameter string is the path of the file to open
|
||||
Open a File transport (typically not for a real file, but for a PTY or other unix
|
||||
virtual files).
|
||||
The parameter string is the path of the file to open.
|
||||
'''
|
||||
|
||||
# Open the file
|
||||
@@ -39,12 +40,12 @@ async def open_file_transport(spec):
|
||||
|
||||
# Setup reading
|
||||
read_transport, packet_source = await asyncio.get_running_loop().connect_read_pipe(
|
||||
lambda: StreamPacketSource(), file
|
||||
StreamPacketSource, file
|
||||
)
|
||||
|
||||
# Setup writing
|
||||
write_transport, _ = await asyncio.get_running_loop().connect_write_pipe(
|
||||
lambda: asyncio.BaseProtocol(), file
|
||||
asyncio.BaseProtocol, file
|
||||
)
|
||||
packet_sink = StreamPacketSink(write_transport)
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ async def open_hci_socket_transport(spec):
|
||||
or a 0-based integer to indicate the adapter number.
|
||||
'''
|
||||
|
||||
HCI_CHANNEL_USER = 1
|
||||
HCI_CHANNEL_USER = 1 # pylint: disable=invalid-name
|
||||
|
||||
# Create a raw HCI socket
|
||||
try:
|
||||
@@ -49,10 +49,12 @@ async def open_hci_socket_transport(spec):
|
||||
socket.SOCK_RAW | socket.SOCK_NONBLOCK,
|
||||
socket.BTPROTO_HCI,
|
||||
)
|
||||
except AttributeError:
|
||||
except AttributeError as error:
|
||||
# Not supported on this platform
|
||||
logger.info("HCI sockets not supported on this platform")
|
||||
raise Exception('Bluetooth HCI sockets not supported on this platform')
|
||||
raise Exception(
|
||||
'Bluetooth HCI sockets not supported on this platform'
|
||||
) from error
|
||||
|
||||
# Compute the adapter index
|
||||
if spec is None:
|
||||
@@ -66,13 +68,19 @@ async def open_hci_socket_transport(spec):
|
||||
try:
|
||||
ctypes.cdll.LoadLibrary('libc.so.6')
|
||||
libc = ctypes.CDLL('libc.so.6', use_errno=True)
|
||||
except OSError:
|
||||
except OSError as error:
|
||||
logger.info("HCI sockets not supported on this platform")
|
||||
raise Exception('Bluetooth HCI sockets not supported on this platform')
|
||||
raise Exception(
|
||||
'Bluetooth HCI sockets not supported on this platform'
|
||||
) from error
|
||||
libc.bind.argtypes = (ctypes.c_int, ctypes.POINTER(ctypes.c_char), ctypes.c_int)
|
||||
libc.bind.restype = ctypes.c_int
|
||||
bind_address = struct.pack(
|
||||
'<HHH', socket.AF_BLUETOOTH, adapter_index, HCI_CHANNEL_USER
|
||||
# pylint: disable=no-member
|
||||
'<HHH',
|
||||
socket.AF_BLUETOOTH,
|
||||
adapter_index,
|
||||
HCI_CHANNEL_USER,
|
||||
)
|
||||
if (
|
||||
libc.bind(
|
||||
@@ -85,9 +93,9 @@ async def open_hci_socket_transport(spec):
|
||||
raise IOError(ctypes.get_errno(), os.strerror(ctypes.get_errno()))
|
||||
|
||||
class HciSocketSource(ParserSource):
|
||||
def __init__(self, socket):
|
||||
def __init__(self, hci_socket):
|
||||
super().__init__()
|
||||
self.socket = socket
|
||||
self.socket = hci_socket
|
||||
asyncio.get_running_loop().add_reader(
|
||||
socket.fileno(), self.recv_until_would_block
|
||||
)
|
||||
@@ -107,8 +115,8 @@ async def open_hci_socket_transport(spec):
|
||||
asyncio.get_running_loop().remove_reader(self.socket.fileno())
|
||||
|
||||
class HciSocketSink:
|
||||
def __init__(self, socket):
|
||||
self.socket = socket
|
||||
def __init__(self, hci_socket):
|
||||
self.socket = hci_socket
|
||||
self.packets = collections.deque()
|
||||
self.writer_added = False
|
||||
|
||||
@@ -127,10 +135,13 @@ async def open_hci_socket_transport(spec):
|
||||
break
|
||||
|
||||
if self.packets:
|
||||
# There's still something to send, ensure that we are monitoring the socket
|
||||
# There's still something to send, ensure that we are monitoring the
|
||||
# socket
|
||||
if not self.writer_added:
|
||||
asyncio.get_running_loop().add_writer(
|
||||
socket.fileno(), self.send_until_would_block
|
||||
# pylint: disable=no-member
|
||||
socket.fileno(),
|
||||
self.send_until_would_block,
|
||||
)
|
||||
self.writer_added = True
|
||||
else:
|
||||
@@ -148,9 +159,9 @@ async def open_hci_socket_transport(spec):
|
||||
asyncio.get_running_loop().remove_writer(self.socket.fileno())
|
||||
|
||||
class HciSocketTransport(Transport):
|
||||
def __init__(self, socket, source, sink):
|
||||
def __init__(self, hci_socket, source, sink):
|
||||
super().__init__(source, sink)
|
||||
self.socket = socket
|
||||
self.socket = hci_socket
|
||||
|
||||
async def close(self):
|
||||
logger.debug('closing HCI socket transport')
|
||||
|
||||
@@ -47,11 +47,11 @@ async def open_pty_transport(spec):
|
||||
tty.setraw(replica)
|
||||
|
||||
read_transport, packet_source = await asyncio.get_running_loop().connect_read_pipe(
|
||||
lambda: StreamPacketSource(), io.open(primary, 'rb', closefd=False)
|
||||
StreamPacketSource, io.open(primary, 'rb', closefd=False)
|
||||
)
|
||||
|
||||
write_transport, _ = await asyncio.get_running_loop().connect_write_pipe(
|
||||
lambda: asyncio.BaseProtocol(), io.open(primary, 'wb', closefd=False)
|
||||
asyncio.BaseProtocol, io.open(primary, 'wb', closefd=False)
|
||||
)
|
||||
packet_sink = StreamPacketSink(write_transport)
|
||||
|
||||
|
||||
@@ -17,11 +17,12 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
|
||||
import libusb_package
|
||||
import usb.core
|
||||
import usb.util
|
||||
import threading
|
||||
import time
|
||||
from colors import color
|
||||
|
||||
from .common import Transport, ParserSource
|
||||
@@ -49,6 +50,7 @@ async def open_pyusb_transport(spec):
|
||||
04b4:f901 --> the BT USB dongle with vendor=04b4 and product=f901
|
||||
'''
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
USB_RECIPIENT_DEVICE = 0x00
|
||||
USB_REQUEST_TYPE_CLASS = 0x01 << 5
|
||||
USB_ENDPOINT_EVENTS_IN = 0x81
|
||||
@@ -109,7 +111,7 @@ async def open_pyusb_transport(spec):
|
||||
def run(self):
|
||||
while self.stop_event is None:
|
||||
time.sleep(1)
|
||||
self.loop.call_soon_threadsafe(lambda: self.stop_event.set())
|
||||
self.loop.call_soon_threadsafe(self.stop_event.set)
|
||||
|
||||
class UsbPacketSource(asyncio.Protocol, ParserSource):
|
||||
def __init__(self, device, sco_enabled):
|
||||
@@ -117,6 +119,7 @@ async def open_pyusb_transport(spec):
|
||||
self.device = device
|
||||
self.loop = asyncio.get_running_loop()
|
||||
self.queue = asyncio.Queue()
|
||||
self.dequeue_task = None
|
||||
self.event_thread = threading.Thread(
|
||||
target=self.run, args=(USB_ENDPOINT_EVENTS_IN, hci.HCI_EVENT_PACKET)
|
||||
)
|
||||
@@ -135,8 +138,8 @@ async def open_pyusb_transport(spec):
|
||||
)
|
||||
self.sco_thread.stop_event = None
|
||||
|
||||
def data_received(self, packet):
|
||||
self.parser.feed_data(packet)
|
||||
def data_received(self, data):
|
||||
self.parser.feed_data(data)
|
||||
|
||||
def enqueue(self, packet):
|
||||
self.queue.put_nowait(packet)
|
||||
@@ -180,16 +183,17 @@ async def open_pyusb_transport(spec):
|
||||
except usb.core.USBTimeoutError:
|
||||
continue
|
||||
except usb.core.USBError:
|
||||
# Don't log this: because pyusb doesn't really support multiple threads
|
||||
# reading at the same time, we can get occasional USBError(errno=5)
|
||||
# Input/Output errors reported, but they seem to be harmless.
|
||||
# Don't log this: because pyusb doesn't really support multiple
|
||||
# threads reading at the same time, we can get occasional
|
||||
# USBError(errno=5) Input/Output errors reported, but they seem to
|
||||
# be harmless.
|
||||
# Until support for async or multi-thread support is added to pyusb,
|
||||
# we'll just live with this as is...
|
||||
# logger.warning(f'USB read error: {error}')
|
||||
time.sleep(1) # Sleep one second to avoid busy looping
|
||||
|
||||
stop_event = current_thread.stop_event
|
||||
self.loop.call_soon_threadsafe(lambda: stop_event.set())
|
||||
self.loop.call_soon_threadsafe(stop_event.set)
|
||||
|
||||
class UsbTransport(Transport):
|
||||
def __init__(self, device, source, sink):
|
||||
@@ -243,6 +247,7 @@ async def open_pyusb_transport(spec):
|
||||
|
||||
# Select an alternate setting for SCO, if available
|
||||
sco_enabled = False
|
||||
# pylint: disable=line-too-long
|
||||
# NOTE: this is disabled for now, because SCO with alternate settings is broken,
|
||||
# see: https://github.com/libusb/libusb/issues/36
|
||||
#
|
||||
|
||||
@@ -60,7 +60,7 @@ async def open_serial_transport(spec):
|
||||
device = spec
|
||||
serial_transport, packet_source = await serial_asyncio.create_serial_connection(
|
||||
asyncio.get_running_loop(),
|
||||
lambda: StreamPacketSource(),
|
||||
StreamPacketSource,
|
||||
device,
|
||||
baudrate=speed,
|
||||
rtscts=rtscts,
|
||||
|
||||
@@ -37,13 +37,13 @@ async def open_tcp_client_transport(spec):
|
||||
'''
|
||||
|
||||
class TcpPacketSource(StreamPacketSource):
|
||||
def connection_lost(self, error):
|
||||
logger.debug(f'connection lost: {error}')
|
||||
self.terminated.set_result(error)
|
||||
def connection_lost(self, exc):
|
||||
logger.debug(f'connection lost: {exc}')
|
||||
self.terminated.set_result(exc)
|
||||
|
||||
remote_host, remote_port = spec.split(':')
|
||||
tcp_transport, packet_source = await asyncio.get_running_loop().create_connection(
|
||||
lambda: TcpPacketSource(),
|
||||
TcpPacketSource,
|
||||
host=remote_host,
|
||||
port=int(remote_port),
|
||||
)
|
||||
|
||||
@@ -49,8 +49,8 @@ async def open_tcp_server_transport(spec):
|
||||
|
||||
# Called when a new connection is established
|
||||
def connection_made(self, transport):
|
||||
peername = transport.get_extra_info('peername')
|
||||
logger.debug('connection from {}'.format(peername))
|
||||
peer_name = transport.get_extra_info('peer_name')
|
||||
logger.debug(f'connection from {peer_name}')
|
||||
self.packet_sink.transport = transport
|
||||
|
||||
# Called when the client is disconnected
|
||||
|
||||
@@ -57,7 +57,7 @@ async def open_udp_transport(spec):
|
||||
udp_transport,
|
||||
packet_source,
|
||||
) = await asyncio.get_running_loop().create_datagram_endpoint(
|
||||
lambda: UdpPacketSource(),
|
||||
UdpPacketSource,
|
||||
local_addr=(local_host, int(local_port)),
|
||||
remote_addr=(remote_host, int(remote_port)),
|
||||
)
|
||||
|
||||
@@ -17,12 +17,13 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import logging
|
||||
import libusb_package
|
||||
import usb1
|
||||
import threading
|
||||
import collections
|
||||
import ctypes
|
||||
import platform
|
||||
|
||||
import libusb_package
|
||||
import usb1
|
||||
from colors import color
|
||||
|
||||
from .common import Transport, ParserSource
|
||||
@@ -39,9 +40,9 @@ logger = logging.getLogger(__name__)
|
||||
def load_libusb():
|
||||
'''
|
||||
Attempt to load the libusb-1.0 C library from libusb_package in site-packages.
|
||||
If library exists, we create a DLL object and initialize the usb1 backend.
|
||||
This only needs to be done once, but bufore a usb1.USBContext is created.
|
||||
If library does not exists, do nothing and usb1 will search default system paths
|
||||
If the library exists, we create a DLL object and initialize the usb1 backend.
|
||||
This only needs to be done once, but before a usb1.USBContext is created.
|
||||
If the library does not exists, do nothing and usb1 will search default system paths
|
||||
when usb1.USBContext is created.
|
||||
'''
|
||||
if libusb_path := libusb_package.get_library_path():
|
||||
@@ -49,6 +50,7 @@ def load_libusb():
|
||||
libusb_dll = dll_loader(libusb_path, use_errno=True, use_last_error=True)
|
||||
usb1.loadLibrary(libusb_dll)
|
||||
|
||||
|
||||
async def open_usb_transport(spec):
|
||||
'''
|
||||
Open a USB transport.
|
||||
@@ -60,21 +62,26 @@ async def open_usb_transport(spec):
|
||||
With <index> as the 0-based index to select amongst all the devices that appear
|
||||
to be supporting Bluetooth HCI (0 being the first one), or
|
||||
Where <vendor> and <product> are the vendor ID and product ID in hexadecimal. The
|
||||
/<serial-number> suffix or #<index> suffix max be specified when more than one device with
|
||||
the same vendor and product identifiers are present.
|
||||
/<serial-number> suffix or #<index> suffix max be specified when more than one
|
||||
device with the same vendor and product identifiers are present.
|
||||
|
||||
In addition, if the moniker ends with the symbol "!", the device will be used in "forced" mode:
|
||||
the first USB interface of the device will be used, regardless of the interface class/subclass.
|
||||
This may be useful for some devices that use a custom class/subclass but may nonetheless work as-is.
|
||||
In addition, if the moniker ends with the symbol "!", the device will be used in
|
||||
"forced" mode:
|
||||
the first USB interface of the device will be used, regardless of the interface
|
||||
class/subclass.
|
||||
This may be useful for some devices that use a custom class/subclass but may
|
||||
nonetheless work as-is.
|
||||
|
||||
Examples:
|
||||
0 --> the first BT USB dongle
|
||||
04b4:f901 --> the BT USB dongle with vendor=04b4 and product=f901
|
||||
04b4:f901#2 --> the third USB device with vendor=04b4 and product=f901
|
||||
04b4:f901/00E04C239987 --> the BT USB dongle with vendor=04b4 and product=f901 and serial number 00E04C239987
|
||||
04b4:f901/00E04C239987 --> the BT USB dongle with vendor=04b4 and product=f901 and
|
||||
serial number 00E04C239987
|
||||
usb:0B05:17CB! --> the BT USB dongle vendor=0B05 and product=17CB, in "forced" mode.
|
||||
'''
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
USB_RECIPIENT_DEVICE = 0x00
|
||||
USB_REQUEST_TYPE_CLASS = 0x01 << 5
|
||||
USB_DEVICE_CLASS_DEVICE = 0x00
|
||||
@@ -125,6 +132,7 @@ async def open_usb_transport(spec):
|
||||
status = transfer.getStatus()
|
||||
# logger.debug(f'<<< USB out transfer callback: status={status}')
|
||||
|
||||
# pylint: disable=no-member
|
||||
if status == usb1.TRANSFER_COMPLETED:
|
||||
self.loop.call_soon_threadsafe(self.on_packet_sent_)
|
||||
elif status == usb1.TRANSFER_CANCELLED:
|
||||
@@ -165,15 +173,20 @@ async def open_usb_transport(spec):
|
||||
else:
|
||||
logger.warning(color(f'unsupported packet type {packet_type}', 'red'))
|
||||
|
||||
async def close(self):
|
||||
def close(self):
|
||||
self.closed = True
|
||||
|
||||
async def terminate(self):
|
||||
if not self.closed:
|
||||
self.close()
|
||||
|
||||
# Empty the packet queue so that we don't send any more data
|
||||
self.packets.clear()
|
||||
|
||||
# If we have a transfer in flight, cancel it
|
||||
if self.transfer.isSubmitted():
|
||||
# Try to cancel the transfer, but that may fail because it may have already completed
|
||||
# Try to cancel the transfer, but that may fail because it may have
|
||||
# already completed
|
||||
try:
|
||||
self.transfer.cancel()
|
||||
|
||||
@@ -192,12 +205,15 @@ async def open_usb_transport(spec):
|
||||
self.events_in = events_in
|
||||
self.loop = asyncio.get_running_loop()
|
||||
self.queue = asyncio.Queue()
|
||||
self.dequeue_task = None
|
||||
self.closed = False
|
||||
self.event_loop_done = self.loop.create_future()
|
||||
self.cancel_done = {
|
||||
hci.HCI_EVENT_PACKET: self.loop.create_future(),
|
||||
hci.HCI_ACL_DATA_PACKET: self.loop.create_future(),
|
||||
}
|
||||
self.events_in_transfer = None
|
||||
self.acl_in_transfer = None
|
||||
|
||||
# Create a thread to process events
|
||||
self.event_thread = threading.Thread(target=self.run)
|
||||
@@ -228,8 +244,13 @@ async def open_usb_transport(spec):
|
||||
def on_packet_received(self, transfer):
|
||||
packet_type = transfer.getUserData()
|
||||
status = transfer.getStatus()
|
||||
# logger.debug(f'<<< USB IN transfer callback: status={status} packet_type={packet_type} length={transfer.getActualLength()}')
|
||||
# logger.debug(
|
||||
# f'<<< USB IN transfer callback: status={status} '
|
||||
# f'packet_type={packet_type} '
|
||||
# f'length={transfer.getActualLength()}'
|
||||
# )
|
||||
|
||||
# pylint: disable=no-member
|
||||
if status == usb1.TRANSFER_COMPLETED:
|
||||
packet = (
|
||||
bytes([packet_type])
|
||||
@@ -263,6 +284,7 @@ async def open_usb_transport(spec):
|
||||
self.events_in_transfer.isSubmitted()
|
||||
or self.acl_in_transfer.isSubmitted()
|
||||
):
|
||||
# pylint: disable=no-member
|
||||
try:
|
||||
self.context.handleEvents()
|
||||
except usb1.USBErrorInterrupted:
|
||||
@@ -271,19 +293,26 @@ async def open_usb_transport(spec):
|
||||
logger.debug('USB event loop done')
|
||||
self.loop.call_soon_threadsafe(self.event_loop_done.set_result, None)
|
||||
|
||||
async def close(self):
|
||||
def close(self):
|
||||
self.closed = True
|
||||
|
||||
async def terminate(self):
|
||||
if not self.closed:
|
||||
self.close()
|
||||
|
||||
self.dequeue_task.cancel()
|
||||
|
||||
# Cancel the transfers
|
||||
for transfer in (self.events_in_transfer, self.acl_in_transfer):
|
||||
if transfer.isSubmitted():
|
||||
# Try to cancel the transfer, but that may fail because it may have already completed
|
||||
# Try to cancel the transfer, but that may fail because it may have
|
||||
# already completed
|
||||
packet_type = transfer.getUserData()
|
||||
try:
|
||||
transfer.cancel()
|
||||
logger.debug(
|
||||
f'waiting for IN[{packet_type}] transfer cancellation to be done...'
|
||||
f'waiting for IN[{packet_type}] transfer cancellation '
|
||||
'to be done...'
|
||||
)
|
||||
await self.cancel_done[packet_type]
|
||||
logger.debug(f'IN[{packet_type}] transfer cancellation done')
|
||||
@@ -314,8 +343,10 @@ async def open_usb_transport(spec):
|
||||
sink.start()
|
||||
|
||||
async def close(self):
|
||||
await self.source.close()
|
||||
await self.sink.close()
|
||||
self.source.close()
|
||||
self.sink.close()
|
||||
await self.source.terminate()
|
||||
await self.sink.terminate()
|
||||
self.device.releaseInterface(self.interface)
|
||||
self.device.close()
|
||||
self.context.close()
|
||||
@@ -400,6 +431,7 @@ async def open_usb_transport(spec):
|
||||
|
||||
# Look for the first interface with the right class and endpoints
|
||||
def find_endpoints(device):
|
||||
# pylint: disable-next=too-many-nested-blocks
|
||||
for (configuration_index, configuration) in enumerate(device):
|
||||
interface = None
|
||||
for interface in configuration:
|
||||
@@ -448,10 +480,13 @@ async def open_usb_transport(spec):
|
||||
acl_out,
|
||||
events_in,
|
||||
)
|
||||
else:
|
||||
logger.debug(
|
||||
f'skipping configuration {configuration_index + 1} / interface {setting.getNumber()}'
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f'skipping configuration {configuration_index + 1} / '
|
||||
f'interface {setting.getNumber()}'
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
endpoints = find_endpoints(found)
|
||||
if endpoints is None:
|
||||
@@ -469,6 +504,7 @@ async def open_usb_transport(spec):
|
||||
device = found.open()
|
||||
|
||||
# Auto-detach the kernel driver if supported
|
||||
# pylint: disable=no-member
|
||||
if usb1.hasCapability(usb1.CAP_SUPPORTS_DETACH_KERNEL_DRIVER):
|
||||
try:
|
||||
logger.debug('auto-detaching kernel driver')
|
||||
|
||||
@@ -44,11 +44,13 @@ async def open_ws_server_transport(spec):
|
||||
source = ParserSource()
|
||||
sink = PumpedPacketSink(self.send_packet)
|
||||
self.connection = asyncio.get_running_loop().create_future()
|
||||
self.server = None
|
||||
|
||||
super().__init__(source, sink)
|
||||
|
||||
async def serve(self, local_host, local_port):
|
||||
self.sink.start()
|
||||
# pylint: disable-next=no-member
|
||||
self.server = await websockets.serve(
|
||||
ws_handler=self.on_connection,
|
||||
host=local_host if local_host != '_' else None,
|
||||
@@ -58,15 +60,17 @@ async def open_ws_server_transport(spec):
|
||||
|
||||
async def on_connection(self, connection):
|
||||
logger.debug(
|
||||
f'new connection on {connection.local_address} from {connection.remote_address}'
|
||||
f'new connection on {connection.local_address} '
|
||||
f'from {connection.remote_address}'
|
||||
)
|
||||
self.connection.set_result(connection)
|
||||
# pylint: disable=no-member
|
||||
try:
|
||||
async for packet in connection:
|
||||
if type(packet) is bytes:
|
||||
if isinstance(packet, bytes):
|
||||
self.source.parser.feed_data(packet)
|
||||
else:
|
||||
logger.warn('discarding packet: not a BINARY frame')
|
||||
logger.warning('discarding packet: not a BINARY frame')
|
||||
except websockets.WebSocketException as error:
|
||||
logger.debug(f'exception while receiving packet: {error}')
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ def composite_listener(cls):
|
||||
registers/deregisters all methods named `on_<event_name>` as a listener for
|
||||
the <event_name> event with an emitter.
|
||||
"""
|
||||
# pylint: disable=protected-access
|
||||
|
||||
def register(self, emitter):
|
||||
for method_name in dir(cls):
|
||||
@@ -65,7 +66,6 @@ def composite_listener(cls):
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class AbortableEventEmitter(EventEmitter):
|
||||
|
||||
def abort_on(self, event: str, awaitable: Awaitable):
|
||||
"""
|
||||
Set a coroutine or future to abort when an event occur.
|
||||
@@ -77,7 +77,7 @@ class AbortableEventEmitter(EventEmitter):
|
||||
def on_event(*_):
|
||||
msg = f'abort: {event} event occurred.'
|
||||
if isinstance(future, asyncio.Task):
|
||||
# python prior to 3.9 does not support passing a message on `Task.cancel`
|
||||
# python < 3.9 does not support passing a message on `Task.cancel`
|
||||
if sys.version_info < (3, 9, 0):
|
||||
future.cancel()
|
||||
else:
|
||||
@@ -105,6 +105,7 @@ class CompositeEventEmitter(AbortableEventEmitter):
|
||||
|
||||
@listener.setter
|
||||
def listener(self, listener):
|
||||
# pylint: disable=protected-access
|
||||
if self._listener:
|
||||
# Call the deregistration methods for each base class that has them
|
||||
for cls in self._listener.__class__.mro():
|
||||
@@ -168,7 +169,8 @@ class AsyncRunner:
|
||||
await coroutine
|
||||
except Exception:
|
||||
logger.warning(
|
||||
f'{color("!!! Exception in wrapper:", "red")} {traceback.format_exc()}'
|
||||
f'{color("!!! Exception in wrapper:", "red")} '
|
||||
f'{traceback.format_exc()}'
|
||||
)
|
||||
|
||||
asyncio.create_task(run())
|
||||
|
||||
Reference in New Issue
Block a user