forked from auracaster/bumble_mirror
initial import
This commit is contained in:
0
bumble/__init__.py
Normal file
0
bumble/__init__.py
Normal file
554
bumble/a2dp.py
Normal file
554
bumble/a2dp.py
Normal file
@@ -0,0 +1,554 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import struct
|
||||
import bitstruct
|
||||
import logging
|
||||
from collections import namedtuple
|
||||
from colors import color
|
||||
|
||||
from .company_ids import COMPANY_IDENTIFIERS
|
||||
from .sdp import (
|
||||
DataElement,
|
||||
ServiceAttribute,
|
||||
SDP_PUBLIC_BROWSE_ROOT,
|
||||
SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
|
||||
SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
|
||||
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
|
||||
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID
|
||||
)
|
||||
from .core import (
|
||||
BT_L2CAP_PROTOCOL_ID,
|
||||
BT_AUDIO_SOURCE_SERVICE,
|
||||
BT_AUDIO_SINK_SERVICE,
|
||||
BT_AVDTP_PROTOCOL_ID,
|
||||
BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE,
|
||||
name_or_number
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
A2DP_SBC_CODEC_TYPE = 0x00
|
||||
A2DP_MPEG_1_2_AUDIO_CODEC_TYPE = 0x01
|
||||
A2DP_MPEG_2_4_AAC_CODEC_TYPE = 0x02
|
||||
A2DP_ATRAC_FAMILY_CODEC_TYPE = 0x03
|
||||
A2DP_NON_A2DP_CODEC_TYPE = 0xFF
|
||||
|
||||
A2DP_CODEC_TYPE_NAMES = {
|
||||
A2DP_SBC_CODEC_TYPE: 'A2DP_SBC_CODEC_TYPE',
|
||||
A2DP_MPEG_1_2_AUDIO_CODEC_TYPE: 'A2DP_MPEG_1_2_AUDIO_CODEC_TYPE',
|
||||
A2DP_MPEG_2_4_AAC_CODEC_TYPE: 'A2DP_MPEG_2_4_AAC_CODEC_TYPE',
|
||||
A2DP_ATRAC_FAMILY_CODEC_TYPE: 'A2DP_ATRAC_FAMILY_CODEC_TYPE',
|
||||
A2DP_NON_A2DP_CODEC_TYPE: 'A2DP_NON_A2DP_CODEC_TYPE'
|
||||
}
|
||||
|
||||
|
||||
SBC_SYNC_WORD = 0x9C
|
||||
|
||||
SBC_SAMPLING_FREQUENCIES = [
|
||||
16000,
|
||||
22050,
|
||||
44100,
|
||||
48000
|
||||
]
|
||||
|
||||
SBC_MONO_CHANNEL_MODE = 0x00
|
||||
SBC_DUAL_CHANNEL_MODE = 0x01
|
||||
SBC_STEREO_CHANNEL_MODE = 0x02
|
||||
SBC_JOINT_STEREO_CHANNEL_MODE = 0x03
|
||||
|
||||
SBC_CHANNEL_MODE_NAMES = {
|
||||
SBC_MONO_CHANNEL_MODE: 'SBC_MONO_CHANNEL_MODE',
|
||||
SBC_DUAL_CHANNEL_MODE: 'SBC_DUAL_CHANNEL_MODE',
|
||||
SBC_STEREO_CHANNEL_MODE: 'SBC_STEREO_CHANNEL_MODE',
|
||||
SBC_JOINT_STEREO_CHANNEL_MODE: 'SBC_JOINT_STEREO_CHANNEL_MODE'
|
||||
}
|
||||
|
||||
SBC_BLOCK_LENGTHS = [4, 8, 12, 16]
|
||||
|
||||
SBC_SUBBANDS = [4, 8]
|
||||
|
||||
SBC_SNR_ALLOCATION_METHOD = 0x00
|
||||
SBC_LOUDNESS_ALLOCATION_METHOD = 0x01
|
||||
|
||||
SBC_ALLOCATION_METHOD_NAMES = {
|
||||
SBC_SNR_ALLOCATION_METHOD: 'SBC_SNR_ALLOCATION_METHOD',
|
||||
SBC_LOUDNESS_ALLOCATION_METHOD: 'SBC_LOUDNESS_ALLOCATION_METHOD'
|
||||
}
|
||||
|
||||
MPEG_2_4_AAC_SAMPLING_FREQUENCIES = [
|
||||
8000,
|
||||
11025,
|
||||
12000,
|
||||
16000,
|
||||
22050,
|
||||
24000,
|
||||
32000,
|
||||
44100,
|
||||
48000,
|
||||
64000,
|
||||
88200,
|
||||
96000
|
||||
]
|
||||
|
||||
MPEG_2_AAC_LC_OBJECT_TYPE = 0x00
|
||||
MPEG_4_AAC_LC_OBJECT_TYPE = 0x01
|
||||
MPEG_4_AAC_LTP_OBJECT_TYPE = 0x02
|
||||
MPEG_4_AAC_SCALABLE_OBJECT_TYPE = 0x03
|
||||
|
||||
MPEG_2_4_OBJECT_TYPE_NAMES = {
|
||||
MPEG_2_AAC_LC_OBJECT_TYPE: 'MPEG_2_AAC_LC_OBJECT_TYPE',
|
||||
MPEG_4_AAC_LC_OBJECT_TYPE: 'MPEG_4_AAC_LC_OBJECT_TYPE',
|
||||
MPEG_4_AAC_LTP_OBJECT_TYPE: 'MPEG_4_AAC_LTP_OBJECT_TYPE',
|
||||
MPEG_4_AAC_SCALABLE_OBJECT_TYPE: 'MPEG_4_AAC_SCALABLE_OBJECT_TYPE'
|
||||
}
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def flags_to_list(flags, values):
|
||||
result = []
|
||||
for i in range(len(values)):
|
||||
if flags & (1 << (len(values) - i - 1)):
|
||||
result.append(values[i])
|
||||
return result
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def make_audio_source_service_sdp_records(service_record_handle, version=(1, 3)):
|
||||
from .avdtp import AVDTP_PSM
|
||||
version_int = version[0] << 8 | version[1]
|
||||
return [
|
||||
ServiceAttribute(SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID, DataElement.unsigned_integer_32(service_record_handle)),
|
||||
ServiceAttribute(SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID, DataElement.sequence([
|
||||
DataElement.uuid(SDP_PUBLIC_BROWSE_ROOT)
|
||||
])),
|
||||
ServiceAttribute(SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID, DataElement.sequence([
|
||||
DataElement.uuid(BT_AUDIO_SOURCE_SERVICE)
|
||||
])),
|
||||
ServiceAttribute(SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID, DataElement.sequence([
|
||||
DataElement.sequence([
|
||||
DataElement.uuid(BT_L2CAP_PROTOCOL_ID),
|
||||
DataElement.unsigned_integer_16(AVDTP_PSM)
|
||||
]),
|
||||
DataElement.sequence([
|
||||
DataElement.uuid(BT_AVDTP_PROTOCOL_ID),
|
||||
DataElement.unsigned_integer_16(version_int)
|
||||
])
|
||||
])),
|
||||
ServiceAttribute(SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID, DataElement.sequence([
|
||||
DataElement.uuid(BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE),
|
||||
DataElement.unsigned_integer_16(version_int)
|
||||
])),
|
||||
]
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def make_audio_sink_service_sdp_records(service_record_handle, version=(1, 3)):
|
||||
from .avdtp import AVDTP_PSM
|
||||
version_int = version[0] << 8 | version[1]
|
||||
return [
|
||||
ServiceAttribute(SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID, DataElement.unsigned_integer_32(service_record_handle)),
|
||||
ServiceAttribute(SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID, DataElement.sequence([
|
||||
DataElement.uuid(SDP_PUBLIC_BROWSE_ROOT)
|
||||
])),
|
||||
ServiceAttribute(SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID, DataElement.sequence([
|
||||
DataElement.uuid(BT_AUDIO_SINK_SERVICE)
|
||||
])),
|
||||
ServiceAttribute(SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID, DataElement.sequence([
|
||||
DataElement.sequence([
|
||||
DataElement.uuid(BT_L2CAP_PROTOCOL_ID),
|
||||
DataElement.unsigned_integer_16(AVDTP_PSM)
|
||||
]),
|
||||
DataElement.sequence([
|
||||
DataElement.uuid(BT_AVDTP_PROTOCOL_ID),
|
||||
DataElement.unsigned_integer_16(version_int)
|
||||
])
|
||||
])),
|
||||
ServiceAttribute(SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID, DataElement.sequence([
|
||||
DataElement.uuid(BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE),
|
||||
DataElement.unsigned_integer_16(version_int)
|
||||
])),
|
||||
]
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class SbcMediaCodecInformation(
|
||||
namedtuple(
|
||||
'SbcMediaCodecInformation',
|
||||
[
|
||||
'sampling_frequency',
|
||||
'channel_mode',
|
||||
'block_length',
|
||||
'subbands',
|
||||
'allocation_method',
|
||||
'minimum_bitpool_value',
|
||||
'maximum_bitpool_value'
|
||||
]
|
||||
)
|
||||
):
|
||||
'''
|
||||
A2DP spec - 4.3.2 Codec Specific Information Elements
|
||||
'''
|
||||
|
||||
BIT_FIELDS = 'u4u4u4u2u2u8u8'
|
||||
SAMPLING_FREQUENCY_BITS = {
|
||||
16000: 1 << 3,
|
||||
32000: 1 << 2,
|
||||
44100: 1 << 1,
|
||||
48000: 1
|
||||
}
|
||||
CHANNEL_MODE_BITS = {
|
||||
SBC_MONO_CHANNEL_MODE: 1 << 3,
|
||||
SBC_DUAL_CHANNEL_MODE: 1 << 2,
|
||||
SBC_STEREO_CHANNEL_MODE: 1 << 1,
|
||||
SBC_JOINT_STEREO_CHANNEL_MODE: 1
|
||||
}
|
||||
BLOCK_LENGTH_BITS = {
|
||||
4: 1 << 3,
|
||||
8: 1 << 2,
|
||||
12: 1 << 1,
|
||||
16: 1
|
||||
}
|
||||
SUBBANDS_BITS = {
|
||||
4: 1 << 1,
|
||||
8: 1
|
||||
}
|
||||
ALLOCATION_METHOD_BITS = {
|
||||
SBC_SNR_ALLOCATION_METHOD: 1 << 1,
|
||||
SBC_LOUDNESS_ALLOCATION_METHOD: 1
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(data):
|
||||
return SbcMediaCodecInformation(*bitstruct.unpack(SbcMediaCodecInformation.BIT_FIELDS, data))
|
||||
|
||||
@classmethod
|
||||
def from_discrete_values(
|
||||
cls,
|
||||
sampling_frequency,
|
||||
channel_mode,
|
||||
block_length,
|
||||
subbands,
|
||||
allocation_method,
|
||||
minimum_bitpool_value,
|
||||
maximum_bitpool_value
|
||||
):
|
||||
return SbcMediaCodecInformation(
|
||||
sampling_frequency = cls.SAMPLING_FREQUENCY_BITS[sampling_frequency],
|
||||
channel_mode = cls.CHANNEL_MODE_BITS[channel_mode],
|
||||
block_length = cls.BLOCK_LENGTH_BITS[block_length],
|
||||
subbands = cls.SUBBANDS_BITS[subbands],
|
||||
allocation_method = cls.ALLOCATION_METHOD_BITS[allocation_method],
|
||||
minimum_bitpool_value = minimum_bitpool_value,
|
||||
maximum_bitpool_value = maximum_bitpool_value
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_lists(
|
||||
cls,
|
||||
sampling_frequencies,
|
||||
channel_modes,
|
||||
block_lengths,
|
||||
subbands,
|
||||
allocation_methods,
|
||||
minimum_bitpool_value,
|
||||
maximum_bitpool_value
|
||||
):
|
||||
return SbcMediaCodecInformation(
|
||||
sampling_frequency = sum(cls.SAMPLING_FREQUENCY_BITS[x] for x in sampling_frequencies),
|
||||
channel_mode = sum(cls.CHANNEL_MODE_BITS[x] for x in channel_modes),
|
||||
block_length = sum(cls.BLOCK_LENGTH_BITS[x] for x in block_lengths),
|
||||
subbands = sum(cls.SUBBANDS_BITS[x] for x in subbands),
|
||||
allocation_method = sum(cls.ALLOCATION_METHOD_BITS[x] for x in allocation_methods),
|
||||
minimum_bitpool_value = minimum_bitpool_value,
|
||||
maximum_bitpool_value = maximum_bitpool_value
|
||||
)
|
||||
|
||||
def __bytes__(self):
|
||||
return bitstruct.pack(self.BIT_FIELDS, *self)
|
||||
|
||||
def __str__(self):
|
||||
channel_modes = ['MONO', 'DUAL_CHANNEL', 'STEREO', 'JOINT_STEREO']
|
||||
allocation_methods = ['SNR', 'Loudness']
|
||||
return '\n'.join([
|
||||
'SbcMediaCodecInformation(',
|
||||
f' sampling_frequency: {",".join([str(x) for x in flags_to_list(self.sampling_frequency, SBC_SAMPLING_FREQUENCIES)])}',
|
||||
f' channel_mode: {",".join([str(x) for x in flags_to_list(self.channel_mode, channel_modes)])}',
|
||||
f' block_length: {",".join([str(x) for x in flags_to_list(self.block_length, SBC_BLOCK_LENGTHS)])}',
|
||||
f' subbands: {",".join([str(x) for x in flags_to_list(self.subbands, SBC_SUBBANDS)])}',
|
||||
f' allocation_method: {",".join([str(x) for x in flags_to_list(self.allocation_method, allocation_methods)])}',
|
||||
f' minimum_bitpool_value: {self.minimum_bitpool_value}',
|
||||
f' maximum_bitpool_value: {self.maximum_bitpool_value}'
|
||||
')'
|
||||
])
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class AacMediaCodecInformation(
|
||||
namedtuple(
|
||||
'AacMediaCodecInformation',
|
||||
[
|
||||
'object_type',
|
||||
'sampling_frequency',
|
||||
'channels',
|
||||
'vbr',
|
||||
'bitrate'
|
||||
]
|
||||
)
|
||||
):
|
||||
'''
|
||||
A2DP spec - 4.5.2 Codec Specific Information Elements
|
||||
'''
|
||||
|
||||
BIT_FIELDS = 'u8u12u2p2u1u23'
|
||||
OBJECT_TYPE_BITS = {
|
||||
MPEG_2_AAC_LC_OBJECT_TYPE: 1 << 7,
|
||||
MPEG_4_AAC_LC_OBJECT_TYPE: 1 << 6,
|
||||
MPEG_4_AAC_LTP_OBJECT_TYPE: 1 << 5,
|
||||
MPEG_4_AAC_SCALABLE_OBJECT_TYPE: 1 << 4
|
||||
}
|
||||
SAMPLING_FREQUENCY_BITS = {
|
||||
8000: 1 << 11,
|
||||
11025: 1 << 10,
|
||||
12000: 1 << 9,
|
||||
16000: 1 << 8,
|
||||
22050: 1 << 7,
|
||||
24000: 1 << 6,
|
||||
32000: 1 << 5,
|
||||
44100: 1 << 4,
|
||||
48000: 1 << 3,
|
||||
64000: 1 << 2,
|
||||
88200: 1 << 1,
|
||||
96000: 1
|
||||
}
|
||||
CHANNELS_BITS = {
|
||||
1: 1 << 1,
|
||||
2: 1
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(data):
|
||||
return AacMediaCodecInformation(*bitstruct.unpack(AacMediaCodecInformation.BIT_FIELDS, data))
|
||||
|
||||
@classmethod
|
||||
def from_discrete_values(
|
||||
cls,
|
||||
object_type,
|
||||
sampling_frequency,
|
||||
channels,
|
||||
vbr,
|
||||
bitrate
|
||||
):
|
||||
return AacMediaCodecInformation(
|
||||
object_type = cls.OBJECT_TYPE_BITS[object_type],
|
||||
sampling_frequency = cls.SAMPLING_FREQUENCY_BITS[sampling_frequency],
|
||||
channels = cls.CHANNELS_BITS[channels],
|
||||
vbr = vbr,
|
||||
bitrate = bitrate
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_lists(
|
||||
cls,
|
||||
object_types,
|
||||
sampling_frequencies,
|
||||
channels,
|
||||
vbr,
|
||||
bitrate
|
||||
):
|
||||
return AacMediaCodecInformation(
|
||||
object_type = sum(cls.OBJECT_TYPE_BITS[x] for x in object_types),
|
||||
sampling_frequency = sum(cls.SAMPLING_FREQUENCY_BITS[x] for x in sampling_frequencies),
|
||||
channels = sum(cls.CHANNELS_BITS[x] for x in channels),
|
||||
vbr = vbr,
|
||||
bitrate = bitrate
|
||||
)
|
||||
|
||||
def __bytes__(self):
|
||||
return bitstruct.pack(self.BIT_FIELDS, *self)
|
||||
|
||||
def __str__(self):
|
||||
object_types = ['MPEG_2_AAC_LC', 'MPEG_4_AAC_LC', 'MPEG_4_AAC_LTP', 'MPEG_4_AAC_SCALABLE', '[4]', '[5]', '[6]', '[7]']
|
||||
channels = [1, 2]
|
||||
return '\n'.join([
|
||||
'AacMediaCodecInformation(',
|
||||
f' object_type: {",".join([str(x) for x in flags_to_list(self.object_type, object_types)])}',
|
||||
f' sampling_frequency: {",".join([str(x) for x in flags_to_list(self.sampling_frequency, MPEG_2_4_AAC_SAMPLING_FREQUENCIES)])}',
|
||||
f' channels: {",".join([str(x) for x in flags_to_list(self.channels, channels)])}',
|
||||
f' vbr: {self.vbr}',
|
||||
f' bitrate: {self.bitrate}'
|
||||
')'
|
||||
])
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class VendorSpecificMediaCodecInformation:
|
||||
'''
|
||||
A2DP spec - 4.7.2 Codec Specific Information Elements
|
||||
'''
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(data):
|
||||
(vendor_id, codec_id) = struct.unpack_from('<IH', data, 0)
|
||||
return VendorSpecificMediaCodecInformation(vendor_id, codec_id, data[6:])
|
||||
|
||||
def __init__(self, vendor_id, codec_id, value):
|
||||
self.vendor_id = vendor_id
|
||||
self.codec_id = codec_id
|
||||
self.value = value
|
||||
|
||||
def __bytes__(self):
|
||||
return struct.pack('<IH', self.vendor_id, self.codec_id, self.value)
|
||||
|
||||
def __str__(self):
|
||||
return '\n'.join([
|
||||
'VendorSpecificMediaCodecInformation(',
|
||||
f' vendor_id: {self.vendor_id:08X} ({name_or_number(COMPANY_IDENTIFIERS, self.vendor_id & 0xFFFF)})',
|
||||
f' codec_id: {self.codec_id:04X}',
|
||||
f' value: {self.value.hex()}'
|
||||
')'
|
||||
])
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class SbcFrame:
|
||||
def __init__(
|
||||
self,
|
||||
sampling_frequency,
|
||||
block_count,
|
||||
channel_mode,
|
||||
subband_count,
|
||||
payload
|
||||
):
|
||||
self.sampling_frequency = sampling_frequency
|
||||
self.block_count = block_count
|
||||
self.channel_mode = channel_mode
|
||||
self.subband_count = subband_count
|
||||
self.payload = payload
|
||||
|
||||
@property
|
||||
def sample_count(self):
|
||||
return self.subband_count * self.block_count
|
||||
|
||||
@property
|
||||
def bitrate(self):
|
||||
return 8 * ((len(self.payload) * self.sampling_frequency) // self.sample_count)
|
||||
|
||||
@property
|
||||
def duration(self):
|
||||
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)})'
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class SbcParser:
|
||||
def __init__(self, read):
|
||||
self.read = read
|
||||
|
||||
@property
|
||||
def frames(self):
|
||||
async def generate_frames():
|
||||
while True:
|
||||
# Read 4 bytes of header
|
||||
header = await self.read(4)
|
||||
if len(header) != 4:
|
||||
return
|
||||
|
||||
# Check the sync word
|
||||
if header[0] != SBC_SYNC_WORD:
|
||||
logger.debug('invalid sync word')
|
||||
return
|
||||
|
||||
# Extract some of the header fields
|
||||
sampling_frequency = SBC_SAMPLING_FREQUENCIES[(header[1] >> 6) & 3]
|
||||
blocks = 4 * (1 + ((header[1] >> 4) & 3))
|
||||
channel_mode = (header[1] >> 2) & 3
|
||||
channels = 1 if channel_mode == SBC_MONO_CHANNEL_MODE else 2
|
||||
subbands = 8 if ((header[1]) & 1) else 4
|
||||
bitpool = header[2]
|
||||
|
||||
# Compute the frame length
|
||||
frame_length = 4 + (4 * subbands * channels) // 8
|
||||
if channel_mode in (SBC_MONO_CHANNEL_MODE, SBC_DUAL_CHANNEL_MODE):
|
||||
frame_length += (blocks * channels * bitpool) // 8
|
||||
else:
|
||||
frame_length += ((1 if channel_mode == SBC_JOINT_STEREO_CHANNEL_MODE else 0) * subbands + blocks * bitpool) // 8
|
||||
|
||||
# Read the rest of the frame
|
||||
payload = header + await self.read(frame_length - 4)
|
||||
|
||||
# Emit the next frame
|
||||
yield SbcFrame(sampling_frequency, blocks, channel_mode, subbands, payload)
|
||||
|
||||
return generate_frames()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class SbcPacketSource:
|
||||
def __init__(self, read, mtu, codec_capabilities):
|
||||
self.read = read
|
||||
self.mtu = mtu
|
||||
self.codec_capabilities = codec_capabilities
|
||||
|
||||
@property
|
||||
def packets(self):
|
||||
async def generate_packets():
|
||||
from .avdtp import MediaPacket # Import here to avoid a circular reference
|
||||
|
||||
sequence_number = 0
|
||||
timestamp = 0
|
||||
frames = []
|
||||
frames_size = 0
|
||||
max_rtp_payload = self.mtu - 12 - 1
|
||||
|
||||
# NOTE: this doesn't support frame fragments
|
||||
sbc_parser = SbcParser(self.read)
|
||||
async for frame in sbc_parser.frames:
|
||||
print(frame)
|
||||
|
||||
if frames_size + len(frame.payload) > max_rtp_payload or len(frames) == 16:
|
||||
# Need to flush what has been accumulated so far
|
||||
|
||||
# Emit a packet
|
||||
sbc_payload = bytes([len(frames)]) + b''.join([frame.payload for frame in frames])
|
||||
packet = MediaPacket(2, 0, 0, 0, sequence_number, timestamp, 0, [], 96, sbc_payload)
|
||||
packet.timestamp_seconds = timestamp / frame.sampling_frequency
|
||||
yield packet
|
||||
|
||||
# Prepare for next packets
|
||||
sequence_number += 1
|
||||
timestamp += sum([frame.sample_count for frame in frames])
|
||||
frames = [frame]
|
||||
frames_size = len(frame.payload)
|
||||
else:
|
||||
# Accumulate
|
||||
frames.append(frame)
|
||||
frames_size += len(frame.payload)
|
||||
|
||||
return generate_packets()
|
||||
728
bumble/att.py
Normal file
728
bumble/att.py
Normal file
@@ -0,0 +1,728 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# ATT - Attribute Protocol
|
||||
#
|
||||
# See Bluetooth spec @ Vol 3, Part F
|
||||
#
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from colors import color
|
||||
from pyee import EventEmitter
|
||||
|
||||
from .core import *
|
||||
from .hci import *
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
ATT_CID = 0x04
|
||||
|
||||
ATT_ERROR_RESPONSE = 0x01
|
||||
ATT_EXCHANGE_MTU_REQUEST = 0x02
|
||||
ATT_EXCHANGE_MTU_RESPONSE = 0x03
|
||||
ATT_FIND_INFORMATION_REQUEST = 0x04
|
||||
ATT_FIND_INFORMATION_RESPONSE = 0x05
|
||||
ATT_FIND_BY_TYPE_VALUE_REQUEST = 0x06
|
||||
ATT_FIND_BY_TYPE_VALUE_RESPONSE = 0x07
|
||||
ATT_READ_BY_TYPE_REQUEST = 0x08
|
||||
ATT_READ_BY_TYPE_RESPONSE = 0x09
|
||||
ATT_READ_REQUEST = 0x0A
|
||||
ATT_READ_RESPONSE = 0x0B
|
||||
ATT_READ_BLOB_REQUEST = 0x0C
|
||||
ATT_READ_BLOB_RESPONSE = 0x0D
|
||||
ATT_READ_MULTIPLE_REQUEST = 0x0E
|
||||
ATT_READ_MULTIPLE_RESPONSE = 0x0F
|
||||
ATT_READ_BY_GROUP_TYPE_REQUEST = 0x10
|
||||
ATT_READ_BY_GROUP_TYPE_RESPONSE = 0x11
|
||||
ATT_WRITE_REQUEST = 0x12
|
||||
ATT_WRITE_RESPONSE = 0x13
|
||||
ATT_WRITE_COMMAND = 0x52
|
||||
ATT_SIGNED_WRITE_COMMAND = 0xD2
|
||||
ATT_PREPARE_WRITE_REQUEST = 0x16
|
||||
ATT_PREPARE_WRITE_RESPONSE = 0x17
|
||||
ATT_EXECUTE_WRITE_REQUEST = 0x18
|
||||
ATT_EXECUTE_WRITE_RESPONSE = 0x19
|
||||
ATT_HANDLE_VALUE_NOTIFICATION = 0x1B
|
||||
ATT_HANDLE_VALUE_INDICATION = 0x1D
|
||||
ATT_HANDLE_VALUE_CONFIRMATION = 0x1E
|
||||
|
||||
ATT_PDU_NAMES = {
|
||||
ATT_ERROR_RESPONSE: 'ATT_ERROR_RESPONSE',
|
||||
ATT_EXCHANGE_MTU_REQUEST: 'ATT_EXCHANGE_MTU_REQUEST',
|
||||
ATT_EXCHANGE_MTU_RESPONSE: 'ATT_EXCHANGE_MTU_RESPONSE',
|
||||
ATT_FIND_INFORMATION_REQUEST: 'ATT_FIND_INFORMATION_REQUEST',
|
||||
ATT_FIND_INFORMATION_RESPONSE: 'ATT_FIND_INFORMATION_RESPONSE',
|
||||
ATT_FIND_BY_TYPE_VALUE_REQUEST: 'ATT_FIND_BY_TYPE_VALUE_REQUEST',
|
||||
ATT_FIND_BY_TYPE_VALUE_RESPONSE: 'ATT_FIND_BY_TYPE_VALUE_RESPONSE',
|
||||
ATT_READ_BY_TYPE_REQUEST: 'ATT_READ_BY_TYPE_REQUEST',
|
||||
ATT_READ_BY_TYPE_RESPONSE: 'ATT_READ_BY_TYPE_RESPONSE',
|
||||
ATT_READ_REQUEST: 'ATT_READ_REQUEST',
|
||||
ATT_READ_RESPONSE: 'ATT_READ_RESPONSE',
|
||||
ATT_READ_BLOB_REQUEST: 'ATT_READ_BLOB_REQUEST',
|
||||
ATT_READ_BLOB_RESPONSE: 'ATT_READ_BLOB_RESPONSE',
|
||||
ATT_READ_MULTIPLE_REQUEST: 'ATT_READ_MULTIPLE_REQUEST',
|
||||
ATT_READ_MULTIPLE_RESPONSE: 'ATT_READ_MULTIPLE_RESPONSE',
|
||||
ATT_READ_BY_GROUP_TYPE_REQUEST: 'ATT_READ_BY_GROUP_TYPE_REQUEST',
|
||||
ATT_READ_BY_GROUP_TYPE_RESPONSE: 'ATT_READ_BY_GROUP_TYPE_RESPONSE',
|
||||
ATT_WRITE_REQUEST: 'ATT_WRITE_REQUEST',
|
||||
ATT_WRITE_RESPONSE: 'ATT_WRITE_RESPONSE',
|
||||
ATT_WRITE_COMMAND: 'ATT_WRITE_COMMAND',
|
||||
ATT_SIGNED_WRITE_COMMAND: 'ATT_SIGNED_WRITE_COMMAND',
|
||||
ATT_PREPARE_WRITE_REQUEST: 'ATT_PREPARE_WRITE_REQUEST',
|
||||
ATT_PREPARE_WRITE_RESPONSE: 'ATT_PREPARE_WRITE_RESPONSE',
|
||||
ATT_EXECUTE_WRITE_REQUEST: 'ATT_EXECUTE_WRITE_REQUEST',
|
||||
ATT_EXECUTE_WRITE_RESPONSE: 'ATT_EXECUTE_WRITE_RESPONSE',
|
||||
ATT_HANDLE_VALUE_NOTIFICATION: 'ATT_HANDLE_VALUE_NOTIFICATION',
|
||||
ATT_HANDLE_VALUE_INDICATION: 'ATT_HANDLE_VALUE_INDICATION',
|
||||
ATT_HANDLE_VALUE_CONFIRMATION: 'ATT_HANDLE_VALUE_CONFIRMATION'
|
||||
}
|
||||
|
||||
ATT_REQUESTS = [
|
||||
ATT_EXCHANGE_MTU_REQUEST,
|
||||
ATT_FIND_INFORMATION_REQUEST,
|
||||
ATT_FIND_BY_TYPE_VALUE_REQUEST,
|
||||
ATT_READ_BY_TYPE_REQUEST,
|
||||
ATT_READ_REQUEST,
|
||||
ATT_READ_BLOB_REQUEST,
|
||||
ATT_READ_MULTIPLE_REQUEST,
|
||||
ATT_READ_BY_GROUP_TYPE_REQUEST,
|
||||
ATT_WRITE_REQUEST,
|
||||
ATT_PREPARE_WRITE_REQUEST,
|
||||
ATT_EXECUTE_WRITE_REQUEST
|
||||
]
|
||||
|
||||
ATT_RESPONSES = [
|
||||
ATT_ERROR_RESPONSE,
|
||||
ATT_EXCHANGE_MTU_RESPONSE,
|
||||
ATT_FIND_INFORMATION_RESPONSE,
|
||||
ATT_FIND_BY_TYPE_VALUE_RESPONSE,
|
||||
ATT_READ_BY_TYPE_RESPONSE,
|
||||
ATT_READ_RESPONSE,
|
||||
ATT_READ_BLOB_RESPONSE,
|
||||
ATT_READ_MULTIPLE_RESPONSE,
|
||||
ATT_READ_BY_GROUP_TYPE_RESPONSE,
|
||||
ATT_WRITE_RESPONSE,
|
||||
ATT_PREPARE_WRITE_RESPONSE,
|
||||
ATT_EXECUTE_WRITE_RESPONSE
|
||||
]
|
||||
|
||||
ATT_INVALID_HANDLE_ERROR = 0x01
|
||||
ATT_READ_NOT_PERMITTED_ERROR = 0x02
|
||||
ATT_WRITE_NOT_PERMITTED_ERROR = 0x03
|
||||
ATT_INVALID_PDU_ERROR = 0x04
|
||||
ATT_INSUFFICIENT_AUTHENTICATION_ERROR = 0x05
|
||||
ATT_REQUEST_NOT_SUPPORTED_ERROR = 0x06
|
||||
ATT_INVALID_OFFSET_ERROR = 0x07
|
||||
ATT_INSUFFICIENT_AUTHORIZATION_ERROR = 0x08
|
||||
ATT_PREPARE_QUEUE_FULL_ERROR = 0x09
|
||||
ATT_ATTRIBUTE_NOT_FOUND_ERROR = 0x0A
|
||||
ATT_ATTRIBUTE_NOT_LONG_ERROR = 0x0B
|
||||
ATT_INSUFFICIENT_ENCRYPTION_KEY_SIZE_ERROR = 0x0C
|
||||
ATT_INVALID_ATTRIBUTE_LENGTH_ERROR = 0x0D
|
||||
ATT_UNLIKELY_ERROR_ERROR = 0x0E
|
||||
ATT_INSUFFICIENT_ENCRYPTION_ERROR = 0x0F
|
||||
ATT_UNSUPPORTED_GROUP_TYPE_ERROR = 0x10
|
||||
ATT_INSUFFICIENT_RESOURCES_ERROR = 0x11
|
||||
|
||||
ATT_ERROR_NAMES = {
|
||||
ATT_INVALID_HANDLE_ERROR: 'ATT_INVALID_HANDLE_ERROR',
|
||||
ATT_READ_NOT_PERMITTED_ERROR: 'ATT_READ_NOT_PERMITTED_ERROR',
|
||||
ATT_WRITE_NOT_PERMITTED_ERROR: 'ATT_WRITE_NOT_PERMITTED_ERROR',
|
||||
ATT_INVALID_PDU_ERROR: 'ATT_INVALID_PDU_ERROR',
|
||||
ATT_INSUFFICIENT_AUTHENTICATION_ERROR: 'ATT_INSUFFICIENT_AUTHENTICATION_ERROR',
|
||||
ATT_REQUEST_NOT_SUPPORTED_ERROR: 'ATT_REQUEST_NOT_SUPPORTED_ERROR',
|
||||
ATT_INVALID_OFFSET_ERROR: 'ATT_INVALID_OFFSET_ERROR',
|
||||
ATT_INSUFFICIENT_AUTHORIZATION_ERROR: 'ATT_INSUFFICIENT_AUTHORIZATION_ERROR',
|
||||
ATT_PREPARE_QUEUE_FULL_ERROR: 'ATT_PREPARE_QUEUE_FULL_ERROR',
|
||||
ATT_ATTRIBUTE_NOT_FOUND_ERROR: 'ATT_ATTRIBUTE_NOT_FOUND_ERROR',
|
||||
ATT_ATTRIBUTE_NOT_LONG_ERROR: 'ATT_ATTRIBUTE_NOT_LONG_ERROR',
|
||||
ATT_INSUFFICIENT_ENCRYPTION_KEY_SIZE_ERROR: 'ATT_INSUFFICIENT_ENCRYPTION_KEY_SIZE_ERROR',
|
||||
ATT_INVALID_ATTRIBUTE_LENGTH_ERROR: 'ATT_INVALID_ATTRIBUTE_LENGTH_ERROR',
|
||||
ATT_UNLIKELY_ERROR_ERROR: 'ATT_UNLIKELY_ERROR_ERROR',
|
||||
ATT_INSUFFICIENT_ENCRYPTION_ERROR: 'ATT_INSUFFICIENT_ENCRYPTION_ERROR',
|
||||
ATT_UNSUPPORTED_GROUP_TYPE_ERROR: 'ATT_UNSUPPORTED_GROUP_TYPE_ERROR',
|
||||
ATT_INSUFFICIENT_RESOURCES_ERROR: 'ATT_INSUFFICIENT_RESOURCES_ERROR'
|
||||
}
|
||||
|
||||
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
|
||||
UUID_2_FIELD_SPEC = lambda x, y: UUID.parse_uuid_2(x, y) # noqa: E731
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Utils
|
||||
# -----------------------------------------------------------------------------
|
||||
def key_with_value(dictionary, target_value):
|
||||
for key, value in dictionary.items():
|
||||
if value == target_value:
|
||||
return key
|
||||
return None
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Exceptions
|
||||
# -----------------------------------------------------------------------------
|
||||
class ATT_Error(Exception):
|
||||
def __init__(self, error_code, att_handle=0x0000):
|
||||
self.error_code = error_code
|
||||
self.att_handle = att_handle
|
||||
|
||||
def __str__(self):
|
||||
return f'ATT_Error({ATT_PDU.error_name(self.error_code)})'
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Attribute Protocol
|
||||
# -----------------------------------------------------------------------------
|
||||
class ATT_PDU:
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.3 ATTRIBUTE PDU
|
||||
'''
|
||||
pdu_classes = {}
|
||||
op_code = 0
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(pdu):
|
||||
op_code = pdu[0]
|
||||
|
||||
cls = ATT_PDU.pdu_classes.get(op_code)
|
||||
if cls is None:
|
||||
instance = ATT_PDU(pdu)
|
||||
instance.name = ATT_PDU.pdu_name(op_code)
|
||||
instance.op_code = op_code
|
||||
return instance
|
||||
self = cls.__new__(cls)
|
||||
ATT_PDU.__init__(self, pdu)
|
||||
if hasattr(self, 'fields'):
|
||||
self.init_from_bytes(pdu, 1)
|
||||
return self
|
||||
|
||||
@staticmethod
|
||||
def pdu_name(op_code):
|
||||
return name_or_number(ATT_PDU_NAMES, op_code, 2)
|
||||
|
||||
@staticmethod
|
||||
def error_name(error_code):
|
||||
return name_or_number(ATT_ERROR_NAMES, error_code, 2)
|
||||
|
||||
@staticmethod
|
||||
def subclass(fields):
|
||||
def inner(cls):
|
||||
cls.name = cls.__name__.upper()
|
||||
cls.op_code = key_with_value(ATT_PDU_NAMES, cls.name)
|
||||
if cls.op_code is None:
|
||||
raise KeyError(f'PDU name {cls.name} not found in ATT_PDU_NAMES')
|
||||
cls.fields = fields
|
||||
|
||||
# Register a factory for this class
|
||||
ATT_PDU.pdu_classes[cls.op_code] = cls
|
||||
|
||||
return cls
|
||||
|
||||
return inner
|
||||
|
||||
def __init__(self, pdu=None, **kwargs):
|
||||
if hasattr(self, 'fields') and kwargs:
|
||||
HCI_Object.init_from_fields(self, self.fields, kwargs)
|
||||
if pdu is None:
|
||||
pdu = bytes([self.op_code]) + HCI_Object.dict_to_bytes(kwargs, self.fields)
|
||||
self.pdu = pdu
|
||||
|
||||
def init_from_bytes(self, pdu, offset):
|
||||
return HCI_Object.init_from_bytes(self, pdu, offset, self.fields)
|
||||
|
||||
def to_bytes(self):
|
||||
return self.pdu
|
||||
|
||||
@property
|
||||
def is_command(self):
|
||||
return ((self.op_code >> 6) & 1) == 1
|
||||
|
||||
@property
|
||||
def has_authentication_signature(self):
|
||||
return ((self.op_code >> 7) & 1) == 1
|
||||
|
||||
def __bytes__(self):
|
||||
return self.to_bytes()
|
||||
|
||||
def __str__(self):
|
||||
result = color(self.name, 'yellow')
|
||||
if fields := getattr(self, 'fields', None):
|
||||
result += ':\n' + HCI_Object.format_fields(self.__dict__, fields, ' ')
|
||||
else:
|
||||
if len(self.pdu) > 1:
|
||||
result += f': {self.pdu.hex()}'
|
||||
return result
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('request_opcode_in_error', {'size': 1, 'mapper': ATT_PDU.pdu_name}),
|
||||
('attribute_handle_in_error', HANDLE_FIELD_SPEC),
|
||||
('error_code', {'size': 1, 'mapper': ATT_PDU.error_name})
|
||||
])
|
||||
class ATT_Error_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.1.1 Error Response
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('client_rx_mtu', 2)
|
||||
])
|
||||
class ATT_Exchange_MTU_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.2.1 Exchange MTU Request
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('server_rx_mtu', 2)
|
||||
])
|
||||
class ATT_Exchange_MTU_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.2.2 Exchange MTU Response
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('starting_handle', HANDLE_FIELD_SPEC),
|
||||
('ending_handle', HANDLE_FIELD_SPEC)
|
||||
])
|
||||
class ATT_Find_Information_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.3.1 Find Information Request
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('format', 1),
|
||||
('information_data', '*')
|
||||
])
|
||||
class ATT_Find_Information_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.3.2 Find Information Response
|
||||
'''
|
||||
|
||||
def parse_information_data(self):
|
||||
self.information = []
|
||||
offset = 0
|
||||
uuid_size = 2 if self.format == 1 else 16
|
||||
while offset + uuid_size <= len(self.information_data):
|
||||
handle = struct.unpack_from('<H', self.information_data, offset)[0]
|
||||
uuid = self.information_data[2 + offset:2 + offset + uuid_size]
|
||||
self.information.append((handle, uuid))
|
||||
offset += 2 + uuid_size
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.parse_information_data()
|
||||
|
||||
def init_from_bytes(self, pdu, offset):
|
||||
super().init_from_bytes(pdu, offset)
|
||||
self.parse_information_data()
|
||||
|
||||
def __str__(self):
|
||||
result = color(self.name, 'yellow')
|
||||
result += ':\n' + HCI_Object.format_fields(self.__dict__, [
|
||||
('format', 1),
|
||||
('information', {'mapper': lambda x: ', '.join([f'0x{handle:04X}:{uuid.hex()}' for handle, uuid in x])})
|
||||
], ' ')
|
||||
return result
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('starting_handle', HANDLE_FIELD_SPEC),
|
||||
('ending_handle', HANDLE_FIELD_SPEC),
|
||||
('attribute_type', UUID_2_FIELD_SPEC),
|
||||
('attribute_value', '*')
|
||||
])
|
||||
class ATT_Find_By_Type_Value_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.3.3 Find By Type Value Request
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('handles_information_list', '*')
|
||||
])
|
||||
class ATT_Find_By_Type_Value_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.3.4 Find By Type Value Response
|
||||
'''
|
||||
|
||||
def parse_handles_information_list(self):
|
||||
self.handles_information = []
|
||||
offset = 0
|
||||
while offset + 4 <= len(self.handles_information_list):
|
||||
found_attribute_handle, group_end_handle = struct.unpack_from('<HH', self.handles_information_list, offset)
|
||||
self.handles_information.append((found_attribute_handle, group_end_handle))
|
||||
offset += 4
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.parse_handles_information_list()
|
||||
|
||||
def init_from_bytes(self, pdu, offset):
|
||||
super().init_from_bytes(pdu, offset)
|
||||
self.parse_handles_information_list()
|
||||
|
||||
def __str__(self):
|
||||
result = color(self.name, 'yellow')
|
||||
result += ':\n' + HCI_Object.format_fields(self.__dict__, [
|
||||
('handles_information', {'mapper': lambda x: ', '.join([f'0x{handle1:04X}-0x{handle2:04X}' for handle1, handle2 in x])})
|
||||
], ' ')
|
||||
return result
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('starting_handle', HANDLE_FIELD_SPEC),
|
||||
('ending_handle', HANDLE_FIELD_SPEC),
|
||||
('attribute_type', UUID_2_16_FIELD_SPEC)
|
||||
])
|
||||
class ATT_Read_By_Type_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.1 Read By Type Request
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('length', 1),
|
||||
('attribute_data_list', '*')
|
||||
])
|
||||
class ATT_Read_By_Type_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.2 Read By Type Response
|
||||
'''
|
||||
|
||||
def parse_attribute_data_list(self):
|
||||
self.attributes = []
|
||||
offset = 0
|
||||
while self.length != 0 and offset + self.length <= len(self.attribute_data_list):
|
||||
attribute_handle, = struct.unpack_from('<H', self.attribute_data_list, offset)
|
||||
attribute_value = self.attribute_data_list[offset + 2:offset + self.length]
|
||||
self.attributes.append((attribute_handle, attribute_value))
|
||||
offset += self.length
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.parse_attribute_data_list()
|
||||
|
||||
def init_from_bytes(self, pdu, offset):
|
||||
super().init_from_bytes(pdu, offset)
|
||||
self.parse_attribute_data_list()
|
||||
|
||||
def __str__(self):
|
||||
result = color(self.name, 'yellow')
|
||||
result += ':\n' + HCI_Object.format_fields(self.__dict__, [
|
||||
('length', 1),
|
||||
('attributes', {'mapper': lambda x: ', '.join([f'0x{handle:04X}:{value.hex()}' for handle, value in x])})
|
||||
], ' ')
|
||||
return result
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('attribute_handle', HANDLE_FIELD_SPEC)
|
||||
])
|
||||
class ATT_Read_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.3 Read Request
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('attribute_value', '*')
|
||||
])
|
||||
class ATT_Read_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.4 Read Response
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('attribute_handle', HANDLE_FIELD_SPEC),
|
||||
('value_offset', 2)
|
||||
])
|
||||
class ATT_Read_Blob_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.5 Read Blob Request
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('part_attribute_value', '*')
|
||||
])
|
||||
class ATT_Read_Blob_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.6 Read Blob Response
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('set_of_handles', '*')
|
||||
])
|
||||
class ATT_Read_Multiple_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.7 Read Multiple Request
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('set_of_values', '*')
|
||||
])
|
||||
class ATT_Read_Multiple_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.8 Read Multiple Response
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('starting_handle', HANDLE_FIELD_SPEC),
|
||||
('ending_handle', HANDLE_FIELD_SPEC),
|
||||
('attribute_group_type', UUID_2_16_FIELD_SPEC)
|
||||
])
|
||||
class ATT_Read_By_Group_Type_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.9 Read by Group Type Request
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('length', 1),
|
||||
('attribute_data_list', '*')
|
||||
])
|
||||
class ATT_Read_By_Group_Type_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.4.10 Read by Group Type Response
|
||||
'''
|
||||
|
||||
def parse_attribute_data_list(self):
|
||||
self.attributes = []
|
||||
offset = 0
|
||||
while self.length != 0 and offset + self.length <= len(self.attribute_data_list):
|
||||
attribute_handle, end_group_handle = struct.unpack_from('<HH', self.attribute_data_list, offset)
|
||||
attribute_value = self.attribute_data_list[offset + 4:offset + self.length]
|
||||
self.attributes.append((attribute_handle, end_group_handle, attribute_value))
|
||||
offset += self.length
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.parse_attribute_data_list()
|
||||
|
||||
def init_from_bytes(self, pdu, offset):
|
||||
super().init_from_bytes(pdu, offset)
|
||||
self.parse_attribute_data_list()
|
||||
|
||||
def __str__(self):
|
||||
result = color(self.name, 'yellow')
|
||||
result += ':\n' + HCI_Object.format_fields(self.__dict__, [
|
||||
('length', 1),
|
||||
('attributes', {'mapper': lambda x: ', '.join([f'0x{handle:04X}-0x{end:04X}:{value.hex()}' for handle, end, value in x])})
|
||||
], ' ')
|
||||
return result
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('attribute_handle', HANDLE_FIELD_SPEC),
|
||||
('attribute_value', '*')
|
||||
])
|
||||
class ATT_Write_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.5.1 Write Request
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([])
|
||||
class ATT_Write_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.5.2 Write Response
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('attribute_handle', HANDLE_FIELD_SPEC),
|
||||
('attribute_value', '*')
|
||||
])
|
||||
class ATT_Write_Command(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.5.3 Write Command
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('attribute_handle', HANDLE_FIELD_SPEC),
|
||||
('attribute_value', '*')
|
||||
# ('authentication_signature', 'TODO')
|
||||
])
|
||||
class ATT_Signed_Write_Command(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.5.4 Signed Write Command
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('attribute_handle', HANDLE_FIELD_SPEC),
|
||||
('value_offset', 2),
|
||||
('part_attribute_value', '*')
|
||||
])
|
||||
class ATT_Prepare_Write_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.6.1 Prepare Write Request
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('attribute_handle', HANDLE_FIELD_SPEC),
|
||||
('value_offset', 2),
|
||||
('part_attribute_value', '*')
|
||||
])
|
||||
class ATT_Prepare_Write_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.6.2 Prepare Write Response
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([])
|
||||
class ATT_Execute_Write_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.6.3 Execute Write Request
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([])
|
||||
class ATT_Execute_Write_Response(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.6.4 Execute Write Response
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('attribute_handle', HANDLE_FIELD_SPEC),
|
||||
('attribute_value', '*')
|
||||
])
|
||||
class ATT_Handle_Value_Notification(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.7.1 Handle Value Notification
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([
|
||||
('attribute_handle', HANDLE_FIELD_SPEC),
|
||||
('attribute_value', '*')
|
||||
])
|
||||
class ATT_Handle_Value_Indication(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.7.2 Handle Value Indication
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([])
|
||||
class ATT_Handle_Value_Confirmation(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.7.3 Handle Value Confirmation
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Attribute(EventEmitter):
|
||||
# Permission flags
|
||||
READABLE = 0x01
|
||||
WRITEABLE = 0x02
|
||||
READ_REQUIRES_ENCRYPTION = 0x04
|
||||
WRITE_REQUIRES_ENCRYPTION = 0x08
|
||||
READ_REQUIRES_AUTHENTICATION = 0x10
|
||||
WRITE_REQUIRES_AUTHENTICATION = 0x20
|
||||
READ_REQUIRES_AUTHORIZATION = 0x40
|
||||
WRITE_REQUIRES_AUTHORIZATION = 0x80
|
||||
|
||||
def __init__(self, attribute_type, permissions, value = b''):
|
||||
EventEmitter.__init__(self)
|
||||
self.handle = 0
|
||||
self.permissions = permissions
|
||||
|
||||
# Convert the type to a UUID
|
||||
if type(attribute_type) is 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:
|
||||
self.value = bytes(value, 'utf-8')
|
||||
else:
|
||||
self.value = value
|
||||
|
||||
def read_value(self, connection):
|
||||
if type(self.value) is bytes:
|
||||
return self.value
|
||||
else:
|
||||
if read := getattr(self.value, 'read', None):
|
||||
try:
|
||||
return read(connection)
|
||||
except ATT_Error as error:
|
||||
raise ATT_Error(error_code=error.error_code, att_handle=self.handle)
|
||||
else:
|
||||
return bytes(self.value)
|
||||
|
||||
def write_value(self, connection, value):
|
||||
if write := getattr(self.value, 'write', None):
|
||||
try:
|
||||
write(connection, value)
|
||||
except ATT_Error as error:
|
||||
raise ATT_Error(error_code=error.error_code, att_handle=self.handle)
|
||||
else:
|
||||
self.value = value
|
||||
|
||||
self.emit('write', connection, value)
|
||||
|
||||
def __repr__(self):
|
||||
if len(self.value) > 0:
|
||||
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})'
|
||||
1921
bumble/avdtp.py
Normal file
1921
bumble/avdtp.py
Normal file
File diff suppressed because it is too large
Load Diff
82
bumble/bridge.py
Normal file
82
bumble/bridge.py
Normal file
@@ -0,0 +1,82 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import logging
|
||||
|
||||
from .hci import HCI_Packet
|
||||
from .helpers import PacketTracer
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class HCI_Bridge:
|
||||
class Forwarder:
|
||||
def __init__(self, hci_sink, sender_hci_sink, packet_filter, trace):
|
||||
self.hci_sink = hci_sink
|
||||
self.sender_hci_sink = sender_hci_sink
|
||||
self.packet_filter = packet_filter
|
||||
self.trace = trace
|
||||
|
||||
def on_packet(self, packet):
|
||||
# Convert the packet bytes to an object
|
||||
hci_packet = HCI_Packet.from_bytes(packet)
|
||||
|
||||
# Filter the packet
|
||||
if self.packet_filter is not None:
|
||||
filtered = self.packet_filter(hci_packet)
|
||||
if filtered is not None:
|
||||
packet, respond_to_sender = filtered
|
||||
hci_packet = HCI_Packet.from_bytes(packet)
|
||||
if respond_to_sender:
|
||||
self.sender_hci_sink.on_packet(packet)
|
||||
return
|
||||
|
||||
# Analyze the packet
|
||||
self.trace(hci_packet)
|
||||
|
||||
# Bridge the packet
|
||||
self.hci_sink.on_packet(packet)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hci_host_source,
|
||||
hci_host_sink,
|
||||
hci_controller_source,
|
||||
hci_controller_sink,
|
||||
host_to_controller_filter = None,
|
||||
controller_to_host_filter = None
|
||||
):
|
||||
tracer = PacketTracer(emit_message=logger.info)
|
||||
host_to_controller_forwarder = HCI_Bridge.Forwarder(
|
||||
hci_controller_sink,
|
||||
hci_host_sink,
|
||||
host_to_controller_filter,
|
||||
lambda packet: tracer.trace(packet, 0)
|
||||
)
|
||||
hci_host_source.set_packet_sink(host_to_controller_forwarder)
|
||||
|
||||
controller_to_host_forwarder = HCI_Bridge.Forwarder(
|
||||
hci_host_sink,
|
||||
hci_controller_sink,
|
||||
controller_to_host_filter,
|
||||
lambda packet: tracer.trace(packet, 1)
|
||||
)
|
||||
hci_controller_source.set_packet_sink(controller_to_host_forwarder)
|
||||
2708
bumble/company_ids.py
Normal file
2708
bumble/company_ids.py
Normal file
File diff suppressed because it is too large
Load Diff
895
bumble/controller.py
Normal file
895
bumble/controller.py
Normal file
@@ -0,0 +1,895 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import logging
|
||||
import asyncio
|
||||
import itertools
|
||||
import random
|
||||
|
||||
from .hci import *
|
||||
from .l2cap import *
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Utils
|
||||
# -----------------------------------------------------------------------------
|
||||
class DataObject:
|
||||
pass
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Connection:
|
||||
def __init__(self, controller, handle, role, peer_address, link):
|
||||
self.controller = controller
|
||||
self.handle = handle
|
||||
self.role = role
|
||||
self.peer_address = peer_address
|
||||
self.link = link
|
||||
self.assembler = HCI_AclDataPacketAssembler(self.on_acl_pdu)
|
||||
|
||||
def on_hci_acl_data_packet(self, packet):
|
||||
self.assembler.feed_packet(packet)
|
||||
self.controller.send_hci_packet(HCI_Number_Of_Completed_Packets_Event([(self.handle, 1)]))
|
||||
|
||||
def on_acl_pdu(self, data):
|
||||
if self.link:
|
||||
self.link.send_acl_data(self.controller.random_address, self.peer_address, data)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Controller:
|
||||
def __init__(self, name, host_source = None, host_sink = None, link = None):
|
||||
self.name = name
|
||||
self.hci_sink = None
|
||||
self.link = link
|
||||
|
||||
self.central_connections = {} # Connections where this controller is the central
|
||||
self.peripheral_connections = {} # Connections where this controller is the peripheral
|
||||
|
||||
self.hci_version = HCI_VERSION_BLUETOOTH_CORE_5_0
|
||||
self.hci_revision = 0
|
||||
self.lmp_version = HCI_VERSION_BLUETOOTH_CORE_5_0
|
||||
self.lmp_subversion = 0
|
||||
self.lmp_features = bytes.fromhex('0000000060000000') # BR/EDR Not Supported, LE Supported (Controller)
|
||||
self.manufacturer_name = 0xFFFF
|
||||
self.hc_le_data_packet_length = 27
|
||||
self.hc_total_num_le_data_packets = 64
|
||||
self.supported_commands = bytes.fromhex('2000800000c000000000e40000002822000000000000040000f7ffff7f00000030f0f9ff01008004000000000000000000000000000000000000000000000000')
|
||||
self.le_features = bytes.fromhex('ff49010000000000')
|
||||
self.le_states = bytes.fromhex('ffff3fffff030000')
|
||||
self.avertising_channel_tx_power = 0
|
||||
self.white_list_size = 8
|
||||
self.resolving_list_size = 8
|
||||
self.supported_max_tx_octets = 27
|
||||
self.supported_max_tx_time = 10000 # microseconds
|
||||
self.supported_max_rx_octets = 27
|
||||
self.supported_max_rx_time = 10000 # microseconds
|
||||
self.suggested_max_tx_octets = 27
|
||||
self.suggested_max_tx_time = 0x0148 # microseconds
|
||||
self.default_phy = bytes([0, 0, 0])
|
||||
self.le_scan_type = 0
|
||||
self.le_scan_interval = 0x10
|
||||
self.le_scan_window = 0x10
|
||||
self.le_scan_enable = 0
|
||||
self.le_scan_own_address_type = Address.RANDOM_DEVICE_ADDRESS
|
||||
self.le_scanning_filter_policy = 0
|
||||
self.le_scan_response_data = None
|
||||
self.le_address_resolution = False
|
||||
self.le_rpa_timeout = 0
|
||||
self.sync_flow_control = False
|
||||
self.local_name = 'Bumble'
|
||||
|
||||
self.advertising_interval = 2000 # Fixed for now
|
||||
self.advertising_data = None
|
||||
self.advertising_timer_handle = None
|
||||
|
||||
self._random_address = Address('00:00:00:00:00:00')
|
||||
self._public_address = None
|
||||
|
||||
# Set the source and sink interfaces
|
||||
if host_source:
|
||||
host_source.set_packet_sink(self)
|
||||
self.host = host_sink
|
||||
|
||||
# Add this controller to the link if specified
|
||||
if link:
|
||||
link.add_controller(self)
|
||||
|
||||
@property
|
||||
def host(self):
|
||||
return self.hci_sink
|
||||
|
||||
@host.setter
|
||||
def host(self, 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:
|
||||
host.controller = self
|
||||
|
||||
def set_packet_sink(self, sink):
|
||||
'''
|
||||
Method from the Packet Source interface
|
||||
'''
|
||||
self.hci_sink = sink
|
||||
|
||||
@property
|
||||
def public_address(self):
|
||||
return self._public_address
|
||||
|
||||
@public_address.setter
|
||||
def public_address(self, address):
|
||||
if type(address) is str:
|
||||
address = Address(address)
|
||||
self._public_address = address
|
||||
|
||||
@property
|
||||
def random_address(self):
|
||||
return self._random_address
|
||||
|
||||
@random_address.setter
|
||||
def random_address(self, address):
|
||||
if type(address) is str:
|
||||
address = Address(address)
|
||||
self._random_address = address
|
||||
logger.debug(f'new random address: {address}')
|
||||
|
||||
if self.link:
|
||||
self.link.on_address_changed(self)
|
||||
|
||||
# Packet Sink protocol (packets coming from the host via HCI)
|
||||
def on_packet(self, packet):
|
||||
self.on_hci_packet(HCI_Packet.from_bytes(packet))
|
||||
|
||||
def on_hci_packet(self, packet):
|
||||
logger.debug(f'{color("<<<", "blue")} [{self.name}] {color("HOST -> CONTROLLER", "blue")}: {packet}')
|
||||
|
||||
# If the packet is a command, invoke the handler for this packet
|
||||
if packet.hci_packet_type == HCI_COMMAND_PACKET:
|
||||
self.on_hci_command_packet(packet)
|
||||
elif packet.hci_packet_type == HCI_EVENT_PACKET:
|
||||
self.on_hci_event_packet(packet)
|
||||
elif packet.hci_packet_type == HCI_ACL_DATA_PACKET:
|
||||
self.on_hci_acl_data_packet(packet)
|
||||
else:
|
||||
logger.warning(f'!!! unknown packet type {packet.hci_packet_type}')
|
||||
|
||||
def on_hci_command_packet(self, command):
|
||||
handler_name = f'on_{command.name.lower()}'
|
||||
handler = getattr(self, handler_name, self.on_hci_command)
|
||||
result = handler(command)
|
||||
if type(result) is bytes:
|
||||
self.send_hci_packet(HCI_Command_Complete_Event(
|
||||
num_hci_command_packets = 1,
|
||||
command_opcode = command.op_code,
|
||||
return_parameters = result
|
||||
))
|
||||
|
||||
def on_hci_event_packet(self, event):
|
||||
logger.warning('!!! unexpected event packet')
|
||||
|
||||
def on_hci_acl_data_packet(self, packet):
|
||||
# Look for the connection to which this data belongs
|
||||
connection = self.find_connection_by_handle(packet.connection_handle)
|
||||
if connection is None:
|
||||
logger.warning(f'!!! no connection for handle 0x{packet.connection_handle:04X}')
|
||||
return
|
||||
|
||||
# Pass the packet to the connection
|
||||
connection.on_hci_acl_data_packet(packet)
|
||||
|
||||
def send_hci_packet(self, packet):
|
||||
logger.debug(f'{color(">>>", "green")} [{self.name}] {color("CONTROLLER -> HOST", "green")}: {packet}')
|
||||
if self.host:
|
||||
self.host.on_packet(packet.to_bytes())
|
||||
|
||||
# This method allow the controller to emulate the same API as a transport source
|
||||
async def wait_for_termination(self):
|
||||
# For now, just wait forever
|
||||
await asyncio.get_running_loop().create_future()
|
||||
|
||||
############################################################
|
||||
# Link connections
|
||||
############################################################
|
||||
def allocate_connection_handle(self):
|
||||
handle = 0
|
||||
max_handle = 0
|
||||
for connection in itertools.chain(
|
||||
self.central_connections.values(),
|
||||
self.peripheral_connections.values()
|
||||
):
|
||||
max_handle = max(max_handle, connection.handle)
|
||||
if connection.handle == handle:
|
||||
# Already used, continue searching after the current max
|
||||
handle = max_handle + 1
|
||||
return handle
|
||||
|
||||
def find_connection_by_address(self, address):
|
||||
return self.central_connections.get(address) or self.peripheral_connections.get(address)
|
||||
|
||||
def find_connection_by_handle(self, handle):
|
||||
for connection in itertools.chain(
|
||||
self.central_connections.values(),
|
||||
self.peripheral_connections.values()
|
||||
):
|
||||
if connection.handle == handle:
|
||||
return connection
|
||||
return None
|
||||
|
||||
def find_central_connection_by_handle(self, handle):
|
||||
for connection in self.central_connections.values():
|
||||
if connection.handle == handle:
|
||||
return connection
|
||||
return None
|
||||
|
||||
def on_link_central_connected(self, central_address):
|
||||
'''
|
||||
Called when an incoming connection occurs from a central on the link
|
||||
'''
|
||||
|
||||
# Allocate (or reuse) a connection handle
|
||||
peer_address = central_address
|
||||
peer_address_type = central_address.address_type
|
||||
connection = self.peripheral_connections.get(peer_address)
|
||||
if connection is None:
|
||||
connection_handle = self.allocate_connection_handle()
|
||||
connection = Connection(self, connection_handle, BT_PERIPHERAL_ROLE, peer_address, self.link)
|
||||
self.peripheral_connections[peer_address] = connection
|
||||
logger.debug(f'New PERIPHERAL connection handle: 0x{connection_handle:04X}')
|
||||
|
||||
# Then say that the connection has completed
|
||||
self.send_hci_packet(HCI_LE_Connection_Complete_Event(
|
||||
status = HCI_SUCCESS,
|
||||
connection_handle = connection.handle,
|
||||
role = connection.role,
|
||||
peer_address_type = peer_address_type,
|
||||
peer_address = peer_address,
|
||||
conn_interval = 10, # FIXME
|
||||
conn_latency = 0, # FIXME
|
||||
supervision_timeout = 10, # FIXME
|
||||
master_clock_accuracy = 7 # FIXME
|
||||
))
|
||||
|
||||
def on_link_central_disconnected(self, peer_address, reason):
|
||||
'''
|
||||
Called when an active disconnection occurs from a peer
|
||||
'''
|
||||
|
||||
# Send a disconnection complete event
|
||||
if connection := self.peripheral_connections.get(peer_address):
|
||||
self.send_hci_packet(HCI_Disconnection_Complete_Event(
|
||||
status = HCI_SUCCESS,
|
||||
connection_handle = connection.handle,
|
||||
reason = reason
|
||||
))
|
||||
|
||||
# Remove the connection
|
||||
del self.peripheral_connections[peer_address]
|
||||
else:
|
||||
logger.warn(f'!!! No peripheral connection found for {peer_address}')
|
||||
|
||||
def on_link_peripheral_connection_complete(self, le_create_connection_command, status):
|
||||
'''
|
||||
Called by the link when a connection has been made or has failed to be made
|
||||
'''
|
||||
|
||||
if status == HCI_SUCCESS:
|
||||
# Allocate (or reuse) a connection handle
|
||||
peer_address = le_create_connection_command.peer_address
|
||||
connection = self.central_connections.get(peer_address)
|
||||
if connection is None:
|
||||
connection_handle = self.allocate_connection_handle()
|
||||
connection = Connection(
|
||||
self,
|
||||
connection_handle,
|
||||
BT_CENTRAL_ROLE,
|
||||
peer_address,
|
||||
self.link
|
||||
)
|
||||
self.central_connections[peer_address] = connection
|
||||
logger.debug(f'New CENTRAL connection handle: 0x{connection_handle:04X}')
|
||||
else:
|
||||
connection = None
|
||||
|
||||
# Say that the connection has completed
|
||||
self.send_hci_packet(HCI_LE_Connection_Complete_Event(
|
||||
status = status,
|
||||
connection_handle = connection.handle if connection else 0,
|
||||
role = BT_CENTRAL_ROLE,
|
||||
peer_address_type = le_create_connection_command.peer_address_type,
|
||||
peer_address = le_create_connection_command.peer_address,
|
||||
conn_interval = le_create_connection_command.conn_interval_min,
|
||||
conn_latency = le_create_connection_command.conn_latency,
|
||||
supervision_timeout = le_create_connection_command.supervision_timeout,
|
||||
master_clock_accuracy = 0
|
||||
))
|
||||
|
||||
def on_link_peripheral_disconnection_complete(self, disconnection_command, status):
|
||||
'''
|
||||
Called when a disconnection has been completed
|
||||
'''
|
||||
|
||||
# Send a disconnection complete event
|
||||
self.send_hci_packet(HCI_Disconnection_Complete_Event(
|
||||
status = status,
|
||||
connection_handle = disconnection_command.connection_handle,
|
||||
reason = disconnection_command.reason
|
||||
))
|
||||
|
||||
# Remove the connection
|
||||
if connection := self.find_central_connection_by_handle(disconnection_command.connection_handle):
|
||||
logger.debug(f'CENTRAL Connection removed: {connection}')
|
||||
del self.central_connections[connection.peer_address]
|
||||
|
||||
def on_link_peripheral_disconnected(self, peer_address):
|
||||
'''
|
||||
Called when a connection to a peripheral is broken
|
||||
'''
|
||||
|
||||
# Send a disconnection complete event
|
||||
if connection := self.central_connections.get(peer_address):
|
||||
self.send_hci_packet(HCI_Disconnection_Complete_Event(
|
||||
status = HCI_SUCCESS,
|
||||
connection_handle = connection.handle,
|
||||
reason = HCI_CONNECTION_TIMEOUT_ERROR
|
||||
))
|
||||
|
||||
# Remove the connection
|
||||
del self.central_connections[peer_address]
|
||||
else:
|
||||
logger.warn(f'!!! No central connection found for {peer_address}')
|
||||
|
||||
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(
|
||||
HCI_Encryption_Change_Event(
|
||||
status = 0,
|
||||
connection_handle = connection.handle,
|
||||
encryption_enabled = 1
|
||||
)
|
||||
)
|
||||
|
||||
def on_link_acl_data(self, sender_address, data):
|
||||
# Look for the connection to which this data belongs
|
||||
connection = self.find_connection_by_address(sender_address)
|
||||
if connection is None:
|
||||
logger.warning(f'!!! no connection for {sender_address}')
|
||||
return
|
||||
|
||||
# Send the data to the host
|
||||
# TODO: should fragment
|
||||
acl_packet = HCI_AclDataPacket(connection.handle, 2, 0, len(data), data)
|
||||
self.send_hci_packet(acl_packet)
|
||||
|
||||
def on_link_advertising_data(self, sender_address, data):
|
||||
# Ignore if we're not scanning
|
||||
if self.le_scan_enable == 0:
|
||||
return
|
||||
|
||||
# Send a scan report
|
||||
report = HCI_Object(
|
||||
HCI_LE_Advertising_Report_Event.REPORT_FIELDS,
|
||||
event_type = HCI_LE_Advertising_Report_Event.ADV_IND,
|
||||
address_type = sender_address.address_type,
|
||||
address = sender_address,
|
||||
data = data,
|
||||
rssi = -50
|
||||
)
|
||||
self.send_hci_packet(HCI_LE_Advertising_Report_Event([report]))
|
||||
|
||||
# Simulate a scan response
|
||||
report = HCI_Object(
|
||||
HCI_LE_Advertising_Report_Event.REPORT_FIELDS,
|
||||
event_type = HCI_LE_Advertising_Report_Event.SCAN_RSP,
|
||||
address_type = sender_address.address_type,
|
||||
address = sender_address,
|
||||
data = data,
|
||||
rssi = -50
|
||||
)
|
||||
self.send_hci_packet(HCI_LE_Advertising_Report_Event([report]))
|
||||
|
||||
############################################################
|
||||
# Advertising support
|
||||
############################################################
|
||||
def on_advertising_timer_fired(self):
|
||||
self.send_advertising_data()
|
||||
self.advertising_timer_handle = asyncio.get_running_loop().call_later(self.advertising_interval / 1000.0, self.on_advertising_timer_fired)
|
||||
|
||||
def start_advertising(self):
|
||||
# Stop any ongoing advertising before we start again
|
||||
self.stop_advertising()
|
||||
|
||||
# Advertise now
|
||||
self.advertising_timer_handle = asyncio.get_running_loop().call_soon(self.on_advertising_timer_fired)
|
||||
|
||||
def stop_advertising(self):
|
||||
if self.advertising_timer_handle is not None:
|
||||
self.advertising_timer_handle.cancel()
|
||||
self.advertising_timer_handle = None
|
||||
|
||||
def send_advertising_data(self):
|
||||
if self.link and self.advertising_data:
|
||||
self.link.send_advertising_data(self.random_address, self.advertising_data)
|
||||
|
||||
@property
|
||||
def is_advertising(self):
|
||||
return self.advertising_timer_handle is not None
|
||||
|
||||
############################################################
|
||||
# HCI handlers
|
||||
############################################################
|
||||
def on_hci_command(self, command):
|
||||
logger.warning(color(f'--- Unsupported command {command}', 'red'))
|
||||
return bytes([HCI_UNKNOWN_HCI_COMMAND_ERROR])
|
||||
|
||||
def on_hci_create_connection_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.1.5 Create Connection command
|
||||
'''
|
||||
|
||||
# TODO: classic mode not supported yet
|
||||
|
||||
def on_hci_disconnect_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.1.6 Disconnect Command
|
||||
'''
|
||||
# First, say that the disconnection is pending
|
||||
self.send_hci_packet(HCI_Command_Status_Event(
|
||||
status = HCI_COMMAND_STATUS_PENDING,
|
||||
num_hci_command_packets = 1,
|
||||
command_opcode = command.op_code
|
||||
))
|
||||
|
||||
# Notify the link of the disconnection
|
||||
if not (connection := self.find_central_connection_by_handle(command.connection_handle)):
|
||||
logger.warn('connection not found')
|
||||
return
|
||||
|
||||
if self.link:
|
||||
self.link.disconnect(self.random_address, connection.peer_address, command)
|
||||
else:
|
||||
# Remove the connection
|
||||
del self.central_connections[connection.peer_address]
|
||||
|
||||
def on_hci_set_event_mask_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.3.1 Set Event Mask Command
|
||||
'''
|
||||
self.event_mask = command.event_mask
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_reset_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.3.2 Reset Command
|
||||
'''
|
||||
# TODO: cleanup what needs to be reset
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_write_local_name_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.3.11 Write Local Name Command
|
||||
'''
|
||||
local_name = command.local_name
|
||||
if len(local_name):
|
||||
try:
|
||||
first_null = local_name.find(0)
|
||||
if first_null >= 0:
|
||||
local_name = local_name[:first_null]
|
||||
self.local_name = str(local_name, 'utf-8')
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_read_local_name_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.3.12 Read Local Name Command
|
||||
'''
|
||||
local_name = bytes(self.local_name, 'utf-8')[:248]
|
||||
if len(local_name) < 248:
|
||||
local_name = local_name + bytes(248 - len(local_name))
|
||||
|
||||
return bytes([HCI_SUCCESS]) + local_name
|
||||
|
||||
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):
|
||||
'''
|
||||
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):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.3.36 Read Synchronous Flow Control Enable Command
|
||||
'''
|
||||
if self.sync_flow_control:
|
||||
ret = 1
|
||||
else:
|
||||
ret = 0
|
||||
return bytes([HCI_SUCCESS, ret])
|
||||
|
||||
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
|
||||
'''
|
||||
ret = HCI_SUCCESS
|
||||
if command.synchronous_flow_control_enable == 1:
|
||||
self.sync_flow_control = True
|
||||
elif command.synchronous_flow_control_enable == 0:
|
||||
self.sync_flow_control = False
|
||||
else:
|
||||
ret = HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR
|
||||
return bytes([ret])
|
||||
|
||||
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
|
||||
'''
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_set_event_mask_page_2_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.3.69 Set Event Mask Page 2 Command
|
||||
'''
|
||||
self.event_mask_page_2 = command.event_mask_page_2
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
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):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.3.79 Write LE Host Support Command
|
||||
'''
|
||||
# TODO / Just ignore for now
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
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
|
||||
'''
|
||||
# TODO
|
||||
return struct.pack('<BH', HCI_SUCCESS, command.connection_handle)
|
||||
|
||||
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
|
||||
'''
|
||||
return struct.pack('<BBHBHH',
|
||||
HCI_SUCCESS,
|
||||
self.hci_version,
|
||||
self.hci_revision,
|
||||
self.lmp_version,
|
||||
self.manufacturer_name,
|
||||
self.lmp_subversion)
|
||||
|
||||
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):
|
||||
'''
|
||||
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):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.4.6 Read BD_ADDR Command
|
||||
'''
|
||||
bd_addr = self._public_address.to_bytes() if self._public_address is not None else bytes(6)
|
||||
return bytes([HCI_SUCCESS]) + bd_addr
|
||||
|
||||
def on_hci_le_set_event_mask_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.1 LE Set Event Mask Command
|
||||
'''
|
||||
self.le_event_mask = command.le_event_mask
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
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
|
||||
'''
|
||||
return struct.pack('<BHB',
|
||||
HCI_SUCCESS,
|
||||
self.hc_le_data_packet_length,
|
||||
self.hc_total_num_le_data_packets)
|
||||
|
||||
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
|
||||
'''
|
||||
return bytes([HCI_SUCCESS]) + self.le_features
|
||||
|
||||
def on_hci_le_set_random_address_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.4 LE Set Random Address Command
|
||||
'''
|
||||
self.random_address = command.random_address
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_le_set_advertising_parameters_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.5 LE Set Advertising Parameters Command
|
||||
'''
|
||||
self.advertising_parameters = command
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
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
|
||||
'''
|
||||
return bytes([HCI_SUCCESS, self.avertising_channel_tx_power])
|
||||
|
||||
def on_hci_le_set_advertising_data_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.7 LE Set Advertising Data Command
|
||||
'''
|
||||
self.advertising_data = command.advertising_data
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_le_set_scan_response_data_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.8 LE Set Scan Response Data Command
|
||||
'''
|
||||
self.le_scan_response_data = command.scan_response_data
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_le_set_advertising_enable_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.9 LE Set Advertising Enable Command
|
||||
'''
|
||||
if command.advertising_enable:
|
||||
self.start_advertising()
|
||||
else:
|
||||
self.stop_advertising()
|
||||
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_le_set_scan_parameters_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.10 LE Set Scan Parameters Command
|
||||
'''
|
||||
self.le_scan_type = command.le_scan_type
|
||||
self.le_scan_interval = command.le_scan_interval
|
||||
self.le_scan_window = command.le_scan_window
|
||||
self.le_scan_own_address_type = command.own_address_type
|
||||
self.le_scanning_filter_policy = command.scanning_filter_policy
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_le_set_scan_enable_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.11 LE Set Scan Enable Command
|
||||
'''
|
||||
self.le_scan_enable = command.le_scan_enable
|
||||
self.filter_duplicates = command.filter_duplicates
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_le_create_connection_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.12 LE Create Connection Command
|
||||
'''
|
||||
|
||||
if not self.link:
|
||||
return
|
||||
|
||||
logger.debug(f'Connection request to {command.peer_address}')
|
||||
|
||||
# Check that we don't already have a pending connection
|
||||
if self.link.get_pending_connection():
|
||||
self.send_hci_packet(HCI_Command_Status_Event(
|
||||
status = HCI_COMMAND_DISALLOWED_ERROR,
|
||||
num_hci_command_packets = 1,
|
||||
command_opcode = command.op_code
|
||||
))
|
||||
return
|
||||
|
||||
# Initiate the connection
|
||||
self.link.connect(self.random_address, command)
|
||||
|
||||
# Say that the connection is pending
|
||||
self.send_hci_packet(HCI_Command_Status_Event(
|
||||
status = HCI_COMMAND_STATUS_PENDING,
|
||||
num_hci_command_packets = 1,
|
||||
command_opcode = command.op_code
|
||||
))
|
||||
|
||||
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_white_list_size_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.14 LE Read White List Size Command
|
||||
'''
|
||||
return bytes([HCI_SUCCESS, self.white_list_size])
|
||||
|
||||
def on_hci_le_clear_white_list_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.15 LE Clear White List Command
|
||||
'''
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_le_add_device_to_white_list_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.16 LE Add Device To White List Command
|
||||
'''
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_le_remove_device_from_white_list_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.17 LE Remove Device From White List Command
|
||||
'''
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_le_read_remote_features_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.21 LE Read Remote Features Command
|
||||
'''
|
||||
|
||||
# First, say that the command is pending
|
||||
self.send_hci_packet(HCI_Command_Status_Event(
|
||||
status = HCI_COMMAND_STATUS_PENDING,
|
||||
num_hci_command_packets = 1,
|
||||
command_opcode = command.op_code
|
||||
))
|
||||
|
||||
# Then send the remote features
|
||||
self.send_hci_packet(HCI_LE_Read_Remote_Features_Complete_Event(
|
||||
status = HCI_SUCCESS,
|
||||
connection_handle = 0,
|
||||
le_features = bytes.fromhex('dd40000000000000')
|
||||
))
|
||||
|
||||
def on_hci_le_rand_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.23 LE Rand Command
|
||||
'''
|
||||
return bytes([HCI_SUCCESS]) + struct.pack('Q', random.randint(0, 1 << 64))
|
||||
|
||||
def on_hci_le_start_encryption_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.24 LE Start Encryption Command
|
||||
'''
|
||||
|
||||
# Check the parameters
|
||||
if not (connection := self.find_central_connection_by_handle(command.connection_handle)):
|
||||
logger.warn('connection not found')
|
||||
return bytes([HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR])
|
||||
|
||||
# Notify that the connection is now encrypted
|
||||
self.link.on_connection_encrypted(
|
||||
self.random_address,
|
||||
connection.peer_address,
|
||||
command.random_number,
|
||||
command.encrypted_diversifier,
|
||||
command.long_term_key
|
||||
)
|
||||
|
||||
self.send_hci_packet(HCI_Command_Status_Event(
|
||||
status = HCI_COMMAND_STATUS_PENDING,
|
||||
num_hci_command_packets = 1,
|
||||
command_opcode = command.op_code
|
||||
))
|
||||
|
||||
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):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.34 LE Read Suggested Default Data Length Command
|
||||
'''
|
||||
return struct.pack('<BHH',
|
||||
HCI_SUCCESS,
|
||||
self.suggested_max_tx_octets,
|
||||
self.suggested_max_tx_time)
|
||||
|
||||
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
|
||||
'''
|
||||
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):
|
||||
'''
|
||||
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):
|
||||
'''
|
||||
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):
|
||||
'''
|
||||
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):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.41 LE Read Resolving List Size Command
|
||||
'''
|
||||
return bytes([HCI_SUCCESS, self.resolving_list_size])
|
||||
|
||||
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
|
||||
'''
|
||||
ret = HCI_SUCCESS
|
||||
if command.address_resolution == 1:
|
||||
self.le_address_resolution = True
|
||||
elif command.address_resolution == 0:
|
||||
self.le_address_resolution = False
|
||||
else:
|
||||
ret = HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR
|
||||
return bytes([ret])
|
||||
|
||||
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
|
||||
'''
|
||||
self.le_rpa_timeout = command.rpa_timeout
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
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
|
||||
'''
|
||||
return struct.pack('<BHHHH',
|
||||
HCI_SUCCESS,
|
||||
self.supported_max_tx_octets,
|
||||
self.supported_max_tx_time,
|
||||
self.supported_max_rx_octets,
|
||||
self.supported_max_rx_time)
|
||||
|
||||
def on_hci_le_set_default_phy_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.48 LE Set Default PHY Command
|
||||
'''
|
||||
self.default_phy = {
|
||||
'all_phys': command.all_phys,
|
||||
'tx_phys': command.tx_phys,
|
||||
'rx_phys': command.rx_phys
|
||||
}
|
||||
return bytes([HCI_SUCCESS])
|
||||
852
bumble/core.py
Normal file
852
bumble/core.py
Normal file
@@ -0,0 +1,852 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import struct
|
||||
|
||||
from .company_ids import COMPANY_IDENTIFIERS
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
BT_CENTRAL_ROLE = 0
|
||||
BT_PERIPHERAL_ROLE = 1
|
||||
|
||||
BT_BR_EDR_TRANSPORT = 0
|
||||
BT_LE_TRANSPORT = 1
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Utils
|
||||
# -----------------------------------------------------------------------------
|
||||
def bit_flags_to_strings(bits, bit_flag_names):
|
||||
names = []
|
||||
index = 0
|
||||
while bits != 0:
|
||||
if bits & 1:
|
||||
name = bit_flag_names[index] if index < len(bit_flag_names) else f'#{index}'
|
||||
names.append(name)
|
||||
bits >>= 1
|
||||
index += 1
|
||||
|
||||
return names
|
||||
|
||||
|
||||
def name_or_number(dictionary, number, width=2):
|
||||
name = dictionary.get(number)
|
||||
if name is not None:
|
||||
return name
|
||||
return f'[0x{number:0{width}X}]'
|
||||
|
||||
|
||||
def padded_bytes(buffer, size):
|
||||
padding_size = max(size - len(buffer), 0)
|
||||
return buffer + bytes(padding_size)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Exceptions
|
||||
# -----------------------------------------------------------------------------
|
||||
class BaseError(Exception):
|
||||
""" Base class for errors with an error code, error name and namespace"""
|
||||
def __init__(self, error_code, error_namespace='', error_name='', details=''):
|
||||
super().__init__()
|
||||
self.error_code = error_code
|
||||
self.error_namespace = error_namespace
|
||||
self.error_name = error_name
|
||||
self.details = details
|
||||
|
||||
def __str__(self):
|
||||
if self.error_namespace:
|
||||
namespace = f'{self.error_namespace}/'
|
||||
else:
|
||||
namespace = ''
|
||||
if self.error_name:
|
||||
name = f'{self.error_name} [0x{self.error_code:X}]'
|
||||
else:
|
||||
name = f'0x{self.error_code:X}'
|
||||
|
||||
return f'{type(self).__name__}({namespace}{name})'
|
||||
|
||||
|
||||
class ProtocolError(BaseError):
|
||||
""" Protocol Error """
|
||||
|
||||
|
||||
class TimeoutError(Exception):
|
||||
""" Timeout Error """
|
||||
|
||||
|
||||
class InvalidStateError(Exception):
|
||||
""" Invalid State Error """
|
||||
|
||||
|
||||
class ConnectionError(BaseError):
|
||||
""" Connection Error """
|
||||
FAILURE = 0x01
|
||||
CONNECTION_REFUSED = 0x02
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# UUID
|
||||
#
|
||||
# NOTE: the internal byte representation is in little-endian byte order
|
||||
#
|
||||
# Base UUID: 00000000-0000-1000-8000- 00805F9B34FB
|
||||
# -----------------------------------------------------------------------------
|
||||
class UUID:
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part B - 2.5.1 UUID
|
||||
'''
|
||||
BASE_UUID = bytes.fromhex('00001000800000805F9B34FB')
|
||||
UUIDS = [] # Registry of all instances created
|
||||
|
||||
def __init__(self, uuid_str_or_int, name = None):
|
||||
if type(uuid_str_or_int) is int:
|
||||
self.uuid_bytes = struct.pack('<H', uuid_str_or_int)
|
||||
else:
|
||||
if len(uuid_str_or_int) == 36:
|
||||
if uuid_str_or_int[8] != '-' or uuid_str_or_int[13] != '-' or uuid_str_or_int[18] != '-' or uuid_str_or_int[23] != '-':
|
||||
raise ValueError('invalid UUID format')
|
||||
uuid_str = uuid_str_or_int.replace('-', '')
|
||||
else:
|
||||
uuid_str = uuid_str_or_int
|
||||
if len(uuid_str) != 32 and len(uuid_str) != 8 and len(uuid_str) != 4:
|
||||
raise ValueError('invalid UUID format')
|
||||
self.uuid_bytes = bytes(reversed(bytes.fromhex(uuid_str)))
|
||||
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
|
||||
for uuid in self.UUIDS:
|
||||
if self == uuid:
|
||||
if uuid.name is None:
|
||||
uuid.name = self.name
|
||||
return uuid
|
||||
|
||||
self.UUIDS.append(self)
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, uuid_bytes, name = None):
|
||||
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')
|
||||
|
||||
@classmethod
|
||||
def from_16_bits(cls, uuid_16, name = None):
|
||||
return cls.from_bytes(struct.pack('<H', uuid_16), name)
|
||||
|
||||
@classmethod
|
||||
def from_32_bits(cls, uuid_32, name = None):
|
||||
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:])
|
||||
|
||||
@classmethod
|
||||
def parse_uuid_2(cls, bytes, offset):
|
||||
return offset + 2, cls.from_bytes(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:
|
||||
return self.uuid_bytes + UUID.BASE_UUID
|
||||
else:
|
||||
return self.uuid_bytes + bytes([0, 0]) + UUID.BASE_UUID
|
||||
|
||||
def to_pdu_bytes(self):
|
||||
'''
|
||||
Convert to bytes for use in an ATT PDU.
|
||||
According to Vol 3, Part F - 3.2.1 Attribute Type:
|
||||
"All 32-bit Attribute UUIDs shall be converted to 128-bit UUIDs when the
|
||||
Attribute UUID is contained in an ATT PDU."
|
||||
'''
|
||||
return self.to_bytes(force_128 = (len(self.uuid_bytes) == 4))
|
||||
|
||||
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()
|
||||
|
||||
def __bytes__(self):
|
||||
return self.to_bytes()
|
||||
|
||||
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:
|
||||
return UUID(other) == self
|
||||
|
||||
return False
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.uuid_bytes)
|
||||
|
||||
def __str__(self):
|
||||
if len(self.uuid_bytes) == 2:
|
||||
v = struct.unpack('<H', self.uuid_bytes)[0]
|
||||
result = f'UUID-16:{v:04X}'
|
||||
elif len(self.uuid_bytes) == 4:
|
||||
v = struct.unpack('<I', self.uuid_bytes)[0]
|
||||
result = f'UUID-32:{v:08X}'
|
||||
else:
|
||||
result = '-'.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()
|
||||
if self.name is not None:
|
||||
return result + f' ({self.name})'
|
||||
else:
|
||||
return result
|
||||
|
||||
def __repr__(self):
|
||||
return str(self)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Common UUID constants
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Protocol Identifiers
|
||||
BT_SDP_PROTOCOL_ID = UUID.from_16_bits(0x0001, 'SDP')
|
||||
BT_UDP_PROTOCOL_ID = UUID.from_16_bits(0x0002, 'UDP')
|
||||
BT_RFCOMM_PROTOCOL_ID = UUID.from_16_bits(0x0003, 'RFCOMM')
|
||||
BT_TCP_PROTOCOL_ID = UUID.from_16_bits(0x0004, 'TCP')
|
||||
BT_TCS_BIN_PROTOCOL_ID = UUID.from_16_bits(0x0005, 'TCP-BIN')
|
||||
BT_TCS_AT_PROTOCOL_ID = UUID.from_16_bits(0x0006, 'TCS-AT')
|
||||
BT_ATT_PROTOCOL_ID = UUID.from_16_bits(0x0007, 'ATT')
|
||||
BT_OBEX_PROTOCOL_ID = UUID.from_16_bits(0x0008, 'OBEX')
|
||||
BT_IP_PROTOCOL_ID = UUID.from_16_bits(0x0009, 'IP')
|
||||
BT_FTP_PROTOCOL_ID = UUID.from_16_bits(0x000A, 'FTP')
|
||||
BT_HTTP_PROTOCOL_ID = UUID.from_16_bits(0x000C, 'HTTP')
|
||||
BT_WSP_PROTOCOL_ID = UUID.from_16_bits(0x000E, 'WSP')
|
||||
BT_BNEP_PROTOCOL_ID = UUID.from_16_bits(0x000F, 'BNEP')
|
||||
BT_UPNP_PROTOCOL_ID = UUID.from_16_bits(0x0010, 'UPNP')
|
||||
BT_HIDP_PROTOCOL_ID = UUID.from_16_bits(0x0011, 'HIDP')
|
||||
BT_HARDCOPY_CONTROL_CHANNEL_PROTOCOL_ID = UUID.from_16_bits(0x0012, 'HardcopyControlChannel')
|
||||
BT_HARDCOPY_DATA_CHANNEL_PROTOCOL_ID = UUID.from_16_bits(0x0014, 'HardcopyDataChannel')
|
||||
BT_HARDCOPY_NOTIFICATION_PROTOCOL_ID = UUID.from_16_bits(0x0016, 'HardcopyNotification')
|
||||
BT_AVTCP_PROTOCOL_ID = UUID.from_16_bits(0x0017, 'AVCTP')
|
||||
BT_AVDTP_PROTOCOL_ID = UUID.from_16_bits(0x0019, 'AVDTP')
|
||||
BT_CMTP_PROTOCOL_ID = UUID.from_16_bits(0x001B, 'CMTP')
|
||||
BT_MCAP_CONTROL_CHANNEL_PROTOCOL_ID = UUID.from_16_bits(0x001E, 'MCAPControlChannel')
|
||||
BT_MCAP_DATA_CHANNEL_PROTOCOL_ID = UUID.from_16_bits(0x001F, 'MCAPDataChannel')
|
||||
BT_L2CAP_PROTOCOL_ID = UUID.from_16_bits(0x0100, 'L2CAP')
|
||||
|
||||
# Service Classes and Profiles
|
||||
BT_SERVICE_DISCOVERY_SERVER_SERVICE_CLASS_ID_SERVICE = UUID.from_16_bits(0x1000, 'ServiceDiscoveryServerServiceClassID')
|
||||
BT_BROWSE_GROUP_DESCRIPTOR_SERVICE_CLASS_ID_SERVICE = UUID.from_16_bits(0x1001, 'BrowseGroupDescriptorServiceClassID')
|
||||
BT_SERIAL_PORT_SERVICE = UUID.from_16_bits(0x1101, 'SerialPort')
|
||||
BT_LAN_ACCESS_USING_PPP_SERVICE = UUID.from_16_bits(0x1102, 'LANAccessUsingPPP')
|
||||
BT_DIALUP_NETWORKING_SERVICE = UUID.from_16_bits(0x1103, 'DialupNetworking')
|
||||
BT_IR_MCSYNC_SERVICE = UUID.from_16_bits(0x1104, 'IrMCSync')
|
||||
BT_OBEX_OBJECT_PUSH_SERVICE = UUID.from_16_bits(0x1105, 'OBEXObjectPush')
|
||||
BT_OBEX_FILE_TRANSFER_SERVICE = UUID.from_16_bits(0x1106, 'OBEXFileTransfer')
|
||||
BT_IR_MCSYNC_COMMAND_SERVICE = UUID.from_16_bits(0x1107, 'IrMCSyncCommand')
|
||||
BT_HEADSET_SERVICE = UUID.from_16_bits(0x1108, 'Headset')
|
||||
BT_CORDLESS_TELEPHONY_SERVICE = UUID.from_16_bits(0x1109, 'CordlessTelephony')
|
||||
BT_AUDIO_SOURCE_SERVICE = UUID.from_16_bits(0x110A, 'AudioSource')
|
||||
BT_AUDIO_SINK_SERVICE = UUID.from_16_bits(0x110B, 'AudioSink')
|
||||
BT_AV_REMOTE_CONTROL_TARGET_SERVICE = UUID.from_16_bits(0x110C, 'A/V_RemoteControlTarget')
|
||||
BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE = UUID.from_16_bits(0x110D, 'AdvancedAudioDistribution')
|
||||
BT_AV_REMOTE_CONTROL_SERVICE = UUID.from_16_bits(0x110E, 'A/V_RemoteControl')
|
||||
BT_AV_REMOTE_CONTROL_CONTROLLER_SERVICE = UUID.from_16_bits(0x110F, 'A/V_RemoteControlController')
|
||||
BT_INTERCOM_SERVICE = UUID.from_16_bits(0x1110, 'Intercom')
|
||||
BT_FAX_SERVICE = UUID.from_16_bits(0x1111, 'Fax')
|
||||
BT_HEADSET_AUDIO_GATEWAY_SERVICE = UUID.from_16_bits(0x1112, 'Headset - Audio Gateway')
|
||||
BT_WAP_SERVICE = UUID.from_16_bits(0x1113, 'WAP')
|
||||
BT_WAP_CLIENT_SERVICE = UUID.from_16_bits(0x1114, 'WAP_CLIENT')
|
||||
BT_PANU_SERVICE = UUID.from_16_bits(0x1115, 'PANU')
|
||||
BT_NAP_SERVICE = UUID.from_16_bits(0x1116, 'NAP')
|
||||
BT_GN_SERVICE = UUID.from_16_bits(0x1117, 'GN')
|
||||
BT_DIRECT_PRINTING_SERVICE = UUID.from_16_bits(0x1118, 'DirectPrinting')
|
||||
BT_REFERENCE_PRINTING_SERVICE = UUID.from_16_bits(0x1119, 'ReferencePrinting')
|
||||
BT_BASIC_IMAGING_PROFILE_SERVICE = UUID.from_16_bits(0x111A, 'Basic Imaging Profile')
|
||||
BT_IMAGING_RESPONDER_SERVICE = UUID.from_16_bits(0x111B, 'ImagingResponder')
|
||||
BT_IMAGING_AUTOMATIC_ARCHIVE_SERVICE = UUID.from_16_bits(0x111C, 'ImagingAutomaticArchive')
|
||||
BT_IMAGING_REFERENCED_OBJECTS_SERVICE = UUID.from_16_bits(0x111D, 'ImagingReferencedObjects')
|
||||
BT_HANDSFREE_SERVICE = UUID.from_16_bits(0x111E, 'Handsfree')
|
||||
BT_HANDSFREE_AUDIO_GATEWAY_SERVICE = UUID.from_16_bits(0x111F, 'HandsfreeAudioGateway')
|
||||
BT_DIRECT_PRINTING_REFERENCE_OBJECTS_SERVICE = UUID.from_16_bits(0x1120, 'DirectPrintingReferenceObjectsService')
|
||||
BT_REFLECTED_UI_SERVICE = UUID.from_16_bits(0x1121, 'ReflectedUI')
|
||||
BT_BASIC_PRINTING_SERVICE = UUID.from_16_bits(0x1122, 'BasicPrinting')
|
||||
BT_PRINTING_STATUS_SERVICE = UUID.from_16_bits(0x1123, 'PrintingStatus')
|
||||
BT_HUMAN_INTERFACE_DEVICE_SERVICE = UUID.from_16_bits(0x1124, 'HumanInterfaceDeviceService')
|
||||
BT_HARDCOPY_CABLE_REPLACEMENT_SERVICE = UUID.from_16_bits(0x1125, 'HardcopyCableReplacement')
|
||||
BT_HCR_PRINT_SERVICE = UUID.from_16_bits(0x1126, 'HCR_Print')
|
||||
BT_HCR_SCAN_SERVICE = UUID.from_16_bits(0x1127, 'HCR_Scan')
|
||||
BT_COMMON_ISDN_ACCESS_SERVICE = UUID.from_16_bits(0x1128, 'Common_ISDN_Access')
|
||||
BT_SIM_ACCESS_SERVICE = UUID.from_16_bits(0x112D, 'SIM_Access')
|
||||
BT_PHONEBOOK_ACCESS_PCE_SERVICE = UUID.from_16_bits(0x112E, 'Phonebook Access - PCE')
|
||||
BT_PHONEBOOK_ACCESS_PSE_SERVICE = UUID.from_16_bits(0x112F, 'Phonebook Access - PSE')
|
||||
BT_PHONEBOOK_ACCESS_SERVICE = UUID.from_16_bits(0x1130, 'Phonebook Access')
|
||||
BT_HEADSET_HS_SERVICE = UUID.from_16_bits(0x1131, 'Headset - HS')
|
||||
BT_MESSAGE_ACCESS_SERVER_SERVICE = UUID.from_16_bits(0x1132, 'Message Access Server')
|
||||
BT_MESSAGE_NOTIFICATION_SERVER_SERVICE = UUID.from_16_bits(0x1133, 'Message Notification Server')
|
||||
BT_MESSAGE_ACCESS_PROFILE_SERVICE = UUID.from_16_bits(0x1134, 'Message Access Profile')
|
||||
BT_GNSS_SERVICE = UUID.from_16_bits(0x1135, 'GNSS')
|
||||
BT_GNSS_SERVER_SERVICE = UUID.from_16_bits(0x1136, 'GNSS_Server')
|
||||
BT_3D_DISPLAY_SERVICE = UUID.from_16_bits(0x1137, '3D Display')
|
||||
BT_3D_GLASSES_SERVICE = UUID.from_16_bits(0x1138, '3D Glasses')
|
||||
BT_3D_SYNCHRONIZATION_SERVICE = UUID.from_16_bits(0x1139, '3D Synchronization')
|
||||
BT_MPS_PROFILE_SERVICE = UUID.from_16_bits(0x113A, 'MPS Profile')
|
||||
BT_MPS_SC_SERVICE = UUID.from_16_bits(0x113B, 'MPS SC')
|
||||
BT_ACCESS_SERVICE_SERVICE = UUID.from_16_bits(0x113C, 'CTN Access Service')
|
||||
BT_CTN_NOTIFICATION_SERVICE_SERVICE = UUID.from_16_bits(0x113D, 'CTN Notification Service')
|
||||
BT_CTN_PROFILE_SERVICE = UUID.from_16_bits(0x113E, 'CTN Profile')
|
||||
BT_PNP_INFORMATION_SERVICE = UUID.from_16_bits(0x1200, 'PnPInformation')
|
||||
BT_GENERIC_NETWORKING_SERVICE = UUID.from_16_bits(0x1201, 'GenericNetworking')
|
||||
BT_GENERIC_FILE_TRANSFER_SERVICE = UUID.from_16_bits(0x1202, 'GenericFileTransfer')
|
||||
BT_GENERIC_AUDIO_SERVICE = UUID.from_16_bits(0x1203, 'GenericAudio')
|
||||
BT_GENERIC_TELEPHONY_SERVICE = UUID.from_16_bits(0x1204, 'GenericTelephony')
|
||||
BT_UPNP_SERVICE = UUID.from_16_bits(0x1205, 'UPNP_Service')
|
||||
BT_UPNP_IP_SERVICE = UUID.from_16_bits(0x1206, 'UPNP_IP_Service')
|
||||
BT_ESDP_UPNP_IP_PAN_SERVICE = UUID.from_16_bits(0x1300, 'ESDP_UPNP_IP_PAN')
|
||||
BT_ESDP_UPNP_IP_LAP_SERVICE = UUID.from_16_bits(0x1301, 'ESDP_UPNP_IP_LAP')
|
||||
BT_ESDP_UPNP_L2CAP_SERVICE = UUID.from_16_bits(0x1302, 'ESDP_UPNP_L2CAP')
|
||||
BT_VIDEO_SOURCE_SERVICE = UUID.from_16_bits(0x1303, 'VideoSource')
|
||||
BT_VIDEO_SINK_SERVICE = UUID.from_16_bits(0x1304, 'VideoSink')
|
||||
BT_VIDEO_DISTRIBUTION_SERVICE = UUID.from_16_bits(0x1305, 'VideoDistribution')
|
||||
BT_HDP_SERVICE = UUID.from_16_bits(0x1400, 'HDP')
|
||||
BT_HDP_SOURCE_SERVICE = UUID.from_16_bits(0x1401, 'HDP Source')
|
||||
BT_HDP_SINK_SERVICE = UUID.from_16_bits(0x1402, 'HDP Sink')
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# DeviceClass
|
||||
# -----------------------------------------------------------------------------
|
||||
class DeviceClass:
|
||||
# Major Service Classes (flags combined with OR)
|
||||
LIMITED_DISCOVERABLE_MODE_SERVICE_CLASS = (1 << 0)
|
||||
LE_AUDIO_SERVICE_CLASS = (1 << 1)
|
||||
RESERVED = (1 << 2)
|
||||
POSITIONING_SERVICE_CLASS = (1 << 3)
|
||||
NETWORKING_SERVICE_CLASS = (1 << 4)
|
||||
RENDERING_SERVICE_CLASS = (1 << 5)
|
||||
CAPTURING_SERVICE_CLASS = (1 << 6)
|
||||
OBJECT_TRANSFER_SERVICE_CLASS = (1 << 7)
|
||||
AUDIO_SERVICE_CLASS = (1 << 8)
|
||||
TELEPHONY_SERVICE_CLASS = (1 << 9)
|
||||
INFORMATION_SERVICE_CLASS = (1 << 10)
|
||||
|
||||
SERVICE_CLASS_LABELS = [
|
||||
'Limited Discoverable Mode',
|
||||
'LE audio',
|
||||
'(reserved)',
|
||||
'Positioning',
|
||||
'Networking',
|
||||
'Rendering',
|
||||
'Capturing',
|
||||
'Object Transfer',
|
||||
'Audio',
|
||||
'Telephony',
|
||||
'Information'
|
||||
]
|
||||
|
||||
# Major Device Classes
|
||||
MISCELLANEOUS_MAJOR_DEVICE_CLASS = 0x00
|
||||
COMPUTER_MAJOR_DEVICE_CLASS = 0x01
|
||||
PHONE_MAJOR_DEVICE_CLASS = 0x02
|
||||
LAN_NETWORK_ACCESS_POINT_MAJOR_DEVICE_CLASS = 0x03
|
||||
AUDIO_VIDEO_MAJOR_DEVICE_CLASS = 0x04
|
||||
PERIPHERAL_MAJOR_DEVICE_CLASS = 0x05
|
||||
IMAGING_MAJOR_DEVICE_CLASS = 0x06
|
||||
WEARABLE_MAJOR_DEVICE_CLASS = 0x07
|
||||
TOY_MAJOR_DEVICE_CLASS = 0x08
|
||||
HEALTH_MAJOR_DEVICE_CLASS = 0x09
|
||||
UNCATEGORIZED_MAJOR_DEVICE_CLASS = 0x1F
|
||||
|
||||
MAJOR_DEVICE_CLASS_NAMES = {
|
||||
MISCELLANEOUS_MAJOR_DEVICE_CLASS: 'Miscellaneous',
|
||||
COMPUTER_MAJOR_DEVICE_CLASS: 'Computer',
|
||||
PHONE_MAJOR_DEVICE_CLASS: 'Phone',
|
||||
LAN_NETWORK_ACCESS_POINT_MAJOR_DEVICE_CLASS: 'LAN/Network Access Point',
|
||||
AUDIO_VIDEO_MAJOR_DEVICE_CLASS: 'Audio/Video',
|
||||
PERIPHERAL_MAJOR_DEVICE_CLASS: 'Peripheral',
|
||||
IMAGING_MAJOR_DEVICE_CLASS: 'Imaging',
|
||||
WEARABLE_MAJOR_DEVICE_CLASS: 'Wearable',
|
||||
TOY_MAJOR_DEVICE_CLASS: 'Toy',
|
||||
HEALTH_MAJOR_DEVICE_CLASS: 'Health',
|
||||
UNCATEGORIZED_MAJOR_DEVICE_CLASS: 'Uncategorized'
|
||||
}
|
||||
|
||||
COMPUTER_UNCATEGORIZED_MINOR_DEVICE_CLASS = 0x00
|
||||
COMPUTER_DESKTOP_WORKSTATION_MINOR_DEVICE_CLASS = 0x01
|
||||
COMPUTER_SERVER_CLASS_COMPUTER_MINOR_DEVICE_CLASS = 0x02
|
||||
COMPUTER_LAPTOP_COMPUTER_MINOR_DEVICE_CLASS = 0x03
|
||||
COMPUTER_HANDHELD_PC_PDA_MINOR_DEVICE_CLASS = 0x04
|
||||
COMPUTER_PALM_SIZE_PC_PDA_MINOR_DEVICE_CLASS = 0x05
|
||||
COMPUTER_WEARABLE_COMPUTER_MINOR_DEVICE_CLASS = 0x06
|
||||
COMPUTER_TABLET_MINOR_DEVICE_CLASS = 0x07
|
||||
|
||||
COMPUTER_MINOR_DEVICE_CLASS_NAMES = {
|
||||
COMPUTER_UNCATEGORIZED_MINOR_DEVICE_CLASS: 'Uncategorized',
|
||||
COMPUTER_DESKTOP_WORKSTATION_MINOR_DEVICE_CLASS: 'Desktop workstation',
|
||||
COMPUTER_SERVER_CLASS_COMPUTER_MINOR_DEVICE_CLASS: 'Server-class computer',
|
||||
COMPUTER_LAPTOP_COMPUTER_MINOR_DEVICE_CLASS: 'Laptop',
|
||||
COMPUTER_HANDHELD_PC_PDA_MINOR_DEVICE_CLASS: 'Handheld PC/PDA',
|
||||
COMPUTER_PALM_SIZE_PC_PDA_MINOR_DEVICE_CLASS: 'Palm-size PC/PDA',
|
||||
COMPUTER_WEARABLE_COMPUTER_MINOR_DEVICE_CLASS: 'Wearable computer',
|
||||
COMPUTER_TABLET_MINOR_DEVICE_CLASS: 'Tablet'
|
||||
}
|
||||
|
||||
PHONE_UNCATEGORIZED_MINOR_DEVICE_CLASS = 0x00
|
||||
PHONE_CELLULAR_MINOR_DEVICE_CLASS = 0x01
|
||||
PHONE_CORDLESS_MINOR_DEVICE_CLASS = 0x02
|
||||
PHONE_SMARTPHONE_MINOR_DEVICE_CLASS = 0x03
|
||||
PHONE_WIRED_MODEM_OR_VOICE_GATEWAY_MINOR_DEVICE_CLASS = 0x04
|
||||
PHONE_COMMON_ISDN_MINOR_DEVICE_CLASS = 0x05
|
||||
|
||||
PHONE_MINOR_DEVICE_CLASS_NAMES = {
|
||||
PHONE_UNCATEGORIZED_MINOR_DEVICE_CLASS: 'Uncategorized',
|
||||
PHONE_CELLULAR_MINOR_DEVICE_CLASS: 'Cellular',
|
||||
PHONE_CORDLESS_MINOR_DEVICE_CLASS: 'Cordless',
|
||||
PHONE_SMARTPHONE_MINOR_DEVICE_CLASS: 'Smartphone',
|
||||
PHONE_WIRED_MODEM_OR_VOICE_GATEWAY_MINOR_DEVICE_CLASS: 'Wired modem or voice gateway',
|
||||
PHONE_COMMON_ISDN_MINOR_DEVICE_CLASS: 'Common ISDN access'
|
||||
}
|
||||
|
||||
AUDIO_VIDEO_UNCATEGORIZED_MINOR_DEVICE_CLASS = 0x00
|
||||
AUDIO_VIDEO_WEARABLE_HEADSET_DEVICE_MINOR_DEVICE_CLASS = 0x01
|
||||
AUDIO_VIDEO_HANDS_FREE_DEVICE_MINOR_DEVICE_CLASS = 0x02
|
||||
# (RESERVED) = 0x03
|
||||
AUDIO_VIDEO_MICROPHONE_MINOR_DEVICE_CLASS = 0x04
|
||||
AUDIO_VIDEO_LOUDSPEAKER_MINOR_DEVICE_CLASS = 0x05
|
||||
AUDIO_VIDEO_HEADPHONES_MINOR_DEVICE_CLASS = 0x06
|
||||
AUDIO_VIDEO_PORTABLE_AUDIO_MINOR_DEVICE_CLASS = 0x07
|
||||
AUDIO_VIDEO_CAR_AUDIO_MINOR_DEVICE_CLASS = 0x08
|
||||
AUDIO_VIDEO_SET_TOP_BOX_MINOR_DEVICE_CLASS = 0x09
|
||||
AUDIO_VIDEO_HIFI_AUDIO_DEVICE_MINOR_DEVICE_CLASS = 0x0A
|
||||
AUDIO_VIDEO_VCR_MINOR_DEVICE_CLASS = 0x0B
|
||||
AUDIO_VIDEO_VIDEO_CAMERA_MINOR_DEVICE_CLASS = 0x0C
|
||||
AUDIO_VIDEO_CAMCORDER_MINOR_DEVICE_CLASS = 0x0D
|
||||
AUDIO_VIDEO_VIDEO_MONITOR_MINOR_DEVICE_CLASS = 0x0E
|
||||
AUDIO_VIDEO_VIDEO_DISPLAY_AND_LOUDSPEAKER_MINOR_DEVICE_CLASS = 0x0F
|
||||
AUDIO_VIDEO_VIDEO_CONFERENCING_MINOR_DEVICE_CLASS = 0x10
|
||||
# (RESERVED) = 0x11
|
||||
AUDIO_VIDEO_GAMING_OR_TOY_MINOR_DEVICE_CLASS = 0x12
|
||||
|
||||
AUDIO_VIDEO_MINOR_DEVICE_CLASS_NAMES = {
|
||||
AUDIO_VIDEO_UNCATEGORIZED_MINOR_DEVICE_CLASS: 'Uncategorized',
|
||||
AUDIO_VIDEO_WEARABLE_HEADSET_DEVICE_MINOR_DEVICE_CLASS: 'Wearable Headset Device',
|
||||
AUDIO_VIDEO_HANDS_FREE_DEVICE_MINOR_DEVICE_CLASS: 'Hands-free Device',
|
||||
AUDIO_VIDEO_MICROPHONE_MINOR_DEVICE_CLASS: 'Microphone',
|
||||
AUDIO_VIDEO_LOUDSPEAKER_MINOR_DEVICE_CLASS: 'Loudspeaker',
|
||||
AUDIO_VIDEO_HEADPHONES_MINOR_DEVICE_CLASS: 'Headphones',
|
||||
AUDIO_VIDEO_PORTABLE_AUDIO_MINOR_DEVICE_CLASS: 'Portable Audio',
|
||||
AUDIO_VIDEO_CAR_AUDIO_MINOR_DEVICE_CLASS: 'Car audio',
|
||||
AUDIO_VIDEO_SET_TOP_BOX_MINOR_DEVICE_CLASS: 'Set-top box',
|
||||
AUDIO_VIDEO_HIFI_AUDIO_DEVICE_MINOR_DEVICE_CLASS: 'HiFi Audio Device',
|
||||
AUDIO_VIDEO_VCR_MINOR_DEVICE_CLASS: 'VCR',
|
||||
AUDIO_VIDEO_VIDEO_CAMERA_MINOR_DEVICE_CLASS: 'Video Camera',
|
||||
AUDIO_VIDEO_CAMCORDER_MINOR_DEVICE_CLASS: 'Camcorder',
|
||||
AUDIO_VIDEO_VIDEO_MONITOR_MINOR_DEVICE_CLASS: 'Video Monitor',
|
||||
AUDIO_VIDEO_VIDEO_DISPLAY_AND_LOUDSPEAKER_MINOR_DEVICE_CLASS: 'Video Display and Loudspeaker',
|
||||
AUDIO_VIDEO_VIDEO_CONFERENCING_MINOR_DEVICE_CLASS: 'Video Conferencing',
|
||||
AUDIO_VIDEO_GAMING_OR_TOY_MINOR_DEVICE_CLASS: 'Gaming/Toy'
|
||||
}
|
||||
|
||||
PERIPHERAL_UNCATEGORIZED_MINOR_DEVICE_CLASS = 0x00
|
||||
PERIPHERAL_KEYBOARD_MINOR_DEVICE_CLASS = 0x10
|
||||
PERIPHERAL_POINTING_DEVICE_MINOR_DEVICE_CLASS = 0x20
|
||||
PERIPHERAL_COMBO_KEYBOARD_POINTING_DEVICE_MINOR_DEVICE_CLASS = 0x30
|
||||
PERIPHERAL_JOYSTICK_MINOR_DEVICE_CLASS = 0x01
|
||||
PERIPHERAL_GAMEPAD_MINOR_DEVICE_CLASS = 0x02
|
||||
PERIPHERAL_REMOTE_CONTROL_MINOR_DEVICE_CLASS = 0x03
|
||||
PERIPHERAL_SENSING_DEVICE_MINOR_DEVICE_CLASS = 0x04
|
||||
PERIPHERAL_DIGITIZER_TABLET_MINOR_DEVICE_CLASS = 0x05
|
||||
PERIPHERAL_CARD_READER_MINOR_DEVICE_CLASS = 0x06
|
||||
PERIPHERAL_DIGITAL_PEN_MINOR_DEVICE_CLASS = 0x07
|
||||
PERIPHERAL_HANDHELD_SCANNER_MINOR_DEVICE_CLASS = 0x08
|
||||
PERIPHERAL_HANDHELD_GESTURAL_INPUT_DEVICE_MINOR_DEVICE_CLASS = 0x09
|
||||
|
||||
PERIPHERAL_MINOR_DEVICE_CLASS_NAMES = {
|
||||
PERIPHERAL_UNCATEGORIZED_MINOR_DEVICE_CLASS: 'Uncategorized',
|
||||
PERIPHERAL_KEYBOARD_MINOR_DEVICE_CLASS: 'Keyboard',
|
||||
PERIPHERAL_POINTING_DEVICE_MINOR_DEVICE_CLASS: 'Pointing device',
|
||||
PERIPHERAL_COMBO_KEYBOARD_POINTING_DEVICE_MINOR_DEVICE_CLASS: 'Combo keyboard/pointing device',
|
||||
PERIPHERAL_JOYSTICK_MINOR_DEVICE_CLASS: 'Joystick',
|
||||
PERIPHERAL_GAMEPAD_MINOR_DEVICE_CLASS: 'Gamepad',
|
||||
PERIPHERAL_REMOTE_CONTROL_MINOR_DEVICE_CLASS: 'Remote control',
|
||||
PERIPHERAL_SENSING_DEVICE_MINOR_DEVICE_CLASS: 'Sensing device',
|
||||
PERIPHERAL_DIGITIZER_TABLET_MINOR_DEVICE_CLASS: 'Digitizer tablet',
|
||||
PERIPHERAL_CARD_READER_MINOR_DEVICE_CLASS: 'Card Reader',
|
||||
PERIPHERAL_DIGITAL_PEN_MINOR_DEVICE_CLASS: 'Digital Pen',
|
||||
PERIPHERAL_HANDHELD_SCANNER_MINOR_DEVICE_CLASS: 'Handheld scanner',
|
||||
PERIPHERAL_HANDHELD_GESTURAL_INPUT_DEVICE_MINOR_DEVICE_CLASS: 'Handheld gestural input device'
|
||||
}
|
||||
|
||||
MINOR_DEVICE_CLASS_NAMES = {
|
||||
COMPUTER_MAJOR_DEVICE_CLASS: COMPUTER_MINOR_DEVICE_CLASS_NAMES,
|
||||
PHONE_MAJOR_DEVICE_CLASS: PHONE_MINOR_DEVICE_CLASS_NAMES,
|
||||
AUDIO_VIDEO_MAJOR_DEVICE_CLASS: AUDIO_VIDEO_MINOR_DEVICE_CLASS_NAMES,
|
||||
PERIPHERAL_MAJOR_DEVICE_CLASS: PERIPHERAL_MINOR_DEVICE_CLASS_NAMES
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def split_class_of_device(class_of_device):
|
||||
# Split the bit fields of the composite class of device value into:
|
||||
# (service_classes, major_device_class, minor_device_class)
|
||||
return ((class_of_device >> 13 & 0x7FF), (class_of_device >> 8 & 0x1F), (class_of_device >> 2 & 0x3F))
|
||||
|
||||
@staticmethod
|
||||
def pack_class_of_device(service_classes, major_device_class, minor_device_class):
|
||||
return service_classes << 13 | major_device_class << 8 | minor_device_class << 2
|
||||
|
||||
@staticmethod
|
||||
def service_class_labels(service_class_flags):
|
||||
return bit_flags_to_strings(service_class_flags, DeviceClass.SERVICE_CLASS_LABELS)
|
||||
|
||||
@staticmethod
|
||||
def major_device_class_name(device_class):
|
||||
return name_or_number(DeviceClass.MAJOR_DEVICE_CLASS_NAMES, device_class)
|
||||
|
||||
@staticmethod
|
||||
def minor_device_class_name(major_device_class, minor_device_class):
|
||||
class_names = DeviceClass.MINOR_DEVICE_CLASS_NAMES.get(major_device_class)
|
||||
if class_names is None:
|
||||
return f'#{minor_device_class:02X}'
|
||||
return name_or_number(class_names, minor_device_class)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Advertising Data
|
||||
# -----------------------------------------------------------------------------
|
||||
class AdvertisingData:
|
||||
# This list is only partial, it still needs to be filled in from the spec
|
||||
FLAGS = 0x01
|
||||
INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS = 0x02
|
||||
COMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS = 0x03
|
||||
INCOMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS = 0x04
|
||||
COMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS = 0x05
|
||||
INCOMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS = 0x06
|
||||
COMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS = 0x07
|
||||
SHORTENED_LOCAL_NAME = 0x08
|
||||
COMPLETE_LOCAL_NAME = 0x09
|
||||
TX_POWER_LEVEL = 0x0A
|
||||
CLASS_OF_DEVICE = 0x0D
|
||||
SIMPLE_PAIRING_HASH_C = 0x0E
|
||||
SIMPLE_PAIRING_HASH_C_192 = 0x0E
|
||||
SIMPLE_PAIRING_RANDOMIZER_R = 0x0F
|
||||
SIMPLE_PAIRING_RANDOMIZER_R_192 = 0x0F
|
||||
DEVICE_ID = 0x10
|
||||
SECURITY_MANAGER_TK_VALUE = 0x10
|
||||
SECURITY_MANAGER_OUT_OF_BAND_FLAGS = 0x11
|
||||
PERIPHERAL_CONNECTION_INTERVAL_RANGE = 0x12
|
||||
LIST_OF_16_BIT_SERVICE_SOLICITATION_UUIDS = 0x14
|
||||
LIST_OF_128_BIT_SERVICE_SOLICITATION_UUIDS = 0x15
|
||||
SERVICE_DATA = 0x16
|
||||
SERVICE_DATA_16_BIT_UUID = 0x16
|
||||
PUBLIC_TARGET_ADDRESS = 0x17
|
||||
RANDOM_TARGET_ADDRESS = 0x18
|
||||
APPEARANCE = 0x19
|
||||
ADVERTISING_INTERVAL = 0x1A
|
||||
LE_BLUETOOTH_DEVICE_ADDRESS = 0x1B
|
||||
LE_ROLE = 0x1C
|
||||
SIMPLE_PAIRING_HASH_C_256 = 0x1D
|
||||
SIMPLE_PAIRING_RANDOMIZER_R_256 = 0x1E
|
||||
LIST_OF_32_BIT_SERVICE_SOLICITATION_UUIDS = 0x1F
|
||||
SERVICE_DATA_32_BIT_UUID = 0x20
|
||||
SERVICE_DATA_128_BIT_UUID = 0x21
|
||||
LE_SECURE_CONNECTIONS_CONFIRMATION_VALUE = 0x22
|
||||
LE_SECURE_CONNECTIONS_RANDOM_VALUE = 0x23
|
||||
URI = 0x24
|
||||
INDOOR_POSITIONING = 0x25
|
||||
TRANSPORT_DISCOVERY_DATA = 0x26
|
||||
LE_SUPPORTED_FEATURES = 0x27
|
||||
CHANNEL_MAP_UPDATE_INDICATION = 0x28
|
||||
PB_ADV = 0x29
|
||||
MESH_MESSAGE = 0x2A
|
||||
MESH_BEACON = 0x2B
|
||||
BIGINFO = 0x2C
|
||||
BROADCAST_CODE = 0x2D
|
||||
RESOLVABLE_SET_IDENTIFIER = 0x2E
|
||||
ADVERTISING_INTERVAL_LONG = 0x2F
|
||||
THREE_D_INFORMATION_DATA = 0x3D
|
||||
MANUFACTURER_SPECIFIC_DATA = 0xFF
|
||||
|
||||
AD_TYPE_NAMES = {
|
||||
FLAGS: 'FLAGS',
|
||||
INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS: 'INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS',
|
||||
COMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS: 'COMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS',
|
||||
INCOMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS: 'INCOMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS',
|
||||
COMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS: 'COMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS',
|
||||
INCOMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS: 'INCOMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS',
|
||||
COMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS: 'COMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS',
|
||||
SHORTENED_LOCAL_NAME: 'SHORTENED_LOCAL_NAME',
|
||||
COMPLETE_LOCAL_NAME: 'COMPLETE_LOCAL_NAME',
|
||||
TX_POWER_LEVEL: 'TX_POWER_LEVEL',
|
||||
CLASS_OF_DEVICE: 'CLASS_OF_DEVICE',
|
||||
SIMPLE_PAIRING_HASH_C: 'SIMPLE_PAIRING_HASH_C',
|
||||
SIMPLE_PAIRING_HASH_C_192: 'SIMPLE_PAIRING_HASH_C_192',
|
||||
SIMPLE_PAIRING_RANDOMIZER_R: 'SIMPLE_PAIRING_RANDOMIZER_R',
|
||||
SIMPLE_PAIRING_RANDOMIZER_R_192: 'SIMPLE_PAIRING_RANDOMIZER_R_192',
|
||||
DEVICE_ID: 'DEVICE_ID',
|
||||
SECURITY_MANAGER_TK_VALUE: 'SECURITY_MANAGER_TK_VALUE',
|
||||
SECURITY_MANAGER_OUT_OF_BAND_FLAGS: 'SECURITY_MANAGER_OUT_OF_BAND_FLAGS',
|
||||
PERIPHERAL_CONNECTION_INTERVAL_RANGE: 'PERIPHERAL_CONNECTION_INTERVAL_RANGE',
|
||||
LIST_OF_16_BIT_SERVICE_SOLICITATION_UUIDS: 'LIST_OF_16_BIT_SERVICE_SOLICITATION_UUIDS',
|
||||
LIST_OF_128_BIT_SERVICE_SOLICITATION_UUIDS: 'LIST_OF_128_BIT_SERVICE_SOLICITATION_UUIDS',
|
||||
SERVICE_DATA: 'SERVICE_DATA',
|
||||
SERVICE_DATA_16_BIT_UUID: 'SERVICE_DATA_16_BIT_UUID',
|
||||
PUBLIC_TARGET_ADDRESS: 'PUBLIC_TARGET_ADDRESS',
|
||||
RANDOM_TARGET_ADDRESS: 'RANDOM_TARGET_ADDRESS',
|
||||
APPEARANCE: 'APPEARANCE',
|
||||
ADVERTISING_INTERVAL: 'ADVERTISING_INTERVAL',
|
||||
LE_BLUETOOTH_DEVICE_ADDRESS: 'LE_BLUETOOTH_DEVICE_ADDRESS',
|
||||
LE_ROLE: 'LE_ROLE',
|
||||
SIMPLE_PAIRING_HASH_C_256: 'SIMPLE_PAIRING_HASH_C_256',
|
||||
SIMPLE_PAIRING_RANDOMIZER_R_256: 'SIMPLE_PAIRING_RANDOMIZER_R_256',
|
||||
LIST_OF_32_BIT_SERVICE_SOLICITATION_UUIDS: 'LIST_OF_32_BIT_SERVICE_SOLICITATION_UUIDS',
|
||||
SERVICE_DATA_32_BIT_UUID: 'SERVICE_DATA_32_BIT_UUID',
|
||||
SERVICE_DATA_128_BIT_UUID: 'SERVICE_DATA_128_BIT_UUID',
|
||||
LE_SECURE_CONNECTIONS_CONFIRMATION_VALUE: 'LE_SECURE_CONNECTIONS_CONFIRMATION_VALUE',
|
||||
LE_SECURE_CONNECTIONS_RANDOM_VALUE: 'LE_SECURE_CONNECTIONS_RANDOM_VALUE',
|
||||
URI: 'URI',
|
||||
INDOOR_POSITIONING: 'INDOOR_POSITIONING',
|
||||
TRANSPORT_DISCOVERY_DATA: 'TRANSPORT_DISCOVERY_DATA',
|
||||
LE_SUPPORTED_FEATURES: 'LE_SUPPORTED_FEATURES',
|
||||
CHANNEL_MAP_UPDATE_INDICATION: 'CHANNEL_MAP_UPDATE_INDICATION',
|
||||
PB_ADV: 'PB_ADV',
|
||||
MESH_MESSAGE: 'MESH_MESSAGE',
|
||||
MESH_BEACON: 'MESH_BEACON',
|
||||
BIGINFO: 'BIGINFO',
|
||||
BROADCAST_CODE: 'BROADCAST_CODE',
|
||||
RESOLVABLE_SET_IDENTIFIER: 'RESOLVABLE_SET_IDENTIFIER',
|
||||
ADVERTISING_INTERVAL_LONG: 'ADVERTISING_INTERVAL_LONG',
|
||||
THREE_D_INFORMATION_DATA: 'THREE_D_INFORMATION_DATA',
|
||||
MANUFACTURER_SPECIFIC_DATA: 'MANUFACTURER_SPECIFIC_DATA'
|
||||
}
|
||||
|
||||
LE_LIMITED_DISCOVERABLE_MODE_FLAG = 0x01
|
||||
LE_GENERAL_DISCOVERABLE_MODE_FLAG = 0x02
|
||||
BR_EDR_NOT_SUPPORTED_FLAG = 0x04
|
||||
BR_EDR_CONTROLLER_FLAG = 0x08
|
||||
BR_EDR_HOST_FLAG = 0x10
|
||||
|
||||
def __init__(self, ad_structures = []):
|
||||
self.ad_structures = ad_structures[:]
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(data):
|
||||
instance = AdvertisingData()
|
||||
instance.append(data)
|
||||
return instance
|
||||
|
||||
@staticmethod
|
||||
def flags_to_string(flags, short=False):
|
||||
flag_names = [
|
||||
'LE Limited',
|
||||
'LE General',
|
||||
'No BR/EDR',
|
||||
'BR/EDR C',
|
||||
'BR/EDR H'
|
||||
] if short else [
|
||||
'LE Limited Discoverable Mode',
|
||||
'LE General Discoverable Mode',
|
||||
'BR/EDR Not Supported',
|
||||
'Simultaneous LE and BR/EDR (Controller)',
|
||||
'Simultaneous LE and BR/EDR (Host)'
|
||||
]
|
||||
return ','.join(bit_flags_to_strings(flags, flag_names))
|
||||
|
||||
@staticmethod
|
||||
def uuid_list_to_objects(ad_data, uuid_size):
|
||||
uuids = []
|
||||
offset = 0
|
||||
while (uuid_size * (offset + 1)) <= len(ad_data):
|
||||
uuids.append(UUID.from_bytes(ad_data[offset:offset + uuid_size]))
|
||||
offset += uuid_size
|
||||
return uuids
|
||||
|
||||
@staticmethod
|
||||
def uuid_list_to_string(ad_data, uuid_size):
|
||||
return ', '.join([
|
||||
str(uuid)
|
||||
for uuid in AdvertisingData.uuid_list_to_objects(ad_data, uuid_size)
|
||||
])
|
||||
|
||||
@staticmethod
|
||||
def ad_data_to_string(ad_type, ad_data):
|
||||
if ad_type == AdvertisingData.FLAGS:
|
||||
ad_type_str = 'Flags'
|
||||
ad_data_str = AdvertisingData.flags_to_string(ad_data[0], short=True)
|
||||
elif ad_type == AdvertisingData.COMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS:
|
||||
ad_type_str = 'Complete List of 16-bit Service Class UUIDs'
|
||||
ad_data_str = AdvertisingData.uuid_list_to_string(ad_data, 2)
|
||||
elif ad_type == AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS:
|
||||
ad_type_str = 'Incomplete List of 16-bit Service Class UUIDs'
|
||||
ad_data_str = AdvertisingData.uuid_list_to_string(ad_data, 2)
|
||||
elif ad_type == AdvertisingData.COMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS:
|
||||
ad_type_str = 'Complete List of 32-bit Service Class UUIDs'
|
||||
ad_data_str = AdvertisingData.uuid_list_to_string(ad_data, 4)
|
||||
elif ad_type == AdvertisingData.INCOMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS:
|
||||
ad_type_str = 'Incomplete List of 32-bit Service Class UUIDs'
|
||||
ad_data_str = AdvertisingData.uuid_list_to_string(ad_data, 4)
|
||||
elif ad_type == AdvertisingData.COMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS:
|
||||
ad_type_str = 'Complete List of 128-bit Service Class UUIDs'
|
||||
ad_data_str = AdvertisingData.uuid_list_to_string(ad_data, 16)
|
||||
elif ad_type == AdvertisingData.INCOMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS:
|
||||
ad_type_str = 'Incomplete List of 128-bit Service Class UUIDs'
|
||||
ad_data_str = AdvertisingData.uuid_list_to_string(ad_data, 16)
|
||||
elif ad_type == AdvertisingData.SERVICE_DATA_16_BIT_UUID:
|
||||
ad_type_str = 'Service Data'
|
||||
uuid = UUID.from_bytes(ad_data[:2])
|
||||
ad_data_str = f'service={uuid}, data={ad_data[2:].hex()}'
|
||||
elif ad_type == AdvertisingData.SERVICE_DATA_32_BIT_UUID:
|
||||
ad_type_str = 'Service Data'
|
||||
uuid = UUID.from_bytes(ad_data[:4])
|
||||
ad_data_str = f'service={uuid}, data={ad_data[4:].hex()}'
|
||||
elif ad_type == AdvertisingData.SERVICE_DATA_128_BIT_UUID:
|
||||
ad_type_str = 'Service Data'
|
||||
uuid = UUID.from_bytes(ad_data[:16])
|
||||
ad_data_str = f'service={uuid}, data={ad_data[16:].hex()}'
|
||||
elif ad_type == AdvertisingData.SHORTENED_LOCAL_NAME:
|
||||
ad_type_str = 'Shortened Local Name'
|
||||
ad_data_str = f'"{ad_data.decode("utf-8")}"'
|
||||
elif ad_type == AdvertisingData.COMPLETE_LOCAL_NAME:
|
||||
ad_type_str = 'Complete Local Name'
|
||||
ad_data_str = f'"{ad_data.decode("utf-8")}"'
|
||||
elif ad_type == AdvertisingData.TX_POWER_LEVEL:
|
||||
ad_type_str = 'TX Power Level'
|
||||
ad_data_str = str(ad_data[0])
|
||||
elif ad_type == AdvertisingData.MANUFACTURER_SPECIFIC_DATA:
|
||||
ad_type_str = 'Manufacturer Specific Data'
|
||||
company_id = struct.unpack_from('<H', ad_data, 0)[0]
|
||||
company_name = COMPANY_IDENTIFIERS.get(company_id, f'0x{company_id:04X}')
|
||||
ad_data_str = f'company={company_name}, data={ad_data[2:].hex()}'
|
||||
elif ad_type == AdvertisingData.APPEARANCE:
|
||||
ad_type_str = 'Appearance'
|
||||
ad_data_str = ad_data.hex()
|
||||
else:
|
||||
ad_type_str = AdvertisingData.AD_TYPE_NAMES.get(ad_type, f'0x{ad_type:02X}')
|
||||
ad_data_str = ad_data.hex()
|
||||
|
||||
return f'[{ad_type_str}]: {ad_data_str}'
|
||||
|
||||
@staticmethod
|
||||
def ad_data_to_object(ad_type, ad_data):
|
||||
if ad_type in {
|
||||
AdvertisingData.COMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
|
||||
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS
|
||||
}:
|
||||
return AdvertisingData.uuid_list_to_objects(ad_data, 2)
|
||||
elif ad_type in {
|
||||
AdvertisingData.COMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS,
|
||||
AdvertisingData.INCOMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS
|
||||
}:
|
||||
return AdvertisingData.uuid_list_to_objects(ad_data, 4)
|
||||
elif ad_type in {
|
||||
AdvertisingData.COMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS,
|
||||
AdvertisingData.INCOMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS
|
||||
}:
|
||||
return AdvertisingData.uuid_list_to_objects(ad_data, 16)
|
||||
elif 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:
|
||||
return (UUID.from_bytes(ad_data[:4]), ad_data[4:])
|
||||
elif ad_type == AdvertisingData.SERVICE_DATA_128_BIT_UUID:
|
||||
return (UUID.from_bytes(ad_data[:16]), ad_data[16:])
|
||||
elif ad_type in {
|
||||
AdvertisingData.SHORTENED_LOCAL_NAME,
|
||||
AdvertisingData.COMPLETE_LOCAL_NAME
|
||||
}:
|
||||
return ad_data.decode("utf-8")
|
||||
elif ad_type == AdvertisingData.TX_POWER_LEVEL:
|
||||
return ad_data[0]
|
||||
elif ad_type == AdvertisingData.MANUFACTURER_SPECIFIC_DATA:
|
||||
return (struct.unpack_from('<H', ad_data, 0)[0], ad_data[2:])
|
||||
else:
|
||||
return ad_data
|
||||
|
||||
def append(self, data):
|
||||
offset = 0
|
||||
while offset + 1 < len(data):
|
||||
length = data[offset]
|
||||
offset += 1
|
||||
if length > 0:
|
||||
ad_type = data[offset]
|
||||
ad_data = data[offset + 1:offset + length]
|
||||
self.ad_structures.append((ad_type, ad_data))
|
||||
offset += length
|
||||
|
||||
def get(self, type_id, return_all=False, raw=True):
|
||||
'''
|
||||
Get Advertising Data Structure(s) with a given type
|
||||
|
||||
If return_all is True, returns a (possibly empty) list of matches,
|
||||
else returns the first entry, or None if no structure matches.
|
||||
'''
|
||||
def process_ad_data(ad_data):
|
||||
return ad_data if raw else self.ad_data_to_object(type_id, ad_data)
|
||||
|
||||
if return_all:
|
||||
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)
|
||||
|
||||
def __bytes__(self):
|
||||
return b''.join([bytes([len(x[1]) + 1, x[0]]) + x[1] for x in self.ad_structures])
|
||||
|
||||
def to_string(self, separator=', '):
|
||||
return separator.join([AdvertisingData.ad_data_to_string(x[0], x[1]) for x in self.ad_structures])
|
||||
|
||||
def __str__(self):
|
||||
return self.to_string()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Connection Parameters
|
||||
# -----------------------------------------------------------------------------
|
||||
class ConnectionParameters:
|
||||
def __init__(self, connection_interval, connection_latency, supervision_timeout):
|
||||
self.connection_interval = connection_interval
|
||||
self.connection_latency = connection_latency
|
||||
self.supervision_timeout = supervision_timeout
|
||||
|
||||
def __str__(self):
|
||||
return f'ConnectionParameters(connection_interval={self.connection_interval}, connection_latency={self.connection_latency}, supervision_timeout={self.supervision_timeout}'
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Connection PHY
|
||||
# -----------------------------------------------------------------------------
|
||||
class ConnectionPHY:
|
||||
def __init__(self, tx_phy, rx_phy):
|
||||
self.tx_phy = tx_phy
|
||||
self.rx_phy = rx_phy
|
||||
|
||||
def __str__(self):
|
||||
return f'ConnectionPHY(tx_phy={self.tx_phy}, rx_phy={self.rx_phy})'
|
||||
229
bumble/crypto.py
Normal file
229
bumble/crypto.py
Normal file
@@ -0,0 +1,229 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Crypto support
|
||||
#
|
||||
# See Bluetooth spec Vol 3, Part H - 2.2 CRYPTOGRAPHIC TOOLBOX
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import logging
|
||||
import operator
|
||||
import platform
|
||||
if platform.system() != 'Emscripten':
|
||||
import secrets
|
||||
from cryptography.hazmat.primitives.ciphers import (
|
||||
Cipher,
|
||||
algorithms,
|
||||
modes
|
||||
)
|
||||
from cryptography.hazmat.primitives.asymmetric.ec import (
|
||||
generate_private_key,
|
||||
ECDH,
|
||||
EllipticCurvePublicNumbers,
|
||||
EllipticCurvePrivateNumbers,
|
||||
SECP256R1
|
||||
)
|
||||
from cryptography.hazmat.primitives import cmac
|
||||
else:
|
||||
# TODO: implement stubs
|
||||
pass
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Classes
|
||||
# -----------------------------------------------------------------------------
|
||||
class EccKey:
|
||||
def __init__(self, private_key):
|
||||
self.private_key = private_key
|
||||
|
||||
@classmethod
|
||||
def generate(cls):
|
||||
private_key = generate_private_key(SECP256R1())
|
||||
return cls(private_key)
|
||||
|
||||
@classmethod
|
||||
def from_private_key_bytes(cls, d_bytes, x_bytes, y_bytes):
|
||||
d = int.from_bytes(d_bytes, byteorder='big', signed=False)
|
||||
x = int.from_bytes(x_bytes, byteorder='big', signed=False)
|
||||
y = int.from_bytes(y_bytes, byteorder='big', signed=False)
|
||||
private_key = EllipticCurvePrivateNumbers(d, EllipticCurvePublicNumbers(x, y, SECP256R1())).private_key()
|
||||
return cls(private_key)
|
||||
|
||||
@property
|
||||
def x(self):
|
||||
return self.private_key.public_key().public_numbers().x.to_bytes(32, byteorder='big')
|
||||
|
||||
@property
|
||||
def y(self):
|
||||
return self.private_key.public_key().public_numbers().y.to_bytes(32, byteorder='big')
|
||||
|
||||
def dh(self, public_key_x, public_key_y):
|
||||
x = int.from_bytes(public_key_x, byteorder='big', signed=False)
|
||||
y = int.from_bytes(public_key_y, byteorder='big', signed=False)
|
||||
public_key = EllipticCurvePublicNumbers(x, y, SECP256R1()).public_key()
|
||||
shared_key = self.private_key.exchange(ECDH(), public_key)
|
||||
|
||||
return shared_key
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Functions
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def xor(x, y):
|
||||
assert(len(x) == len(y))
|
||||
return bytes(map(operator.xor, x, y))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def r():
|
||||
'''
|
||||
Generate 16 bytes of random data
|
||||
'''
|
||||
return secrets.token_bytes(16)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def e(key, data):
|
||||
'''
|
||||
AES-128 ECB, expecting byte-swapped inputs and producing a byte-swapped output.
|
||||
|
||||
See Bluetooth spec Vol 3, Part H - 2.2.1 Security function e
|
||||
'''
|
||||
|
||||
cipher = Cipher(algorithms.AES(bytes(reversed(key))), modes.ECB())
|
||||
encryptor = cipher.encryptor()
|
||||
return bytes(reversed(encryptor.update(bytes(reversed(data)))))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def ah(k, r):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part H - 2.2.2 Random Address Hash function ah
|
||||
'''
|
||||
|
||||
padding = bytes(13)
|
||||
r_prime = r + padding
|
||||
return e(k, r_prime)[0:3]
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def c1(k, r, preq, pres, iat, rat, ia, ra):
|
||||
'''
|
||||
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
|
||||
p2 = ra + ia + bytes([0, 0, 0, 0])
|
||||
return e(k, xor(e(k, xor(r, p1)), p2))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def s1(k, r1, r2):
|
||||
'''
|
||||
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])
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def aes_cmac(m, k):
|
||||
'''
|
||||
See Bluetooth spec, Vol 3, Part H - 2.2.5 FunctionAES-CMAC
|
||||
|
||||
NOTE: the input and output of this internal function are in big-endian byte order
|
||||
'''
|
||||
mac = cmac.CMAC(algorithms.AES(k))
|
||||
mac.update(m)
|
||||
return mac.finalize()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def f4(u, v, x, z):
|
||||
'''
|
||||
See Bluetooth spec, Vol 3, Part H - 2.2.6 LE Secure Connections Confirm Value Generation Function f4
|
||||
'''
|
||||
return bytes(reversed(aes_cmac(bytes(reversed(u)) + bytes(reversed(v)) + z, bytes(reversed(x)))))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def f5(w, n1, n2, a1, a2):
|
||||
'''
|
||||
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
|
||||
'''
|
||||
salt = bytes.fromhex('6C888391AAF5A53860370BDB5A6083BE')
|
||||
t = aes_cmac(bytes(reversed(w)), salt)
|
||||
key_id = bytes([0x62, 0x74, 0x6c, 0x65])
|
||||
return (
|
||||
bytes(reversed(aes_cmac(
|
||||
bytes([0]) +
|
||||
key_id +
|
||||
bytes(reversed(n1)) +
|
||||
bytes(reversed(n2)) +
|
||||
bytes(reversed(a1)) +
|
||||
bytes(reversed(a2)) +
|
||||
bytes([1, 0]),
|
||||
t
|
||||
))),
|
||||
bytes(reversed(aes_cmac(
|
||||
bytes([1]) +
|
||||
key_id +
|
||||
bytes(reversed(n1)) +
|
||||
bytes(reversed(n2)) +
|
||||
bytes(reversed(a1)) +
|
||||
bytes(reversed(a2)) +
|
||||
bytes([1, 0]),
|
||||
t
|
||||
)))
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def f6(w, n1, n2, r, io_cap, a1, a2):
|
||||
'''
|
||||
See Bluetooth spec, Vol 3, Part H - 2.2.8 LE Secure Connections Check Value Generation Function f6
|
||||
'''
|
||||
return bytes(reversed(aes_cmac(
|
||||
bytes(reversed(n1)) +
|
||||
bytes(reversed(n2)) +
|
||||
bytes(reversed(r)) +
|
||||
bytes(reversed(io_cap)) +
|
||||
bytes(reversed(a1)) +
|
||||
bytes(reversed(a2)),
|
||||
bytes(reversed(w))
|
||||
)))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
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
|
||||
'''
|
||||
return int.from_bytes(
|
||||
aes_cmac(bytes(reversed(u)) + bytes(reversed(v)) + bytes(reversed(y)), bytes(reversed(x)))[-4:],
|
||||
byteorder='big'
|
||||
)
|
||||
1256
bumble/device.py
Normal file
1256
bumble/device.py
Normal file
File diff suppressed because it is too large
Load Diff
59
bumble/gap.py
Normal file
59
bumble/gap.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import logging
|
||||
import struct
|
||||
|
||||
from .gatt import (
|
||||
Service,
|
||||
Characteristic,
|
||||
GATT_GENERIC_ACCESS_SERVICE,
|
||||
GATT_DEVICE_NAME_CHARACTERISTIC,
|
||||
GATT_APPEARANCE_CHARACTERISTIC
|
||||
)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Classes
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class GenericAccessService(Service):
|
||||
def __init__(self, device_name, appearance = (0, 0)):
|
||||
device_name_characteristic = Characteristic(
|
||||
GATT_DEVICE_NAME_CHARACTERISTIC,
|
||||
Characteristic.READ,
|
||||
Characteristic.READABLE,
|
||||
device_name.encode('utf-8')[:248]
|
||||
)
|
||||
|
||||
appearance_characteristic = Characteristic(
|
||||
GATT_APPEARANCE_CHARACTERISTIC,
|
||||
Characteristic.READ,
|
||||
Characteristic.READABLE,
|
||||
struct.pack('<H', (appearance[0] << 6) | appearance[1])
|
||||
)
|
||||
|
||||
super().__init__(GATT_GENERIC_ACCESS_SERVICE, [
|
||||
device_name_characteristic,
|
||||
appearance_characteristic
|
||||
])
|
||||
308
bumble/gatt.py
Normal file
308
bumble/gatt.py
Normal file
@@ -0,0 +1,308 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# GATT - Generic Attribute Profile
|
||||
#
|
||||
# See Bluetooth spec @ Vol 3, Part G
|
||||
#
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import logging
|
||||
from colors import color
|
||||
|
||||
from .core import *
|
||||
from .hci import *
|
||||
from .att import *
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
GATT_REQUEST_TIMEOUT = 30 # seconds
|
||||
|
||||
GATT_MAX_ATTRIBUTE_VALUE_SIZE = 512
|
||||
|
||||
# Services
|
||||
GATT_GENERIC_ACCESS_SERVICE = UUID.from_16_bits(0x1800, 'Generic Access')
|
||||
GATT_GENERIC_ATTRIBUTE_SERVICE = UUID.from_16_bits(0x1801, 'Generic Attribute')
|
||||
GATT_IMMEDIATE_ALERT_SERVICE = UUID.from_16_bits(0x1802, 'Immediate Alert')
|
||||
GATT_LINK_LOSS_SERVICE = UUID.from_16_bits(0x1803, 'Link Loss')
|
||||
GATT_TX_POWER_SERVICE = UUID.from_16_bits(0x1804, 'TX Power')
|
||||
GATT_CURRENT_TIME_SERVICE = UUID.from_16_bits(0x1805, 'Current Time')
|
||||
GATT_REFERENCE_TIME_UPDATE_SERVICE = UUID.from_16_bits(0x1806, 'Reference Time Update')
|
||||
GATT_NEXT_DST_CHANGE_SERVICE = UUID.from_16_bits(0x1807, 'Next DST Change')
|
||||
GATT_GLUCOSE_SERVICE = UUID.from_16_bits(0x1808, 'Glucose')
|
||||
GATT_HEALTH_THERMOMETER_SERVICE = UUID.from_16_bits(0x1809, 'Health Thermometer')
|
||||
GATT_DEVICE_INFORMATION_SERVICE = UUID.from_16_bits(0x180A, 'Device Information')
|
||||
GATT_DEVICE_HEART_RATE_SERVICE = UUID.from_16_bits(0x180D, 'Heart Rate')
|
||||
GATT_PHONE_ALTERT_STATUS_SERVICE = UUID.from_16_bits(0x180E, 'Phone Alert Status')
|
||||
GATT_DEVICE_BATTERY_SERVICE = UUID.from_16_bits(0x180F, 'Battery')
|
||||
GATT_BLOOD_PRESSURE_SERVICE = UUID.from_16_bits(0x1810, 'Blood Pressure')
|
||||
GATT_ALTERT_NOTIFICATION_SERVICE = UUID.from_16_bits(0x1811, 'Alert Notification')
|
||||
GATT_DEVICE_HUMAN_INTERFACE_DEVICE_SERVICE = UUID.from_16_bits(0x1812, 'Human Interface Device')
|
||||
GATT_DEVICE_SCAN_PARAMETERS_SERVICE = UUID.from_16_bits(0x1813, 'Scan Parameters')
|
||||
GATT_RUNNING_SPEED_AND_CADENCE_SERVICE = UUID.from_16_bits(0x1814, 'Running Speed and Cadence')
|
||||
GATT_AUTOMATION_IO_SERVICE = UUID.from_16_bits(0x1815, 'Automation IO')
|
||||
GATT_CYCLING_SPEED_AND_CADENCE_SERVICE = UUID.from_16_bits(0x1816, 'Cycling Speed and Cadence')
|
||||
GATT_CYCLING_POWER_SERVICE = UUID.from_16_bits(0x1818, 'Cycling Power')
|
||||
GATT_LOCATION_AND_NAVIGATION_SERVICE = UUID.from_16_bits(0x1819, 'Location and Navigation')
|
||||
GATT_ENVIRONMENTAL_SENSING_SERVICE = UUID.from_16_bits(0x181A, 'Environmental Sensing')
|
||||
GATT_BODY_COMPOSITION_SERVICE = UUID.from_16_bits(0x181B, 'Body Composition')
|
||||
GATT_USER_DATA_SERVICE = UUID.from_16_bits(0x181C, 'User Data')
|
||||
GATT_WEIGHT_SCALE_SERVICE = UUID.from_16_bits(0x181D, 'Weight Scale')
|
||||
GATT_BOND_MANAGEMENT_SERVICE = UUID.from_16_bits(0x181E, 'Bond Management')
|
||||
GATT_CONTINUOUS_GLUCOSE_MONITORING_SERVICE = UUID.from_16_bits(0x181F, 'Continuous Glucose Monitoring')
|
||||
GATT_INTERNET_PROTOCOL_SUPPORT_SERVICE = UUID.from_16_bits(0x1820, 'Internet Protocol Support')
|
||||
GATT_INDOOR_POSITIONING_SERVICE = UUID.from_16_bits(0x1821, 'Indoor Positioning')
|
||||
GATT_PULSE_OXIMETER_SERVICE = UUID.from_16_bits(0x1822, 'Pulse Oximeter')
|
||||
GATT_HTTP_PROXY_SERVICE = UUID.from_16_bits(0x1823, 'HTTP Proxy')
|
||||
GATT_TRANSPORT_DISCOVERY_SERVICE = UUID.from_16_bits(0x1824, 'Transport Discovery')
|
||||
GATT_OBJECT_TRANSFER_SERVICE = UUID.from_16_bits(0x1825, 'Object Transfer')
|
||||
GATT_FITNESS_MACHINE_SERVICE = UUID.from_16_bits(0x1826, 'Fitness Machine')
|
||||
GATT_MESH_PROVISIONING_SERVICE = UUID.from_16_bits(0x1827, 'Mesh Provisioning')
|
||||
GATT_MESH_PROXY_SERVICE = UUID.from_16_bits(0x1828, 'Mesh Proxy')
|
||||
GATT_RECONNECTION_CONFIGURATION_SERVICE = UUID.from_16_bits(0x1829, 'Reconnection Configuration')
|
||||
GATT_INSULIN_DELIVERY_SERVICE = UUID.from_16_bits(0x183A, 'Insulin Delivery')
|
||||
GATT_BINARY_SENSOR_SERVICE = UUID.from_16_bits(0x183B, 'Binary Sensor')
|
||||
GATT_EMERGENCY_CONFIGURATION_SERVICE = UUID.from_16_bits(0x183C, 'Emergency Configuration')
|
||||
GATT_PHYSICAL_ACTIVITY_MONITOR_SERVICE = UUID.from_16_bits(0x183E, 'Physical Activity Monitor')
|
||||
GATT_AUDIO_INPUT_CONTROL_SERVICE = UUID.from_16_bits(0x1843, 'Audio Input Control')
|
||||
GATT_VOLUME_CONTROL_SERVICE = UUID.from_16_bits(0x1844, 'Volume Control')
|
||||
GATT_VOLUME_OFFSET_CONTROL_SERVICE = UUID.from_16_bits(0x1845, 'Volume Offset Control')
|
||||
GATT_COORDINATED_SET_IDENTIFICATION_SERVICE = UUID.from_16_bits(0x1846, 'Coordinated Set Identification Service')
|
||||
GATT_DEVICE_TIME_SERVICE = UUID.from_16_bits(0x1847, 'Device Time')
|
||||
GATT_MEDIA_CONTROL_SERVICE = UUID.from_16_bits(0x1848, 'Media Control Service')
|
||||
GATT_GENERIC_MEDIA_CONTROL_SERVICE = UUID.from_16_bits(0x1849, 'Generic Media Control Service')
|
||||
GATT_CONSTANT_TONE_EXTENSION_SERVICE = UUID.from_16_bits(0x184A, 'Constant Tone Extension')
|
||||
GATT_TELEPHONE_BEARER_SERVICE = UUID.from_16_bits(0x184B, 'Telephone Bearer Service')
|
||||
GATT_GENERIC_TELEPHONE_BEARER_SERVICE = UUID.from_16_bits(0x184C, 'Generic Telephone Bearer Service')
|
||||
GATT_MICROPHONE_CONTROL_SERVICE = UUID.from_16_bits(0x184D, 'Microphone Control')
|
||||
|
||||
# Types
|
||||
GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE = UUID.from_16_bits(0x2800, 'Primary Service')
|
||||
GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE = UUID.from_16_bits(0x2801, 'Secondary Service')
|
||||
GATT_INCLUDE_ATTRIBUTE_TYPE = UUID.from_16_bits(0x2802, 'Include')
|
||||
GATT_CHARACTERISTIC_ATTRIBUTE_TYPE = UUID.from_16_bits(0x2803, 'Characteristic')
|
||||
|
||||
# Descriptors
|
||||
GATT_CHARACTERISTIC_EXTENDED_PROPERTIES_DESCRIPTOR = UUID.from_16_bits(0x2900, 'Characteristic Extended Properties')
|
||||
GATT_CHARACTERISTIC_USER_DESCRIPTION_DESCRIPTOR = UUID.from_16_bits(0x2901, 'Characteristic User Description')
|
||||
GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR = UUID.from_16_bits(0x2902, 'Client Characteristic Configuration')
|
||||
GATT_SERVER_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR = UUID.from_16_bits(0x2903, 'Server Characteristic Configuration')
|
||||
GATT_CHARACTERISTIC_PRESENTATION_FORMAT_DESCRIPTOR = UUID.from_16_bits(0x2904, 'Characteristic Format')
|
||||
GATT_CHARACTERISTIC_AGGREGATE_FORMAT_DESCRIPTOR = UUID.from_16_bits(0x2905, 'Characteristic Aggregate Format')
|
||||
GATT_VALID_RANGE_DESCRIPTOR = UUID.from_16_bits(0x2906, 'Valid Range')
|
||||
GATT_EXTERNAL_REPORT_DESCRIPTOR = UUID.from_16_bits(0x2907, 'External Report')
|
||||
GATT_REPORT_REFERENCE_DESCRIPTOR = UUID.from_16_bits(0x2908, 'Report Reference')
|
||||
GATT_NUMBER_OF_DIGITALS_DESCRIPTOR = UUID.from_16_bits(0x2909, 'Number of Digitals')
|
||||
GATT_VALUE_TRIGGER_SETTING_DESCRIPTOR = UUID.from_16_bits(0x290A, 'Value Trigger Setting')
|
||||
GATT_ENVIRONMENTAL_SENSING_CONFIGURATION_DESCRIPTOR = UUID.from_16_bits(0x290B, 'Environmental Sensing Configuration')
|
||||
GATT_ENVIRONMENTAL_SENSING_MEASUREMENT_DESCRIPTOR = UUID.from_16_bits(0x290C, 'Environmental Sensing Measurement')
|
||||
GATT_ENVIRONMENTAL_SENSING_TRIGGER_DESCRIPTOR = UUID.from_16_bits(0x290D, 'Environmental Sensing Trigger Setting')
|
||||
GATT_TIME_TRIGGER_DESCRIPTOR = UUID.from_16_bits(0x290E, 'Time Trigger Setting')
|
||||
GATT_COMPLETE_BE_EDR_TRANSPORT_BLOCK_DATA_DESCRIPTOR = UUID.from_16_bits(0x290F, 'Complete BR-EDR Transport Block Data')
|
||||
|
||||
# Device Information Service
|
||||
GATT_SYSTEM_ID_CHARACTERISTIC = UUID.from_16_bits(0x2A23, 'System ID')
|
||||
GATT_MODEL_NUMBER_STRING_CHARACTERISTIC = UUID.from_16_bits(0x2A24, 'Model Number String')
|
||||
GATT_SERIAL_NUMBER_STRING_CHARACTERISTIC = UUID.from_16_bits(0x2A25, 'Serial Number String')
|
||||
GATT_FIRMWARE_REVISION_STRING_CHARACTERISTIC = UUID.from_16_bits(0x2A26, 'Firmware Revision String')
|
||||
GATT_HARDWARE_REVISION_STRING_CHARACTERISTIC = UUID.from_16_bits(0x2A27, 'Hardware Revision String')
|
||||
GATT_SOFTWARE_REVISION_STRING_CHARACTERISTIC = UUID.from_16_bits(0x2A28, 'Software Revision String')
|
||||
GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC = UUID.from_16_bits(0x2A29, 'Manufacturer Name String')
|
||||
GATT_REGULATORY_CERTIFICATION_DATA_LIST_CHARACTERISTIC = UUID.from_16_bits(0x2A2A, 'IEEE 11073-20601 Regulatory Certification Data List')
|
||||
GATT_PNP_ID_CHARACTERISTIC = UUID.from_16_bits(0x2A50, 'PnP ID')
|
||||
|
||||
# Human Interface Device
|
||||
GATT_HID_INFORMATION_CHARACTERISTIC = UUID.from_16_bits(0x2A4A, 'HID Information')
|
||||
GATT_REPORT_MAP_CHARACTERISTIC = UUID.from_16_bits(0x2A4B, 'Report Map')
|
||||
GATT_HID_CONTROL_POINT_CHARACTERISTIC = UUID.from_16_bits(0x2A4C, 'HID Control Point')
|
||||
GATT_REPORT_CHARACTERISTIC = UUID.from_16_bits(0x2A4D, 'Report')
|
||||
GATT_PROTOCOL_MODE_CHARACTERISTIC = UUID.from_16_bits(0x2A4E, 'Protocol Mode')
|
||||
|
||||
# Misc
|
||||
GATT_DEVICE_NAME_CHARACTERISTIC = UUID.from_16_bits(0x2A00, 'Device Name')
|
||||
GATT_APPEARANCE_CHARACTERISTIC = UUID.from_16_bits(0x2A01, 'Appearance')
|
||||
GATT_PERIPHERAL_PRIVACY_FLAG_CHARACTERISTIC = UUID.from_16_bits(0x2A02, 'Peripheral Privacy Flag')
|
||||
GATT_RECONNECTION_ADDRESS_CHARACTERISTIC = UUID.from_16_bits(0x2A03, 'Reconnection Address')
|
||||
GATT_PERIPHERAL_PREFERRREED_CONNECTION_PARAMETERS_CHARACTERISTIC = UUID.from_16_bits(0x2A04, 'Peripheral Preferred Connection Parameters')
|
||||
GATT_SERVICE_CHANGED_CHARACTERISTIC = UUID.from_16_bits(0x2A05, 'Service Changed')
|
||||
GATT_ALERT_LEVEL_CHARACTERISTIC = UUID.from_16_bits(0x2A06, 'Alert Level')
|
||||
GATT_TX_POWER_LEVEL_CHARACTERISTIC = UUID.from_16_bits(0x2A07, 'Tx Power Level')
|
||||
GATT_BATTERY_LEVEL_CHARACTERISTIC = UUID.from_16_bits(0x2A19, 'Battery Level')
|
||||
GATT_BOOT_KEYBOARD_INPUT_REPORT_CHARACTERISTIC = UUID.from_16_bits(0x2A22, 'Boot Keyboard Input Report')
|
||||
GATT_CURRENT_TIME_CHARACTERISTIC = UUID.from_16_bits(0x2A2B, 'Current Time')
|
||||
GATT_BOOT_KEYBOARD_OUTPUT_REPORT_CHARACTERISTIC = UUID.from_16_bits(0x2A32, 'Boot Keyboard Output Report')
|
||||
GATT_CENTRAL_ADDRESS_RESOLUTION__CHARACTERISTIC = UUID.from_16_bits(0x2AA6, 'Central Address Resolution')
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Utils
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
def show_services(services):
|
||||
for service in services:
|
||||
print(color(str(service), 'cyan'))
|
||||
|
||||
for characteristic in service.characteristics:
|
||||
print(color(' ' + str(characteristic), 'magenta'))
|
||||
|
||||
for descriptor in characteristic.descriptors:
|
||||
print(color(' ' + str(descriptor), 'green'))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Service(Attribute):
|
||||
'''
|
||||
See Vol 3, Part G - 3.1 SERVICE DEFINITION
|
||||
'''
|
||||
|
||||
def __init__(self, uuid, characteristics, primary=True):
|
||||
# Convert the uuid to a UUID object if it isn't already
|
||||
if type(uuid) is str:
|
||||
uuid = UUID(uuid)
|
||||
|
||||
super().__init__(
|
||||
GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE if primary else GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE,
|
||||
Attribute.READABLE,
|
||||
uuid.to_pdu_bytes()
|
||||
)
|
||||
self.uuid = uuid
|
||||
self.included_services = []
|
||||
self.characteristics = characteristics[:]
|
||||
self.end_group_handle = 0
|
||||
self.primary = primary
|
||||
|
||||
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 "*"}'
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Characteristic(Attribute):
|
||||
'''
|
||||
See Vol 3, Part G - 3.3 CHARACTERISTIC DEFINITION
|
||||
'''
|
||||
|
||||
# Property flags
|
||||
BROADCAST = 0x01
|
||||
READ = 0x02
|
||||
WRITE_WITHOUT_RESPONSE = 0x04
|
||||
WRITE = 0x08
|
||||
NOTIFY = 0x10
|
||||
INDICATE = 0X20
|
||||
AUTHENTICATED_SIGNED_WRITES = 0X40
|
||||
EXTENDED_PROPERTIES = 0X80
|
||||
|
||||
PROPERTY_NAMES = {
|
||||
BROADCAST: 'BROADCAST',
|
||||
READ: 'READ',
|
||||
WRITE_WITHOUT_RESPONSE: 'WRITE_WITHOUT_RESPONSE',
|
||||
WRITE: 'WRITE',
|
||||
NOTIFY: 'NOTIFY',
|
||||
INDICATE: 'INDICATE',
|
||||
AUTHENTICATED_SIGNED_WRITES: 'AUTHENTICATED_SIGNED_WRITES',
|
||||
EXTENDED_PROPERTIES: 'EXTENDED_PROPERTIES'
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def property_name(property):
|
||||
return Characteristic.PROPERTY_NAMES.get(property, '')
|
||||
|
||||
def __init__(self, uuid, properties, permissions, value = b'', descriptors = []):
|
||||
# Convert the uuid to a UUID object if it isn't already
|
||||
if type(uuid) is str:
|
||||
uuid = UUID(uuid)
|
||||
|
||||
super().__init__(uuid, permissions, value)
|
||||
self.uuid = uuid
|
||||
self.properties = properties
|
||||
self._descriptors = descriptors
|
||||
self._descriptors_discovered = False
|
||||
self.end_group_handle = 0
|
||||
self.attach_descriptors()
|
||||
|
||||
def attach_descriptors(self):
|
||||
""" Let all the descriptors know they are attached to this characteristic """
|
||||
for descriptor in self._descriptors:
|
||||
descriptor.characteristic = self
|
||||
|
||||
def add_descriptor(self, descriptor):
|
||||
descriptor.characteristic = self
|
||||
self.descriptors.append(descriptor)
|
||||
|
||||
def get_descriptor(self, descriptor_type):
|
||||
for descriptor in self.descriptors:
|
||||
if descriptor.uuid == descriptor_type:
|
||||
return descriptor
|
||||
|
||||
@property
|
||||
def descriptors(self):
|
||||
return self._descriptors
|
||||
|
||||
@descriptors.setter
|
||||
def descriptors(self, value):
|
||||
self._descriptors = value
|
||||
self._descriptors_discovered = True
|
||||
self.attach_descriptors()
|
||||
|
||||
@property
|
||||
def descriptors_discovered(self):
|
||||
return self._descriptors_discovered
|
||||
|
||||
def get_properties_as_string(self):
|
||||
return ','.join([self.property_name(p) for p in self.PROPERTY_NAMES.keys() if self.properties & p])
|
||||
|
||||
def __str__(self):
|
||||
return f'Characteristic(handle=0x{self.handle:04X}, end=0x{self.end_group_handle:04X}, uuid={self.uuid}, properties={self.get_properties_as_string()})'
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class CharacteristicValue:
|
||||
def __init__(self, read=None, write=None):
|
||||
self._read = read
|
||||
self._write = write
|
||||
|
||||
def read(self, connection):
|
||||
return self._read(connection) if self._read else b''
|
||||
|
||||
def write(self, connection, value):
|
||||
if self._write:
|
||||
self._write(connection, value)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Descriptor(Attribute):
|
||||
'''
|
||||
See Vol 3, Part G - 3.3.3 Characteristic Descriptor Declarations
|
||||
'''
|
||||
|
||||
def __init__(self, uuid, permissions, value = b''):
|
||||
# Convert the uuid to a UUID object if it isn't already
|
||||
if type(uuid) is str:
|
||||
uuid = UUID(uuid)
|
||||
|
||||
super().__init__(uuid, permissions, value)
|
||||
self.uuid = uuid
|
||||
self.characteristic = None
|
||||
|
||||
def __str__(self):
|
||||
return f'Descriptor(handle=0x{self.handle:04X}, uuid={self.uuid}, value={self.read_value(None).hex()})'
|
||||
645
bumble/gatt_client.py
Normal file
645
bumble/gatt_client.py
Normal file
@@ -0,0 +1,645 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# GATT - Generic Attribute Profile
|
||||
# Client
|
||||
#
|
||||
# See Bluetooth spec @ Vol 3, Part G
|
||||
#
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import logging
|
||||
import struct
|
||||
from colors import color
|
||||
|
||||
from .core import ProtocolError, TimeoutError
|
||||
from .hci import *
|
||||
from .att import *
|
||||
from .gatt import (
|
||||
GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
|
||||
GATT_REQUEST_TIMEOUT,
|
||||
GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
|
||||
GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
|
||||
Service,
|
||||
Characteristic,
|
||||
Descriptor
|
||||
)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# GATT Client
|
||||
# -----------------------------------------------------------------------------
|
||||
class Client:
|
||||
def __init__(self, connection):
|
||||
self.connection = connection
|
||||
self.mtu = ATT_DEFAULT_MTU
|
||||
self.mtu_exchange_done = False
|
||||
self.request_semaphore = asyncio.Semaphore(1)
|
||||
self.pending_request = None
|
||||
self.pending_response = None
|
||||
self.notification_subscribers = {} # Notification subscribers, by attribute handle
|
||||
self.indication_subscribers = {} # Indication subscribers, by attribute handle
|
||||
self.services = []
|
||||
|
||||
def send_gatt_pdu(self, pdu):
|
||||
self.connection.send_l2cap_pdu(ATT_CID, pdu)
|
||||
|
||||
async def send_command(self, command):
|
||||
logger.debug(f'GATT Command from client: [0x{self.connection.handle:04X}] {command}')
|
||||
self.send_gatt_pdu(command.to_bytes())
|
||||
|
||||
async def send_request(self, request):
|
||||
logger.debug(f'GATT Request from client: [0x{self.connection.handle:04X}] {request}')
|
||||
|
||||
# Wait until we can send (only one pending command at a time for the connection)
|
||||
response = None
|
||||
async with self.request_semaphore:
|
||||
assert(self.pending_request is None)
|
||||
assert(self.pending_response is None)
|
||||
|
||||
# Create a future value to hold the eventual response
|
||||
self.pending_response = asyncio.get_running_loop().create_future()
|
||||
self.pending_request = request
|
||||
|
||||
try:
|
||||
self.send_gatt_pdu(request.to_bytes())
|
||||
response = await asyncio.wait_for(self.pending_response, GATT_REQUEST_TIMEOUT)
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning(color('!!! GATT Request timeout', 'red'))
|
||||
raise TimeoutError(f'GATT timeout for {request.name}')
|
||||
finally:
|
||||
self.pending_request = None
|
||||
self.pending_response = None
|
||||
|
||||
return response
|
||||
|
||||
def send_confirmation(self, confirmation):
|
||||
logger.debug(f'GATT Confirmation from client: [0x{self.connection.handle:04X}] {confirmation}')
|
||||
self.send_gatt_pdu(confirmation.to_bytes())
|
||||
|
||||
async def request_mtu(self, mtu):
|
||||
# Check the range
|
||||
if mtu < ATT_DEFAULT_MTU:
|
||||
raise ValueError(f'MTU must be >= {ATT_DEFAULT_MTU}')
|
||||
if mtu > 0xFFFF:
|
||||
raise ValueError('MTU must be <= 0xFFFF')
|
||||
|
||||
# We can only send one request per connection
|
||||
if self.mtu_exchange_done:
|
||||
return
|
||||
|
||||
# Send the request
|
||||
self.mtu_exchange_done = True
|
||||
response = await self.send_request(ATT_Exchange_MTU_Request(client_rx_mtu = mtu))
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
raise ProtocolError(
|
||||
response.error_code,
|
||||
'att',
|
||||
ATT_PDU.error_name(response.error_code),
|
||||
response
|
||||
)
|
||||
|
||||
self.mtu = max(ATT_DEFAULT_MTU, response.server_rx_mtu)
|
||||
return self.mtu
|
||||
|
||||
def get_services_by_uuid(self, uuid):
|
||||
return [service for service in self.services if service.uuid == uuid]
|
||||
|
||||
def get_characteristics_by_uuid(self, uuid, service = None):
|
||||
services = [service] if service else self.services
|
||||
return [c for c in [c for s in services for c in s.characteristics] if c.uuid == uuid]
|
||||
|
||||
def on_service_discovered(self, service):
|
||||
''' Add a service to the service list if it wasn't already there '''
|
||||
already_known = False
|
||||
for existing_service in self.services:
|
||||
if existing_service.handle == service.handle:
|
||||
already_known = True
|
||||
break
|
||||
if not already_known:
|
||||
self.services.append(service)
|
||||
|
||||
async def discover_services(self, uuids = None):
|
||||
'''
|
||||
See Vol 3, Part G - 4.4.1 Discover All Primary Services
|
||||
'''
|
||||
starting_handle = 0x0001
|
||||
services = []
|
||||
while starting_handle < 0xFFFF:
|
||||
response = await self.send_request(
|
||||
ATT_Read_By_Group_Type_Request(
|
||||
starting_handle = starting_handle,
|
||||
ending_handle = 0xFFFF,
|
||||
attribute_group_type = GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE
|
||||
)
|
||||
)
|
||||
if response is None:
|
||||
# TODO raise appropriate exception
|
||||
return []
|
||||
|
||||
# Check if we reached the end of the iteration
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
# Unexpected end
|
||||
logger.waning(f'!!! unexpected error while discovering services: {HCI_Constant.error_name(response.error_code)}')
|
||||
# TODO raise appropriate exception
|
||||
return
|
||||
break
|
||||
|
||||
for attribute_handle, end_group_handle, attribute_value in response.attributes:
|
||||
if attribute_handle < starting_handle or end_group_handle < attribute_handle:
|
||||
# Something's not right
|
||||
logger.warning(f'bogus handle values: {attribute_handle} {end_group_handle}')
|
||||
return
|
||||
|
||||
# Create a primary service object
|
||||
service = Service(UUID.from_bytes(attribute_value), [], True)
|
||||
service.handle = attribute_handle
|
||||
service.end_group_handle = end_group_handle
|
||||
|
||||
# Filter out returned services based on the given uuids list
|
||||
if (not uuids) or (service.uuid in uuids):
|
||||
services.append(service)
|
||||
|
||||
# Add the service to the peer's service list
|
||||
self.on_service_discovered(service)
|
||||
|
||||
# Stop if for some reason the list was empty
|
||||
if not response.attributes:
|
||||
break
|
||||
|
||||
# Move on to the next chunk
|
||||
starting_handle = response.attributes[-1][1] + 1
|
||||
|
||||
return services
|
||||
|
||||
async def discover_service(self, uuid):
|
||||
'''
|
||||
See Vol 3, Part G - 4.4.2 Discover Primary Service by Service UUID
|
||||
'''
|
||||
|
||||
# Force uuid to be a UUID object
|
||||
if type(uuid) is str:
|
||||
uuid = UUID(uuid)
|
||||
|
||||
starting_handle = 0x0001
|
||||
services = []
|
||||
while starting_handle < 0xFFFF:
|
||||
response = await self.send_request(
|
||||
ATT_Find_By_Type_Value_Request(
|
||||
starting_handle = starting_handle,
|
||||
ending_handle = 0xFFFF,
|
||||
attribute_type = GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
|
||||
attribute_value = uuid.to_pdu_bytes()
|
||||
)
|
||||
)
|
||||
if response is None:
|
||||
# TODO raise appropriate exception
|
||||
return []
|
||||
|
||||
# Check if we reached the end of the iteration
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR:
|
||||
# Unexpected end
|
||||
logger.waning(f'!!! unexpected error while discovering services: {HCI_Constant.error_name(response.error_code)}')
|
||||
# TODO raise appropriate exception
|
||||
return
|
||||
break
|
||||
|
||||
for attribute_handle, end_group_handle in response.handles_information:
|
||||
if attribute_handle < starting_handle or end_group_handle < attribute_handle:
|
||||
# Something's not right
|
||||
logger.warning(f'bogus handle values: {attribute_handle} {end_group_handle}')
|
||||
return
|
||||
|
||||
# Create a primary service object
|
||||
service = Service(uuid, [], True)
|
||||
service.handle = attribute_handle
|
||||
service.end_group_handle = end_group_handle
|
||||
|
||||
# Add the service to the peer's service list
|
||||
services.append(service)
|
||||
self.on_service_discovered(service)
|
||||
|
||||
# Check if we've reached the end already
|
||||
if end_group_handle == 0xFFFF:
|
||||
break
|
||||
|
||||
# Stop if for some reason the list was empty
|
||||
if not response.handles_information:
|
||||
break
|
||||
|
||||
# Move on to the next chunk
|
||||
starting_handle = response.handles_information[-1][1] + 1
|
||||
|
||||
return services
|
||||
|
||||
async def discover_included_services(self, service):
|
||||
'''
|
||||
See Vol 3, Part G - 4.5.1 Find Included Services
|
||||
'''
|
||||
# TODO
|
||||
return []
|
||||
|
||||
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
|
||||
'''
|
||||
|
||||
# Cast the UUIDs type from string to object if needed
|
||||
uuids = [UUID(uuid) if type(uuid) is str else uuid for uuid in uuids]
|
||||
|
||||
# Decide which services to discover for
|
||||
services = [service] if service else self.services
|
||||
|
||||
# Perform characteristic discovery for each service
|
||||
discovered_characteristics = []
|
||||
for service in services:
|
||||
starting_handle = service.handle
|
||||
ending_handle = service.end_group_handle
|
||||
|
||||
characteristics = []
|
||||
while starting_handle <= ending_handle:
|
||||
response = await self.send_request(
|
||||
ATT_Read_By_Type_Request(
|
||||
starting_handle = starting_handle,
|
||||
ending_handle = ending_handle,
|
||||
attribute_type = GATT_CHARACTERISTIC_ATTRIBUTE_TYPE
|
||||
)
|
||||
)
|
||||
if response is None:
|
||||
# TODO raise appropriate exception
|
||||
return []
|
||||
|
||||
# Check if we reached the end of the iteration
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
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)}')
|
||||
# TODO raise appropriate exception
|
||||
return
|
||||
break
|
||||
|
||||
# Stop if for some reason the list was empty
|
||||
if not response.attributes:
|
||||
break
|
||||
|
||||
# Process all characteristics returned in this iteration
|
||||
for attribute_handle, attribute_value in response.attributes:
|
||||
if attribute_handle < starting_handle:
|
||||
# Something's not right
|
||||
logger.warning(f'bogus handle value: {attribute_handle}')
|
||||
return []
|
||||
|
||||
properties, handle = struct.unpack_from('<BH', attribute_value)
|
||||
characteristic_uuid = UUID.from_bytes(attribute_value[3:])
|
||||
characteristic = Characteristic(characteristic_uuid, properties, 0)
|
||||
characteristic.handle = handle
|
||||
|
||||
# Set the previous characteristic's end handle
|
||||
if characteristics:
|
||||
characteristics[-1].end_group_handle = attribute_handle - 1
|
||||
|
||||
characteristics.append(characteristic)
|
||||
|
||||
# Move on to the next characteristics
|
||||
starting_handle = response.attributes[-1][0] + 1
|
||||
|
||||
# Set the end handle for the last characteristic
|
||||
if characteristics:
|
||||
characteristics[-1].end_group_handle = service.end_group_handle
|
||||
|
||||
# Set the service's characteristics
|
||||
characteristics = [c for c in characteristics if not uuids or c.uuid in uuids]
|
||||
service.characteristics = characteristics
|
||||
discovered_characteristics.extend(characteristics)
|
||||
|
||||
return discovered_characteristics
|
||||
|
||||
async def discover_descriptors(self, characteristic = None, start_handle = None, end_handle = None):
|
||||
'''
|
||||
See Vol 3, Part G - 4.7.1 Discover All Characteristic Descriptors
|
||||
'''
|
||||
if characteristic:
|
||||
starting_handle = characteristic.handle + 1
|
||||
ending_handle = characteristic.end_group_handle
|
||||
elif start_handle and end_handle:
|
||||
starting_handle = start_handle
|
||||
ending_handle = end_handle
|
||||
else:
|
||||
return []
|
||||
|
||||
descriptors = []
|
||||
while starting_handle <= ending_handle:
|
||||
response = await self.send_request(
|
||||
ATT_Find_Information_Request(
|
||||
starting_handle = starting_handle,
|
||||
ending_handle = ending_handle
|
||||
)
|
||||
)
|
||||
if response is None:
|
||||
# TODO raise appropriate exception
|
||||
return []
|
||||
|
||||
# Check if we reached the end of the iteration
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
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)}')
|
||||
# TODO raise appropriate exception
|
||||
return []
|
||||
break
|
||||
|
||||
# Stop if for some reason the list was empty
|
||||
if not response.information:
|
||||
break
|
||||
|
||||
# Process all descriptors returned in this iteration
|
||||
for attribute_handle, attribute_uuid in response.information:
|
||||
if attribute_handle < starting_handle:
|
||||
# Something's not right
|
||||
logger.warning(f'bogus handle value: {attribute_handle}')
|
||||
return []
|
||||
|
||||
descriptor = Descriptor(UUID.from_bytes(attribute_uuid), 0)
|
||||
descriptor.handle = attribute_handle
|
||||
descriptors.append(descriptor)
|
||||
# TODO: read descriptor value
|
||||
|
||||
# Move on to the next descriptor
|
||||
starting_handle = response.information[-1][0] + 1
|
||||
|
||||
# Set the characteristic's descriptors
|
||||
if characteristic:
|
||||
characteristic.descriptors = descriptors
|
||||
|
||||
return descriptors
|
||||
|
||||
async def discover_attributes(self):
|
||||
'''
|
||||
Discover all attributes, regardless of type
|
||||
'''
|
||||
starting_handle = 0x0001
|
||||
ending_handle = 0xFFFF
|
||||
attributes = []
|
||||
while True:
|
||||
response = await self.send_request(
|
||||
ATT_Find_Information_Request(
|
||||
starting_handle = starting_handle,
|
||||
ending_handle = ending_handle
|
||||
)
|
||||
)
|
||||
if response is None:
|
||||
return []
|
||||
|
||||
# Check if we reached the end of the iteration
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
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)}')
|
||||
return []
|
||||
break
|
||||
|
||||
for attribute_handle, attribute_uuid in response.information:
|
||||
if attribute_handle < starting_handle:
|
||||
# Something's not right
|
||||
logger.warning(f'bogus handle value: {attribute_handle}')
|
||||
return []
|
||||
|
||||
attribute = Attribute(attribute_uuid, 0)
|
||||
attribute.handle = attribute_handle
|
||||
attributes.append(attribute)
|
||||
|
||||
# Move on to the next attributes
|
||||
starting_handle = attributes[-1].handle + 1
|
||||
|
||||
return attributes
|
||||
|
||||
async def subscribe(self, characteristic, subscriber=None):
|
||||
# If we haven't already discovered the descriptors for this characteristic, do it now
|
||||
if not characteristic.descriptors_discovered:
|
||||
await self.discover_descriptors(characteristic)
|
||||
|
||||
# Look for the CCCD descriptor
|
||||
cccd = characteristic.get_descriptor(GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR)
|
||||
if not cccd:
|
||||
logger.warning('subscribing to characteristic with no CCCD descriptor')
|
||||
return
|
||||
|
||||
# Set the subscription bits and select the subscriber set
|
||||
bits = 0
|
||||
subscriber_sets = []
|
||||
if characteristic.properties & Characteristic.NOTIFY:
|
||||
bits |= 0x0001
|
||||
subscriber_sets.append(self.notification_subscribers.setdefault(characteristic.handle, set()))
|
||||
if characteristic.properties & Characteristic.INDICATE:
|
||||
bits |= 0x0002
|
||||
subscriber_sets.append(self.indication_subscribers.setdefault(characteristic.handle, set()))
|
||||
|
||||
# Add subscribers to the sets
|
||||
for subscriber_set in subscriber_sets:
|
||||
if subscriber is not None:
|
||||
subscriber_set.add(subscriber)
|
||||
subscriber_set.add(lambda value: characteristic.emit('update', self.connection, value))
|
||||
|
||||
await self.write_value(cccd, struct.pack('<H', bits), with_response=True)
|
||||
|
||||
async def read_value(self, attribute, no_long_read=False):
|
||||
'''
|
||||
See Vol 3, Part G - 4.8.1 Read Characteristic Value
|
||||
|
||||
`attribute` can be an Attribute object, or a handle value
|
||||
'''
|
||||
|
||||
# Send a request to read
|
||||
attribute_handle = attribute if type(attribute) is int else attribute.handle
|
||||
response = await self.send_request(ATT_Read_Request(attribute_handle = attribute_handle))
|
||||
if response is None:
|
||||
raise TimeoutError('read timeout')
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
raise ProtocolError(
|
||||
response.error_code,
|
||||
'att',
|
||||
ATT_PDU.error_name(response.error_code),
|
||||
response
|
||||
)
|
||||
|
||||
# If the value is the max size for the MTU, try to read more unless the caller
|
||||
# specifically asked not to do that
|
||||
attribute_value = response.attribute_value
|
||||
if not no_long_read and len(attribute_value) == self.mtu - 1:
|
||||
logger.debug('using READ BLOB to get the rest of the value')
|
||||
offset = len(attribute_value)
|
||||
while True:
|
||||
response = await self.send_request(
|
||||
ATT_Read_Blob_Request(attribute_handle = attribute_handle, value_offset = offset)
|
||||
)
|
||||
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:
|
||||
break
|
||||
raise ProtocolError(
|
||||
response.error_code,
|
||||
'att',
|
||||
ATT_PDU.error_name(response.error_code),
|
||||
response
|
||||
)
|
||||
|
||||
part = response.part_attribute_value
|
||||
attribute_value += part
|
||||
|
||||
if len(part) < self.mtu - 1:
|
||||
break
|
||||
|
||||
offset += len(part)
|
||||
|
||||
# Return the value as bytes
|
||||
return attribute_value
|
||||
|
||||
async def read_characteristics_by_uuid(self, uuid, service):
|
||||
'''
|
||||
See Vol 3, Part G - 4.8.2 Read Using Characteristic UUID
|
||||
'''
|
||||
|
||||
if service is None:
|
||||
starting_handle = 0x0001
|
||||
ending_handle = 0xFFFF
|
||||
else:
|
||||
starting_handle = service.handle
|
||||
ending_handle = service.end_group_handle
|
||||
|
||||
characteristics_values = []
|
||||
while starting_handle <= ending_handle:
|
||||
response = await self.send_request(
|
||||
ATT_Read_By_Type_Request(
|
||||
starting_handle = starting_handle,
|
||||
ending_handle = ending_handle,
|
||||
attribute_type = uuid
|
||||
)
|
||||
)
|
||||
if response is None:
|
||||
# TODO raise appropriate exception
|
||||
return []
|
||||
|
||||
# Check if we reached the end of the iteration
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
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)}')
|
||||
# TODO raise appropriate exception
|
||||
return []
|
||||
break
|
||||
|
||||
# Stop if for some reason the list was empty
|
||||
if not response.attributes:
|
||||
break
|
||||
|
||||
# Process all characteristics returned in this iteration
|
||||
for attribute_handle, attribute_value in response.attributes:
|
||||
if attribute_handle < starting_handle:
|
||||
# Something's not right
|
||||
logger.warning(f'bogus handle value: {attribute_handle}')
|
||||
return []
|
||||
|
||||
characteristics_values.append(attribute_value)
|
||||
|
||||
# Move on to the next characteristics
|
||||
starting_handle = response.attributes[-1][0] + 1
|
||||
|
||||
return characteristics_values
|
||||
|
||||
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
|
||||
|
||||
`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
|
||||
if with_response:
|
||||
response = await self.send_request(
|
||||
ATT_Write_Request(
|
||||
attribute_handle = attribute_handle,
|
||||
attribute_value = value
|
||||
)
|
||||
)
|
||||
if response.op_code == ATT_ERROR_RESPONSE:
|
||||
raise ProtocolError(
|
||||
response.error_code,
|
||||
'att',
|
||||
ATT_PDU.error_name(response.error_code), response
|
||||
)
|
||||
else:
|
||||
await self.send_command(
|
||||
ATT_Write_Command(
|
||||
attribute_handle = attribute_handle,
|
||||
attribute_value = value
|
||||
)
|
||||
)
|
||||
|
||||
def on_gatt_pdu(self, att_pdu):
|
||||
logger.debug(f'GATT Response to client: [0x{self.connection.handle:04X}] {att_pdu}')
|
||||
if att_pdu.op_code in ATT_RESPONSES:
|
||||
if self.pending_request is None:
|
||||
# Not expected!
|
||||
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
|
||||
if att_pdu.op_code != ATT_ERROR_RESPONSE:
|
||||
expected_response_name = self.pending_request.name.replace('_REQUEST', '_RESPONSE')
|
||||
if att_pdu.name != expected_response_name:
|
||||
logger.warning(f'!!! mismatched response: expected {expected_response_name}')
|
||||
return
|
||||
|
||||
# Return the response to the coroutine that is waiting for it
|
||||
self.pending_response.set_result(att_pdu)
|
||||
else:
|
||||
handler_name = f'on_{att_pdu.name.lower()}'
|
||||
handler = getattr(self, handler_name, None)
|
||||
if handler is not None:
|
||||
handler(att_pdu)
|
||||
else:
|
||||
logger.warning(f'{color(f"--- Ignoring GATT Response from [0x{self.connection.handle:04X}]:", "red")} {att_pdu}')
|
||||
|
||||
def on_att_handle_value_notification(self, notification):
|
||||
# Call all subscribers
|
||||
subscribers = self.notification_subscribers.get(notification.attribute_handle, [])
|
||||
if not subscribers:
|
||||
logger.warning('!!! received notification with no subscriber')
|
||||
for subscriber in subscribers:
|
||||
subscriber(notification.attribute_value)
|
||||
|
||||
def on_att_handle_value_indication(self, indication):
|
||||
# Call all subscribers
|
||||
subscribers = self.indication_subscribers.get(indication.attribute_handle, [])
|
||||
if not subscribers:
|
||||
logger.warning('!!! received indication with no subscriber')
|
||||
for subscriber in subscribers:
|
||||
subscriber(indication.attribute_value)
|
||||
|
||||
# Confirm that we received the indication
|
||||
self.send_confirmation(ATT_Handle_Value_Confirmation())
|
||||
698
bumble/gatt_server.py
Normal file
698
bumble/gatt_server.py
Normal file
@@ -0,0 +1,698 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# GATT - Generic Attribute Profile
|
||||
# Server
|
||||
#
|
||||
# See Bluetooth spec @ Vol 3, Part G
|
||||
#
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from pyee import EventEmitter
|
||||
from colors import color
|
||||
|
||||
from .core import *
|
||||
from .hci import *
|
||||
from .att import *
|
||||
from .gatt import *
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# GATT Server
|
||||
# -----------------------------------------------------------------------------
|
||||
class Server(EventEmitter):
|
||||
def __init__(self, device):
|
||||
super().__init__()
|
||||
self.device = device
|
||||
self.attributes = [] # Attributes, ordered by increasing handle values
|
||||
self.attributes_by_handle = {} # Map for fast attribute access by handle
|
||||
self.max_mtu = 23 # FIXME: 517 # The max MTU we're willing to negotiate
|
||||
self.subscribers = {} # Map of subscriber states by connection handle and attribute handle
|
||||
self.mtus = {} # Map of ATT MTU values by connection handle
|
||||
self.indication_semaphores = defaultdict(lambda: asyncio.Semaphore(1))
|
||||
self.pending_confirmations = defaultdict(lambda: None)
|
||||
|
||||
def send_gatt_pdu(self, connection_handle, pdu):
|
||||
self.device.send_l2cap_pdu(connection_handle, ATT_CID, pdu)
|
||||
|
||||
def next_handle(self):
|
||||
return 1 + len(self.attributes)
|
||||
|
||||
def get_attribute(self, handle):
|
||||
attribute = self.attributes_by_handle.get(handle)
|
||||
if attribute:
|
||||
return attribute
|
||||
|
||||
# Not in the cached map, perform a linear lookup
|
||||
for attribute in self.attributes:
|
||||
if attribute.handle == handle:
|
||||
# Store in cached map
|
||||
self.attributes_by_handle[handle] = attribute
|
||||
return attribute
|
||||
return None
|
||||
|
||||
def add_attribute(self, attribute):
|
||||
# Assign a handle to this attribute
|
||||
attribute.handle = self.next_handle()
|
||||
attribute.end_group_handle = attribute.handle # TODO: keep track of descriptors in the group
|
||||
|
||||
# Add this attribute to the list
|
||||
self.attributes.append(attribute)
|
||||
|
||||
def add_service(self, service):
|
||||
# Add the service attribute to the DB
|
||||
self.add_attribute(service)
|
||||
|
||||
# TODO: add included services
|
||||
|
||||
# Add all characteristics
|
||||
for characteristic in service.characteristics:
|
||||
# Add a Characteristic Declaration (Vol 3, Part G - 3.3.1 Characteristic Declaration)
|
||||
declaration_bytes = struct.pack(
|
||||
'<BH',
|
||||
characteristic.properties,
|
||||
self.next_handle() + 1, # The value will be the next attribute after this declaration
|
||||
) + characteristic.uuid.to_pdu_bytes()
|
||||
characteristic_declaration = Attribute(
|
||||
GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
|
||||
Attribute.READABLE,
|
||||
declaration_bytes
|
||||
)
|
||||
self.add_attribute(characteristic_declaration)
|
||||
|
||||
# Add the characteristic value
|
||||
self.add_attribute(characteristic)
|
||||
|
||||
# Add the descriptors
|
||||
for descriptor in characteristic.descriptors:
|
||||
self.add_attribute(descriptor)
|
||||
|
||||
# If the characteristic supports subscriptions, add a CCCD descriptor
|
||||
# unless there is one already
|
||||
if (
|
||||
characteristic.properties & (Characteristic.NOTIFY | Characteristic.INDICATE) and
|
||||
characteristic.get_descriptor(GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR) is None
|
||||
):
|
||||
self.add_attribute(
|
||||
Descriptor(
|
||||
GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
|
||||
Attribute.READABLE | Attribute.WRITEABLE,
|
||||
CharacteristicValue(
|
||||
read=lambda connection, characteristic=characteristic: self.read_cccd(connection, characteristic),
|
||||
write=lambda connection, value, characteristic=characteristic: self.write_cccd(connection, characteristic, value)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# Update the service and characteristic group ends
|
||||
characteristic_declaration.end_group_handle = self.attributes[-1].handle
|
||||
characteristic.end_group_handle = self.attributes[-1].handle
|
||||
|
||||
# Update the service group end
|
||||
service.end_group_handle = self.attributes[-1].handle
|
||||
|
||||
def add_services(self, services):
|
||||
for service in services:
|
||||
self.add_service(service)
|
||||
|
||||
def read_cccd(self, connection, characteristic):
|
||||
if connection is None:
|
||||
return bytes([0, 0])
|
||||
|
||||
subscribers = self.subscribers.get(connection.handle)
|
||||
cccd = None
|
||||
if subscribers:
|
||||
cccd = subscribers.get(characteristic.handle)
|
||||
|
||||
return cccd or bytes([0, 0])
|
||||
|
||||
def write_cccd(self, connection, characteristic, value):
|
||||
logger.debug(f'Subscription update for connection={connection.handle:04X}, handle={characteristic.handle:04X}: {value.hex()}')
|
||||
|
||||
# Sanity check
|
||||
if len(value) != 2:
|
||||
logger.warn('CCCD value not 2 bytes long')
|
||||
return
|
||||
|
||||
cccds = self.subscribers.setdefault(connection.handle, {})
|
||||
cccds[characteristic.handle] = value
|
||||
logger.debug(f'CCCDs: {cccds}')
|
||||
notify_enabled = (value[0] & 0x01 != 0)
|
||||
indicate_enabled = (value[0] & 0x02 != 0)
|
||||
characteristic.emit('subscription', connection, notify_enabled, indicate_enabled)
|
||||
self.emit('characteristic_subscription', connection, characteristic, notify_enabled, indicate_enabled)
|
||||
|
||||
def send_response(self, connection, response):
|
||||
logger.debug(f'GATT Response from server: [0x{connection.handle:04X}] {response}')
|
||||
self.send_gatt_pdu(connection.handle, response.to_bytes())
|
||||
|
||||
async def notify_subscriber(self, connection, attribute, force=False):
|
||||
# Check if there's a subscriber
|
||||
if not force:
|
||||
subscribers = self.subscribers.get(connection.handle)
|
||||
if not subscribers:
|
||||
logger.debug('not notifying, no subscribers')
|
||||
return
|
||||
cccd = subscribers.get(attribute.handle)
|
||||
if not cccd:
|
||||
logger.debug(f'not notifying, no subscribers for handle {attribute.handle:04X}')
|
||||
return
|
||||
if len(cccd) != 2 or (cccd[0] & 0x01 == 0):
|
||||
logger.debug(f'not notifying, cccd={cccd.hex()}')
|
||||
return
|
||||
|
||||
# Get the value
|
||||
value = attribute.read_value(connection)
|
||||
|
||||
# Truncate if needed
|
||||
mtu = self.get_mtu(connection)
|
||||
if len(value) > mtu - 3:
|
||||
value = value[:mtu - 3]
|
||||
|
||||
# Notify
|
||||
notification = ATT_Handle_Value_Notification(
|
||||
attribute_handle = attribute.handle,
|
||||
attribute_value = value
|
||||
)
|
||||
logger.debug(f'GATT Notify from server: [0x{connection.handle:04X}] {notification}')
|
||||
self.send_gatt_pdu(connection.handle, notification.to_bytes())
|
||||
|
||||
async def notify_subscribers(self, attribute, force=False):
|
||||
# Get all the connections for which there's at least one subscription
|
||||
connections = [
|
||||
connection for connection in [
|
||||
self.device.lookup_connection(connection_handle)
|
||||
for (connection_handle, subscribers) in self.subscribers.items()
|
||||
if force or subscribers.get(attribute.handle)
|
||||
]
|
||||
if connection is not None
|
||||
]
|
||||
|
||||
# Notify for each connection
|
||||
if connections:
|
||||
await asyncio.wait([
|
||||
self.notify_subscriber(connection, attribute, force)
|
||||
for connection in connections
|
||||
])
|
||||
|
||||
async def indicate_subscriber(self, connection, attribute, force=False):
|
||||
# Check if there's a subscriber
|
||||
if not force:
|
||||
subscribers = self.subscribers.get(connection.handle)
|
||||
if not subscribers:
|
||||
logger.debug('not indicating, no subscribers')
|
||||
return
|
||||
cccd = subscribers.get(attribute.handle)
|
||||
if not cccd:
|
||||
logger.debug(f'not indicating, no subscribers for handle {attribute.handle:04X}')
|
||||
return
|
||||
if len(cccd) != 2 or (cccd[0] & 0x02 == 0):
|
||||
logger.debug(f'not indicating, cccd={cccd.hex()}')
|
||||
return
|
||||
|
||||
# Get the value
|
||||
value = attribute.read_value(connection)
|
||||
|
||||
# Truncate if needed
|
||||
mtu = self.get_mtu(connection)
|
||||
if len(value) > mtu - 3:
|
||||
value = value[:mtu - 3]
|
||||
|
||||
# Indicate
|
||||
indication = ATT_Handle_Value_Indication(
|
||||
attribute_handle = attribute.handle,
|
||||
attribute_value = value
|
||||
)
|
||||
logger.debug(f'GATT Indicate from server: [0x{connection.handle:04X}] {indication}')
|
||||
|
||||
# Wait until we can send (only one pending indication at a time per connection)
|
||||
async with self.indication_semaphores[connection.handle]:
|
||||
assert(self.pending_confirmations[connection.handle] is None)
|
||||
|
||||
# Create a future value to hold the eventual response
|
||||
self.pending_confirmations[connection.handle] = asyncio.get_running_loop().create_future()
|
||||
|
||||
try:
|
||||
self.send_gatt_pdu(connection.handle, indication.to_bytes())
|
||||
await asyncio.wait_for(self.pending_confirmations[connection.handle], GATT_REQUEST_TIMEOUT)
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning(color('!!! GATT Indicate timeout', 'red'))
|
||||
raise TimeoutError(f'GATT timeout for {indication.name}')
|
||||
finally:
|
||||
self.pending_confirmations[connection.handle] = None
|
||||
|
||||
async def indicate_subscribers(self, attribute):
|
||||
# Get all the connections for which there's at least one subscription
|
||||
connections = [
|
||||
connection for connection in [
|
||||
self.device.lookup_connection(connection_handle)
|
||||
for (connection_handle, subscribers) in self.subscribers.items()
|
||||
if subscribers.get(attribute.handle)
|
||||
]
|
||||
if connection is not None
|
||||
]
|
||||
|
||||
# Indicate for each connection
|
||||
if connections:
|
||||
await asyncio.wait([
|
||||
self.indicate_subscriber(connection, attribute)
|
||||
for connection in connections
|
||||
])
|
||||
|
||||
def on_disconnection(self, connection):
|
||||
if connection.handle in self.mtus:
|
||||
del self.mtus[connection.handle]
|
||||
if connection.handle in self.subscribers:
|
||||
del self.subscribers[connection.handle]
|
||||
if connection.handle in self.indication_semaphores:
|
||||
del self.indication_semaphores[connection.handle]
|
||||
if connection.handle in self.pending_confirmations:
|
||||
del self.pending_confirmations[connection.handle]
|
||||
|
||||
def on_gatt_pdu(self, connection, att_pdu):
|
||||
logger.debug(f'GATT Request to server: [0x{connection.handle:04X}] {att_pdu}')
|
||||
handler_name = f'on_{att_pdu.name.lower()}'
|
||||
handler = getattr(self, handler_name, None)
|
||||
if handler is not None:
|
||||
try:
|
||||
handler(connection, att_pdu)
|
||||
except ATT_Error as error:
|
||||
logger.debug(f'normal exception returned by handler: {error}')
|
||||
response = ATT_Error_Response(
|
||||
request_opcode_in_error = att_pdu.op_code,
|
||||
attribute_handle_in_error = error.att_handle,
|
||||
error_code = error.error_code
|
||||
)
|
||||
self.send_response(connection, response)
|
||||
except Exception as error:
|
||||
logger.warning(f'{color("!!! Exception in handler:", "red")} {error}')
|
||||
response = ATT_Error_Response(
|
||||
request_opcode_in_error = att_pdu.op_code,
|
||||
attribute_handle_in_error = 0x0000,
|
||||
error_code = ATT_UNLIKELY_ERROR_ERROR
|
||||
)
|
||||
self.send_response(connection, response)
|
||||
raise error
|
||||
else:
|
||||
# No specific handler registered
|
||||
if att_pdu.op_code in ATT_REQUESTS:
|
||||
# Invoke the generic handler
|
||||
self.on_att_request(connection, att_pdu)
|
||||
else:
|
||||
# Just ignore
|
||||
logger.warning(f'{color("--- Ignoring GATT Request from [0x{connection.handle:04X}]:", "red")} {att_pdu}')
|
||||
|
||||
def get_mtu(self, connection):
|
||||
return self.mtus.get(connection.handle, ATT_DEFAULT_MTU)
|
||||
|
||||
#######################################################
|
||||
# ATT handlers
|
||||
#######################################################
|
||||
def on_att_request(self, connection, pdu):
|
||||
'''
|
||||
Handler for requests without a more specific handler
|
||||
'''
|
||||
logger.warning(f'{color(f"--- Unsupported ATT Request from [0x{connection.handle:04X}]:", "red")} {pdu}')
|
||||
response = ATT_Error_Response(
|
||||
request_opcode_in_error = pdu.op_code,
|
||||
attribute_handle_in_error = 0x0000,
|
||||
error_code = ATT_REQUEST_NOT_SUPPORTED_ERROR
|
||||
)
|
||||
self.send_response(connection, response)
|
||||
|
||||
def on_att_exchange_mtu_request(self, connection, request):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.2.1 Exchange MTU Request
|
||||
'''
|
||||
mtu = max(ATT_DEFAULT_MTU, min(self.max_mtu, request.client_rx_mtu))
|
||||
self.mtus[connection.handle] = mtu
|
||||
self.send_response(connection, ATT_Exchange_MTU_Response(server_rx_mtu = mtu))
|
||||
|
||||
# Notify the device
|
||||
self.device.on_connection_att_mtu_update(connection.handle, mtu)
|
||||
|
||||
def on_att_find_information_request(self, connection, request):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.3.1 Find Information Request
|
||||
'''
|
||||
|
||||
# Check the request parameters
|
||||
if request.starting_handle == 0 or request.starting_handle > request.ending_handle:
|
||||
self.send_response(connection, ATT_Error_Response(
|
||||
request_opcode_in_error = request.op_code,
|
||||
attribute_handle_in_error = request.starting_handle,
|
||||
error_code = ATT_INVALID_HANDLE_ERROR
|
||||
))
|
||||
return
|
||||
|
||||
# Build list of returned attributes
|
||||
pdu_space_available = self.get_mtu(connection) - 2
|
||||
attributes = []
|
||||
uuid_size = 0
|
||||
for attribute in (
|
||||
attribute for attribute in self.attributes if
|
||||
attribute.handle >= request.starting_handle and
|
||||
attribute.handle <= request.ending_handle
|
||||
):
|
||||
# TODO: check permissions
|
||||
|
||||
this_uuid_size = len(attribute.type.to_pdu_bytes())
|
||||
|
||||
if attributes:
|
||||
# Check if this attribute has the same type size as the previous one
|
||||
if this_uuid_size != uuid_size:
|
||||
break
|
||||
|
||||
# Check if there's enough space for one more entry
|
||||
uuid_size = this_uuid_size
|
||||
if pdu_space_available < 2 + uuid_size:
|
||||
break
|
||||
|
||||
# Add the attribute to the list
|
||||
attributes.append(attribute)
|
||||
pdu_space_available -= 2 + uuid_size
|
||||
|
||||
# Return the list of attributes
|
||||
if attributes:
|
||||
information_data_list = [
|
||||
struct.pack('<H', attribute.handle) + attribute.type.to_pdu_bytes()
|
||||
for attribute in attributes
|
||||
]
|
||||
response = ATT_Find_Information_Response(
|
||||
format = 1 if len(attributes[0].type.to_pdu_bytes()) == 2 else 2,
|
||||
information_data = b''.join(information_data_list)
|
||||
)
|
||||
else:
|
||||
response = ATT_Error_Response(
|
||||
request_opcode_in_error = request.op_code,
|
||||
attribute_handle_in_error = request.starting_handle,
|
||||
error_code = ATT_ATTRIBUTE_NOT_FOUND_ERROR
|
||||
)
|
||||
|
||||
self.send_response(connection, response)
|
||||
|
||||
def on_att_find_by_type_value_request(self, connection, request):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.3.3 Find By Type Value Request
|
||||
'''
|
||||
|
||||
# Build list of returned attributes
|
||||
pdu_space_available = self.get_mtu(connection) - 2
|
||||
attributes = []
|
||||
for attribute in (
|
||||
attribute for attribute in self.attributes if
|
||||
attribute.handle >= request.starting_handle and
|
||||
attribute.handle <= request.ending_handle and
|
||||
attribute.type == request.attribute_type and
|
||||
attribute.read_value(connection) == request.attribute_value and
|
||||
pdu_space_available >= 4
|
||||
):
|
||||
# TODO: check permissions
|
||||
|
||||
# Add the attribute to the list
|
||||
attributes.append(attribute)
|
||||
pdu_space_available -= 4
|
||||
|
||||
# Return the list of attributes
|
||||
if attributes:
|
||||
handles_information_list = []
|
||||
for attribute in attributes:
|
||||
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:
|
||||
# Not part of a group
|
||||
group_end_handle = attribute.handle
|
||||
handles_information_list.append(struct.pack('<HH', attribute.handle, group_end_handle))
|
||||
response = ATT_Find_By_Type_Value_Response(
|
||||
handles_information_list = b''.join(handles_information_list)
|
||||
)
|
||||
else:
|
||||
response = ATT_Error_Response(
|
||||
request_opcode_in_error = request.op_code,
|
||||
attribute_handle_in_error = request.starting_handle,
|
||||
error_code = ATT_ATTRIBUTE_NOT_FOUND_ERROR
|
||||
)
|
||||
|
||||
self.send_response(connection, response)
|
||||
|
||||
def on_att_read_by_type_request(self, connection, request):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.4.1 Read By Type Request
|
||||
'''
|
||||
|
||||
mtu = self.get_mtu(connection)
|
||||
pdu_space_available = mtu - 2
|
||||
attributes = []
|
||||
for attribute in (
|
||||
attribute for attribute in self.attributes if
|
||||
attribute.type == request.attribute_type and
|
||||
attribute.handle >= request.starting_handle and
|
||||
attribute.handle <= request.ending_handle and
|
||||
pdu_space_available
|
||||
):
|
||||
# TODO: check permissions
|
||||
|
||||
# Check the attribute value size
|
||||
attribute_value = attribute.read_value(connection)
|
||||
max_attribute_size = min(mtu - 4, 253)
|
||||
if len(attribute_value) > max_attribute_size:
|
||||
# We need to truncate
|
||||
attribute_value = attribute_value[:max_attribute_size]
|
||||
if attributes and len(attributes[0][1]) != len(attribute_value):
|
||||
# Not the same size as previous attribute, stop here
|
||||
break
|
||||
|
||||
# Check if there is enough space
|
||||
entry_size = 2 + len(attribute_value)
|
||||
if pdu_space_available < entry_size:
|
||||
break
|
||||
|
||||
# Add the attribute to the list
|
||||
attributes.append((attribute.handle, attribute_value))
|
||||
pdu_space_available -= entry_size
|
||||
|
||||
if attributes:
|
||||
attribute_data_list = [struct.pack('<H', handle) + value for handle, value in attributes]
|
||||
response = ATT_Read_By_Type_Response(
|
||||
length = entry_size,
|
||||
attribute_data_list = b''.join(attribute_data_list)
|
||||
)
|
||||
else:
|
||||
response = ATT_Error_Response(
|
||||
request_opcode_in_error = request.op_code,
|
||||
attribute_handle_in_error = request.starting_handle,
|
||||
error_code = ATT_ATTRIBUTE_NOT_FOUND_ERROR
|
||||
)
|
||||
|
||||
self.send_response(connection, response)
|
||||
|
||||
def on_att_read_request(self, connection, request):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.4.3 Read Request
|
||||
'''
|
||||
|
||||
if attribute := self.get_attribute(request.attribute_handle):
|
||||
# TODO: check permissions
|
||||
value = attribute.read_value(connection)
|
||||
value_size = min(self.get_mtu(connection) - 1, len(value))
|
||||
response = ATT_Read_Response(
|
||||
attribute_value = value[:value_size]
|
||||
)
|
||||
else:
|
||||
response = ATT_Error_Response(
|
||||
request_opcode_in_error = request.op_code,
|
||||
attribute_handle_in_error = request.attribute_handle,
|
||||
error_code = ATT_INVALID_HANDLE_ERROR
|
||||
)
|
||||
self.send_response(connection, response)
|
||||
|
||||
def on_att_read_blob_request(self, connection, request):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.4.5 Read Blob Request
|
||||
'''
|
||||
|
||||
if attribute := self.get_attribute(request.attribute_handle):
|
||||
# TODO: check permissions
|
||||
mtu = self.get_mtu(connection)
|
||||
value = attribute.read_value(connection)
|
||||
if request.value_offset > len(value):
|
||||
response = ATT_Error_Response(
|
||||
request_opcode_in_error = request.op_code,
|
||||
attribute_handle_in_error = request.attribute_handle,
|
||||
error_code = ATT_INVALID_OFFSET_ERROR
|
||||
)
|
||||
elif len(value) <= mtu - 1:
|
||||
response = ATT_Error_Response(
|
||||
request_opcode_in_error = request.op_code,
|
||||
attribute_handle_in_error = request.attribute_handle,
|
||||
error_code = ATT_ATTRIBUTE_NOT_LONG_ERROR
|
||||
)
|
||||
else:
|
||||
part_size = min(mtu - 1, len(value) - request.value_offset)
|
||||
response = ATT_Read_Blob_Response(
|
||||
part_attribute_value = value[request.value_offset:request.value_offset + part_size]
|
||||
)
|
||||
else:
|
||||
response = ATT_Error_Response(
|
||||
request_opcode_in_error = request.op_code,
|
||||
attribute_handle_in_error = request.attribute_handle,
|
||||
error_code = ATT_INVALID_HANDLE_ERROR
|
||||
)
|
||||
self.send_response(connection, response)
|
||||
|
||||
def on_att_read_by_group_type_request(self, connection, request):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.4.9 Read by Group Type Request
|
||||
'''
|
||||
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,
|
||||
error_code = ATT_UNSUPPORTED_GROUP_TYPE_ERROR
|
||||
)
|
||||
self.send_response(connection, response)
|
||||
return
|
||||
|
||||
mtu = self.get_mtu(connection)
|
||||
pdu_space_available = mtu - 2
|
||||
attributes = []
|
||||
for attribute in (
|
||||
attribute for attribute in self.attributes if
|
||||
attribute.type == request.attribute_group_type and
|
||||
attribute.handle >= request.starting_handle and
|
||||
attribute.handle <= request.ending_handle and
|
||||
pdu_space_available
|
||||
):
|
||||
# Check the attribute value size
|
||||
attribute_value = attribute.read_value(connection)
|
||||
max_attribute_size = min(mtu - 6, 251)
|
||||
if len(attribute_value) > max_attribute_size:
|
||||
# We need to truncate
|
||||
attribute_value = attribute_value[:max_attribute_size]
|
||||
if attributes and len(attributes[0][2]) != len(attribute_value):
|
||||
# Not the same size as previous attributes, stop here
|
||||
break
|
||||
|
||||
# Check if there is enough space
|
||||
entry_size = 4 + len(attribute_value)
|
||||
if pdu_space_available < entry_size:
|
||||
break
|
||||
|
||||
# Add the attribute to the list
|
||||
attributes.append((attribute.handle, attribute.end_group_handle, attribute_value))
|
||||
pdu_space_available -= entry_size
|
||||
|
||||
if attributes:
|
||||
attribute_data_list = [
|
||||
struct.pack('<HH', handle, end_group_handle) + value
|
||||
for handle, end_group_handle, value in attributes
|
||||
]
|
||||
response = ATT_Read_By_Group_Type_Response(
|
||||
length = len(attribute_data_list[0]),
|
||||
attribute_data_list = b''.join(attribute_data_list)
|
||||
)
|
||||
else:
|
||||
response = ATT_Error_Response(
|
||||
request_opcode_in_error = request.op_code,
|
||||
attribute_handle_in_error = request.starting_handle,
|
||||
error_code = ATT_ATTRIBUTE_NOT_FOUND_ERROR
|
||||
)
|
||||
|
||||
self.send_response(connection, response)
|
||||
|
||||
def on_att_write_request(self, connection, request):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.5.1 Write Request
|
||||
'''
|
||||
|
||||
# Check that the attribute exists
|
||||
attribute = self.get_attribute(request.attribute_handle)
|
||||
if attribute is None:
|
||||
self.send_response(connection, ATT_Error_Response(
|
||||
request_opcode_in_error = request.op_code,
|
||||
attribute_handle_in_error = request.attribute_handle,
|
||||
error_code = ATT_INVALID_HANDLE_ERROR
|
||||
))
|
||||
return
|
||||
|
||||
# TODO: check permissions
|
||||
|
||||
# Check the request parameters
|
||||
if len(request.attribute_value) > GATT_MAX_ATTRIBUTE_VALUE_SIZE:
|
||||
self.send_response(connection, ATT_Error_Response(
|
||||
request_opcode_in_error = request.op_code,
|
||||
attribute_handle_in_error = request.attribute_handle,
|
||||
error_code = ATT_INVALID_ATTRIBUTE_LENGTH_ERROR
|
||||
))
|
||||
return
|
||||
|
||||
# Accept the value
|
||||
attribute.write_value(connection, request.attribute_value)
|
||||
|
||||
# Done
|
||||
self.send_response(connection, ATT_Write_Response())
|
||||
|
||||
def on_att_write_command(self, connection, request):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.5.3 Write Command
|
||||
'''
|
||||
|
||||
# Check that the attribute exists
|
||||
attribute = self.get_attribute(request.attribute_handle)
|
||||
if attribute is None:
|
||||
return
|
||||
|
||||
# TODO: check permissions
|
||||
|
||||
# Check the request parameters
|
||||
if len(request.attribute_value) > GATT_MAX_ATTRIBUTE_VALUE_SIZE:
|
||||
return
|
||||
|
||||
# Accept the value
|
||||
try:
|
||||
attribute.write_value(connection, request.attribute_value)
|
||||
except Exception as error:
|
||||
logger.warning(f'!!! ignoring exception: {error}')
|
||||
|
||||
def on_att_handle_value_confirmation(self, connection, confirmation):
|
||||
'''
|
||||
See Bluetooth spec Vol 3, Part F - 3.4.7.3 Handle Value Confirmation
|
||||
'''
|
||||
if self.pending_confirmations[connection.handle] is None:
|
||||
# Not expected!
|
||||
logger.warning('!!! unexpected confirmation, there is no pending indication')
|
||||
return
|
||||
|
||||
self.pending_confirmations[connection.handle].set_result(None)
|
||||
3471
bumble/hci.py
Normal file
3471
bumble/hci.py
Normal file
File diff suppressed because it is too large
Load Diff
179
bumble/helpers.py
Normal file
179
bumble/helpers.py
Normal file
@@ -0,0 +1,179 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import logging
|
||||
from colors import color
|
||||
|
||||
from .core import name_or_number
|
||||
from .gatt import ATT_PDU, ATT_CID
|
||||
from .l2cap import (
|
||||
L2CAP_PDU,
|
||||
L2CAP_CONNECTION_REQUEST,
|
||||
L2CAP_CONNECTION_RESPONSE,
|
||||
L2CAP_SIGNALING_CID,
|
||||
L2CAP_LE_SIGNALING_CID,
|
||||
L2CAP_Control_Frame,
|
||||
L2CAP_Connection_Response
|
||||
)
|
||||
from .hci import (
|
||||
HCI_EVENT_PACKET,
|
||||
HCI_ACL_DATA_PACKET,
|
||||
HCI_DISCONNECTION_COMPLETE_EVENT,
|
||||
HCI_AclDataPacketAssembler
|
||||
)
|
||||
from .rfcomm import RFCOMM_Frame, RFCOMM_PSM
|
||||
from .sdp import SDP_PDU, SDP_PSM
|
||||
from .avdtp import (
|
||||
MessageAssembler as AVDTP_MessageAssembler,
|
||||
AVDTP_PSM
|
||||
)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
PSM_NAMES = {
|
||||
RFCOMM_PSM: 'RFCOMM',
|
||||
SDP_PSM: 'SDP',
|
||||
AVDTP_PSM: 'AVDTP'
|
||||
# TODO: add more PSM values
|
||||
}
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class PacketTracer:
|
||||
class AclStream:
|
||||
def __init__(self, analyzer):
|
||||
self.analyzer = analyzer
|
||||
self.packet_assembler = HCI_AclDataPacketAssembler(self.on_acl_pdu)
|
||||
self.avdtp_assemblers = {} # AVDTP assemblers, by source_cid
|
||||
self.psms = {} # PSM, by source_cid
|
||||
self.peer = None # ACL stream in the other direction
|
||||
|
||||
def on_acl_pdu(self, pdu):
|
||||
l2cap_pdu = L2CAP_PDU.from_bytes(pdu)
|
||||
|
||||
if l2cap_pdu.cid == ATT_CID:
|
||||
att_pdu = ATT_PDU.from_bytes(l2cap_pdu.payload)
|
||||
self.analyzer.emit(att_pdu)
|
||||
elif l2cap_pdu.cid == L2CAP_SIGNALING_CID or l2cap_pdu.cid == L2CAP_LE_SIGNALING_CID:
|
||||
control_frame = L2CAP_Control_Frame.from_bytes(l2cap_pdu.payload)
|
||||
self.analyzer.emit(control_frame)
|
||||
|
||||
# Check if this signals a new channel
|
||||
if control_frame.code == L2CAP_CONNECTION_REQUEST:
|
||||
self.psms[control_frame.source_cid] = control_frame.psm
|
||||
elif control_frame.code == L2CAP_CONNECTION_RESPONSE:
|
||||
if control_frame.result == L2CAP_Connection_Response.CONNECTION_SUCCESSFUL:
|
||||
if self.peer:
|
||||
if psm := self.peer.psms.get(control_frame.source_cid):
|
||||
# Found a pending connection
|
||||
self.psms[control_frame.destination_cid] = psm
|
||||
|
||||
# For AVDTP connections, create a packet assembler for each direction
|
||||
if psm == AVDTP_PSM:
|
||||
self.avdtp_assemblers[control_frame.source_cid] = AVDTP_MessageAssembler(self.on_avdtp_message)
|
||||
self.peer.avdtp_assemblers[control_frame.destination_cid] = AVDTP_MessageAssembler(self.peer.on_avdtp_message)
|
||||
|
||||
else:
|
||||
# Try to find the PSM associated with this PDU
|
||||
if self.peer and (psm := self.peer.psms.get(l2cap_pdu.cid)):
|
||||
if psm == SDP_PSM:
|
||||
sdp_pdu = SDP_PDU.from_bytes(l2cap_pdu.payload)
|
||||
self.analyzer.emit(sdp_pdu)
|
||||
elif psm == RFCOMM_PSM:
|
||||
rfcomm_frame = RFCOMM_Frame.from_bytes(l2cap_pdu.payload)
|
||||
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()}')
|
||||
assembler = self.avdtp_assemblers.get(l2cap_pdu.cid)
|
||||
if assembler:
|
||||
assembler.on_pdu(l2cap_pdu.payload)
|
||||
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()}')
|
||||
else:
|
||||
self.analyzer.emit(l2cap_pdu)
|
||||
|
||||
def on_avdtp_message(self, transaction_label, message):
|
||||
self.analyzer.emit(f'{color("AVDTP", "green")} [{transaction_label}] {message}')
|
||||
|
||||
def feed_packet(self, packet):
|
||||
self.packet_assembler.feed_packet(packet)
|
||||
|
||||
class Analyzer:
|
||||
def __init__(self, label, emit_message):
|
||||
self.label = label
|
||||
self.emit_message = emit_message
|
||||
self.acl_streams = {} # ACL streams, by connection handle
|
||||
self.peer = None # Analyzer in the other direction
|
||||
|
||||
def start_acl_stream(self, connection_handle):
|
||||
logger.info(f'[{self.label}] +++ Creating ACL stream for connection 0x{connection_handle:04X}')
|
||||
stream = PacketTracer.AclStream(self)
|
||||
self.acl_streams[connection_handle] = stream
|
||||
|
||||
# Associate with a peer stream if we can
|
||||
if peer_stream := self.peer.acl_streams.get(connection_handle):
|
||||
stream.peer = peer_stream
|
||||
peer_stream.peer = stream
|
||||
|
||||
return stream
|
||||
|
||||
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}')
|
||||
del self.acl_streams[connection_handle]
|
||||
|
||||
# Let the other forwarder know so it can cleanup its stream as well
|
||||
self.peer.end_acl_stream(connection_handle)
|
||||
|
||||
def on_packet(self, packet):
|
||||
self.emit(packet)
|
||||
|
||||
if packet.hci_packet_type == HCI_ACL_DATA_PACKET:
|
||||
# Look for an existing stream for this handle, create one if it is the
|
||||
# first ACL packet for that connection handle
|
||||
if (stream := self.acl_streams.get(packet.connection_handle)) is None:
|
||||
stream = self.start_acl_stream(packet.connection_handle)
|
||||
stream.feed_packet(packet)
|
||||
elif packet.hci_packet_type == HCI_EVENT_PACKET:
|
||||
if packet.event_code == HCI_DISCONNECTION_COMPLETE_EVENT:
|
||||
self.end_acl_stream(packet.connection_handle)
|
||||
|
||||
def emit(self, message):
|
||||
self.emit_message(f'[{self.label}] {message}')
|
||||
|
||||
def trace(self, packet, direction=0):
|
||||
if direction == 0:
|
||||
self.host_to_controller_analyzer.on_packet(packet)
|
||||
else:
|
||||
self.controller_to_host_analyzer.on_packet(packet)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
host_to_controller_label=color('HOST->CONTROLLER', 'blue'),
|
||||
controller_to_host_label=color('CONTROLLER->HOST', 'cyan'),
|
||||
emit_message=logger.info
|
||||
):
|
||||
self.host_to_controller_analyzer = PacketTracer.Analyzer(host_to_controller_label, emit_message)
|
||||
self.controller_to_host_analyzer = PacketTracer.Analyzer(controller_to_host_label, emit_message)
|
||||
self.host_to_controller_analyzer.peer = self.controller_to_host_analyzer
|
||||
self.controller_to_host_analyzer.peer = self.host_to_controller_analyzer
|
||||
94
bumble/hfp.py
Normal file
94
bumble/hfp.py
Normal file
@@ -0,0 +1,94 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import logging
|
||||
import asyncio
|
||||
import collections
|
||||
from colors import color
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Protocol Support
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class HfpProtocol:
|
||||
def __init__(self, dlc):
|
||||
self.dlc = dlc
|
||||
self.buffer = ''
|
||||
self.lines = collections.deque()
|
||||
self.lines_available = asyncio.Event()
|
||||
|
||||
dlc.sink = self.feed
|
||||
|
||||
def feed(self, data):
|
||||
# Convert the data to a string if needed
|
||||
if type(data) == bytes:
|
||||
data = data.decode('utf-8')
|
||||
|
||||
logger.debug(f'<<< Data received: {data}')
|
||||
|
||||
# Add to the buffer and look for lines
|
||||
self.buffer += data
|
||||
while (separator := self.buffer.find('\r')) >= 0:
|
||||
line = self.buffer[:separator].strip()
|
||||
self.buffer = self.buffer[separator + 1:]
|
||||
if len(line) > 0:
|
||||
self.on_line(line)
|
||||
|
||||
def on_line(self, line):
|
||||
self.lines.append(line)
|
||||
self.lines_available.set()
|
||||
|
||||
def send_command_line(self, line):
|
||||
logger.debug(color(f'>>> {line}', 'yellow'))
|
||||
self.dlc.write(line + '\r')
|
||||
|
||||
def send_response_line(self, line):
|
||||
logger.debug(color(f'>>> {line}', 'yellow'))
|
||||
self.dlc.write('\r\n' + line + '\r\n')
|
||||
|
||||
async def next_line(self):
|
||||
await self.lines_available.wait()
|
||||
line = self.lines.popleft()
|
||||
if not self.lines:
|
||||
self.lines_available.clear()
|
||||
logger.debug(color(f'<<< {line}', 'green'))
|
||||
return line
|
||||
|
||||
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())
|
||||
|
||||
self.send_command_line('AT+CIND=?')
|
||||
line = await(self.next_line())
|
||||
line = await(self.next_line())
|
||||
|
||||
self.send_command_line('AT+CIND?')
|
||||
line = await(self.next_line())
|
||||
line = await(self.next_line())
|
||||
|
||||
self.send_command_line('AT+CMER=3,0,0,1')
|
||||
line = await(self.next_line())
|
||||
604
bumble/host.py
Normal file
604
bumble/host.py
Normal file
@@ -0,0 +1,604 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import logging
|
||||
from pyee import EventEmitter
|
||||
from colors import color
|
||||
|
||||
from .hci import *
|
||||
from .l2cap import *
|
||||
from .att import *
|
||||
from .gatt import *
|
||||
from .smp import *
|
||||
from .core import ConnectionParameters
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
HOST_DEFAULT_HC_LE_ACL_DATA_PACKET_LENGTH = 27
|
||||
HOST_HC_TOTAL_NUM_LE_ACL_DATA_PACKETS = 1
|
||||
HOST_DEFAULT_HC_ACL_DATA_PACKET_LENGTH = 27
|
||||
HOST_HC_TOTAL_NUM_ACL_DATA_PACKETS = 1
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Connection:
|
||||
def __init__(self, host, handle, role, peer_address):
|
||||
self.host = host
|
||||
self.handle = handle
|
||||
self.role = role
|
||||
self.peer_address = peer_address
|
||||
self.assembler = HCI_AclDataPacketAssembler(self.on_acl_pdu)
|
||||
|
||||
def on_hci_acl_data_packet(self, packet):
|
||||
self.assembler.feed_packet(packet)
|
||||
|
||||
def on_acl_pdu(self, pdu):
|
||||
l2cap_pdu = L2CAP_PDU.from_bytes(pdu)
|
||||
|
||||
if l2cap_pdu.cid == ATT_CID:
|
||||
self.host.on_gatt_pdu(self, l2cap_pdu.payload)
|
||||
elif l2cap_pdu.cid == SMP_CID:
|
||||
self.host.on_smp_pdu(self, l2cap_pdu.payload)
|
||||
else:
|
||||
self.host.on_l2cap_pdu(self, l2cap_pdu.cid, l2cap_pdu.payload)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Host(EventEmitter):
|
||||
def __init__(self, controller_source = None, controller_sink = None):
|
||||
super().__init__()
|
||||
|
||||
self.hci_sink = None
|
||||
self.ready = False # True when we can accept incoming packets
|
||||
self.connections = {} # Connections, by connection handle
|
||||
self.pending_command = None
|
||||
self.pending_response = None
|
||||
self.hc_le_acl_data_packet_length = HOST_DEFAULT_HC_LE_ACL_DATA_PACKET_LENGTH
|
||||
self.hc_total_num_le_acl_data_packets = HOST_HC_TOTAL_NUM_LE_ACL_DATA_PACKETS
|
||||
self.hc_acl_data_packet_length = HOST_DEFAULT_HC_ACL_DATA_PACKET_LENGTH
|
||||
self.hc_total_num_acl_data_packets = HOST_HC_TOTAL_NUM_ACL_DATA_PACKETS
|
||||
self.acl_packet_queue = collections.deque()
|
||||
self.acl_packets_in_flight = 0
|
||||
self.local_supported_commands = bytes(64)
|
||||
self.command_semaphore = asyncio.Semaphore(1)
|
||||
self.long_term_key_provider = None
|
||||
self.link_key_provider = None
|
||||
|
||||
# Connect to the source and sink if specified
|
||||
if controller_source:
|
||||
controller_source.set_packet_sink(self)
|
||||
if controller_sink:
|
||||
self.set_packet_sink(controller_sink)
|
||||
|
||||
async def reset(self):
|
||||
await self.send_command(HCI_Reset_Command())
|
||||
self.ready = True
|
||||
|
||||
response = await self.send_command(HCI_Read_Local_Supported_Commands_Command())
|
||||
if response.return_parameters.status != HCI_SUCCESS:
|
||||
raise ProtocolError(response.return_parameters.status, 'hci')
|
||||
self.local_supported_commands = response.return_parameters.supported_commands
|
||||
|
||||
await self.send_command(HCI_Set_Event_Mask_Command(event_mask = bytes.fromhex('FFFFFFFFFFFFFFFF')))
|
||||
await self.send_command(HCI_LE_Set_Event_Mask_Command(le_event_mask = bytes.fromhex('FFFFF00000000000')))
|
||||
await self.send_command(HCI_Read_Local_Version_Information_Command())
|
||||
await self.send_command(HCI_Write_LE_Host_Support_Command(le_supported_host = 1, simultaneous_le_host = 0))
|
||||
|
||||
response = await self.send_command(HCI_LE_Read_Buffer_Size_Command())
|
||||
if response.return_parameters.status == HCI_SUCCESS:
|
||||
self.hc_le_acl_data_packet_length = response.return_parameters.hc_le_acl_data_packet_length
|
||||
self.hc_total_num_le_acl_data_packets = response.return_parameters.hc_total_num_le_acl_data_packets
|
||||
logger.debug(f'HCI LE ACL flow control: hc_le_acl_data_packet_length={response.return_parameters.hc_le_acl_data_packet_length}, hc_total_num_le_acl_data_packets={response.return_parameters.hc_total_num_le_acl_data_packets}')
|
||||
else:
|
||||
logger.warn(f'HCI_LE_Read_Buffer_Size_Command failed: {response.return_parameters.status}')
|
||||
if response.return_parameters.hc_le_acl_data_packet_length == 0 or response.return_parameters.hc_total_num_le_acl_data_packets == 0:
|
||||
# Read the non-LE-specific values
|
||||
response = await self.send_command(HCI_Read_Buffer_Size_Command())
|
||||
if response.return_parameters.status == HCI_SUCCESS:
|
||||
self.hc_acl_data_packet_length = response.return_parameters.hc_le_acl_data_packet_length
|
||||
self.hc_le_acl_data_packet_length = self.hc_le_acl_data_packet_length or self.hc_acl_data_packet_length
|
||||
self.hc_total_num_acl_data_packets = response.return_parameters.hc_total_num_le_acl_data_packets
|
||||
self.hc_total_num_le_acl_data_packets = self.hc_total_num_le_acl_data_packets or self.hc_total_num_acl_data_packets
|
||||
logger.debug(f'HCI LE ACL flow control: hc_le_acl_data_packet_length={self.hc_le_acl_data_packet_length}, hc_total_num_le_acl_data_packets={self.hc_total_num_le_acl_data_packets}')
|
||||
else:
|
||||
logger.warn(f'HCI_Read_Buffer_Size_Command failed: {response.return_parameters.status}')
|
||||
|
||||
self.reset_done = True
|
||||
|
||||
@property
|
||||
def controller(self):
|
||||
return self.hci_sink
|
||||
|
||||
@controller.setter
|
||||
def controller(self, controller):
|
||||
self.set_packet_sink(controller)
|
||||
if controller:
|
||||
controller.set_packet_sink(self)
|
||||
|
||||
def set_packet_sink(self, sink):
|
||||
self.hci_sink = sink
|
||||
|
||||
def send_hci_packet(self, packet):
|
||||
self.hci_sink.on_packet(packet.to_bytes())
|
||||
|
||||
async def send_command(self, command):
|
||||
logger.debug(f'{color("### HOST -> CONTROLLER", "blue")}: {command}')
|
||||
|
||||
# Wait until we can send (only one pending command at a time)
|
||||
async with self.command_semaphore:
|
||||
assert(self.pending_command is None)
|
||||
assert(self.pending_response is None)
|
||||
|
||||
# Create a future value to hold the eventual response
|
||||
self.pending_response = asyncio.get_running_loop().create_future()
|
||||
self.pending_command = command
|
||||
|
||||
try:
|
||||
self.send_hci_packet(command)
|
||||
response = await self.pending_response
|
||||
# TODO: check error values
|
||||
return response
|
||||
except Exception as error:
|
||||
logger.warning(f'{color("!!! Exception while sending HCI packet:", "red")} {error}')
|
||||
# raise error
|
||||
finally:
|
||||
self.pending_command = None
|
||||
self.pending_response = None
|
||||
|
||||
# Use this method to send a command from a task
|
||||
def send_command_sync(self, command):
|
||||
async def send_command(command):
|
||||
await self.send_command(command)
|
||||
|
||||
asyncio.create_task(send_command(command))
|
||||
|
||||
def send_l2cap_pdu(self, connection_handle, cid, pdu):
|
||||
l2cap_pdu = L2CAP_PDU(cid, pdu).to_bytes()
|
||||
|
||||
# Send the data to the controller via ACL packets
|
||||
bytes_remaining = len(l2cap_pdu)
|
||||
offset = 0
|
||||
pb_flag = 0
|
||||
while bytes_remaining:
|
||||
data_total_length = min(bytes_remaining, self.hc_le_acl_data_packet_length)
|
||||
acl_packet = HCI_AclDataPacket(
|
||||
connection_handle = connection_handle,
|
||||
pb_flag = pb_flag,
|
||||
bc_flag = 0,
|
||||
data_total_length = data_total_length,
|
||||
data = l2cap_pdu[offset:offset + data_total_length]
|
||||
)
|
||||
logger.debug(f'{color("### HOST -> CONTROLLER", "blue")}: (CID={cid}) {acl_packet}')
|
||||
self.queue_acl_packet(acl_packet)
|
||||
pb_flag = 1
|
||||
offset += data_total_length
|
||||
bytes_remaining -= data_total_length
|
||||
|
||||
def queue_acl_packet(self, acl_packet):
|
||||
self.acl_packet_queue.appendleft(acl_packet)
|
||||
self.check_acl_packet_queue()
|
||||
|
||||
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')
|
||||
|
||||
def check_acl_packet_queue(self):
|
||||
# Send all we can
|
||||
while len(self.acl_packet_queue) > 0 and self.acl_packets_in_flight < self.hc_total_num_le_acl_data_packets:
|
||||
packet = self.acl_packet_queue.pop()
|
||||
self.send_hci_packet(packet)
|
||||
self.acl_packets_in_flight += 1
|
||||
|
||||
# Packet Sink protocol (packets coming from the controller via HCI)
|
||||
def on_packet(self, packet):
|
||||
hci_packet = HCI_Packet.from_bytes(packet)
|
||||
if self.ready or (
|
||||
hci_packet.hci_packet_type == HCI_EVENT_PACKET and
|
||||
hci_packet.event_code == HCI_COMMAND_COMPLETE_EVENT and
|
||||
hci_packet.command_opcode == HCI_RESET_COMMAND
|
||||
):
|
||||
self.on_hci_packet(hci_packet)
|
||||
else:
|
||||
logger.debug('reset not done, ignoring packet from controller')
|
||||
|
||||
def on_hci_packet(self, packet):
|
||||
logger.debug(f'{color("### CONTROLLER -> HOST", "green")}: {packet}')
|
||||
|
||||
# If the packet is a command, invoke the handler for this packet
|
||||
if packet.hci_packet_type == HCI_COMMAND_PACKET:
|
||||
self.on_hci_command_packet(packet)
|
||||
elif packet.hci_packet_type == HCI_EVENT_PACKET:
|
||||
self.on_hci_event_packet(packet)
|
||||
elif packet.hci_packet_type == HCI_ACL_DATA_PACKET:
|
||||
self.on_hci_acl_data_packet(packet)
|
||||
else:
|
||||
logger.warning(f'!!! unknown packet type {packet.hci_packet_type}')
|
||||
|
||||
def on_hci_command_packet(self, command):
|
||||
logger.warning(f'!!! unexpected command packet: {command}')
|
||||
|
||||
def on_hci_event_packet(self, event):
|
||||
handler_name = f'on_{event.name.lower()}'
|
||||
handler = getattr(self, handler_name, self.on_hci_event)
|
||||
handler(event)
|
||||
|
||||
def on_hci_acl_data_packet(self, packet):
|
||||
# Look for the connection to which this data belongs
|
||||
if connection := self.connections.get(packet.connection_handle):
|
||||
connection.on_hci_acl_data_packet(packet)
|
||||
|
||||
def on_gatt_pdu(self, connection, pdu):
|
||||
self.emit('gatt_pdu', connection.handle, pdu)
|
||||
|
||||
def on_smp_pdu(self, connection, pdu):
|
||||
self.emit('smp_pdu', connection.handle, pdu)
|
||||
|
||||
def on_l2cap_pdu(self, connection, cid, pdu):
|
||||
self.emit('l2cap_pdu', connection.handle, cid, pdu)
|
||||
|
||||
def on_command_processed(self, event):
|
||||
if self.pending_response:
|
||||
# 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}')
|
||||
|
||||
self.pending_response.set_result(event)
|
||||
else:
|
||||
logger.warning('!!! no pending response future to set')
|
||||
|
||||
############################################################
|
||||
# HCI handlers
|
||||
############################################################
|
||||
def on_hci_event(self, event):
|
||||
logger.warning(f'{color(f"--- Ignoring event {event}", "red")}')
|
||||
|
||||
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
|
||||
logger.debug('no-command event')
|
||||
else:
|
||||
return self.on_command_processed(event)
|
||||
|
||||
def on_hci_command_status_event(self, event):
|
||||
return self.on_command_processed(event)
|
||||
|
||||
def on_hci_number_of_completed_packets_event(self, event):
|
||||
total_packets = sum(event.num_completed_packets)
|
||||
if total_packets <= self.acl_packets_in_flight:
|
||||
self.acl_packets_in_flight -= total_packets
|
||||
self.check_acl_packet_queue()
|
||||
else:
|
||||
logger.warning(color(f'!!! {total_packets} completed but only {self.acl_packets_in_flight} in flight'))
|
||||
self.acl_packets_in_flight = 0
|
||||
|
||||
# Classic only
|
||||
def on_hci_connection_request_event(self, event):
|
||||
# For now, just accept everything
|
||||
# TODO: delegate the decision
|
||||
self.send_command_sync(
|
||||
HCI_Accept_Connection_Request_Command(
|
||||
bd_addr = event.bd_addr,
|
||||
role = 0x01 # Remain the peripheral
|
||||
)
|
||||
)
|
||||
|
||||
def on_hci_le_connection_complete_event(self, event):
|
||||
# Check if this is a cancellation
|
||||
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)}')
|
||||
|
||||
connection = self.connections.get(event.connection_handle)
|
||||
if connection is None:
|
||||
connection = Connection(self, event.connection_handle, event.role, event.peer_address)
|
||||
self.connections[event.connection_handle] = connection
|
||||
|
||||
# Notify the client
|
||||
connection_parameters = ConnectionParameters(
|
||||
event.conn_interval,
|
||||
event.conn_latency,
|
||||
event.supervision_timeout
|
||||
)
|
||||
self.emit(
|
||||
'connection',
|
||||
event.connection_handle,
|
||||
BT_LE_TRANSPORT,
|
||||
event.peer_address,
|
||||
None,
|
||||
event.role,
|
||||
connection_parameters
|
||||
)
|
||||
else:
|
||||
logger.debug(f'### CONNECTION FAILED: {event.status}')
|
||||
|
||||
# Notify the listeners
|
||||
self.emit('connection_failure', event.status)
|
||||
|
||||
def on_hci_le_enhanced_connection_complete_event(self, event):
|
||||
# Just use the same implementation as for the non-enhanced event for now
|
||||
self.on_hci_le_connection_complete_event(event)
|
||||
|
||||
def on_hci_connection_complete_event(self, event):
|
||||
if event.status == HCI_SUCCESS:
|
||||
# Create/update the connection
|
||||
logger.debug(f'### BR/EDR CONNECTION: [0x{event.connection_handle:04X}] {event.bd_addr}')
|
||||
|
||||
connection = self.connections.get(event.connection_handle)
|
||||
if connection is None:
|
||||
connection = Connection(self, event.connection_handle, BT_CENTRAL_ROLE, event.bd_addr)
|
||||
self.connections[event.connection_handle] = connection
|
||||
|
||||
# Notify the client
|
||||
self.emit(
|
||||
'connection',
|
||||
event.connection_handle,
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
event.bd_addr,
|
||||
None,
|
||||
BT_CENTRAL_ROLE,
|
||||
None
|
||||
)
|
||||
else:
|
||||
logger.debug(f'### BR/EDR CONNECTION FAILED: {event.status}')
|
||||
|
||||
# Notify the client
|
||||
self.emit('connection_failure', event.status)
|
||||
|
||||
def on_hci_disconnection_complete_event(self, event):
|
||||
# Find the connection
|
||||
if (connection := self.connections.get(event.connection_handle)) is None:
|
||||
logger.warning('!!! DISCONNECTION COMPLETE: unknown handle')
|
||||
return
|
||||
|
||||
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}')
|
||||
del self.connections[event.connection_handle]
|
||||
|
||||
# Notify the listeners
|
||||
self.emit('disconnection', event.connection_handle, event.reason)
|
||||
else:
|
||||
logger.debug(f'### DISCONNECTION FAILED: {event.status}')
|
||||
|
||||
# Notify the listeners
|
||||
self.emit('disconnection_failure', event.status)
|
||||
|
||||
def on_hci_le_connection_update_complete_event(self, event):
|
||||
if (connection := self.connections.get(event.connection_handle)) is None:
|
||||
logger.warning('!!! CONNECTION PARAMETERS UPDATE COMPLETE: unknown handle')
|
||||
return
|
||||
|
||||
# Notify the client
|
||||
if event.status == HCI_SUCCESS:
|
||||
connection_parameters = ConnectionParameters(
|
||||
event.conn_interval,
|
||||
event.conn_latency,
|
||||
event.supervision_timeout
|
||||
)
|
||||
self.emit('connection_parameters_update', connection.handle, connection_parameters)
|
||||
else:
|
||||
self.emit('connection_parameters_update_failure', connection.handle, event.status)
|
||||
|
||||
def on_hci_le_phy_update_complete_event(self, event):
|
||||
if (connection := self.connections.get(event.connection_handle)) is None:
|
||||
logger.warning('!!! CONNECTION PHY UPDATE COMPLETE: unknown handle')
|
||||
return
|
||||
|
||||
# Notify the client
|
||||
if event.status == HCI_SUCCESS:
|
||||
connection_phy = ConnectionPHY(event.tx_phy, event.rx_phy)
|
||||
self.emit('connection_phy_update', connection.handle, connection_phy)
|
||||
else:
|
||||
self.emit('connection_phy_update_failure', connection.handle, event.status)
|
||||
|
||||
def on_hci_le_advertising_report_event(self, event):
|
||||
for report in event.reports:
|
||||
self.emit(
|
||||
'advertising_report',
|
||||
report.address,
|
||||
report.data,
|
||||
report.rssi,
|
||||
report.event_type
|
||||
)
|
||||
|
||||
def on_hci_le_remote_connection_parameter_request_event(self, event):
|
||||
if event.connection_handle not in self.connections:
|
||||
logger.warning('!!! REMOTE CONNECTION PARAMETER REQUEST: unknown handle')
|
||||
return
|
||||
|
||||
# For now, just accept everything
|
||||
# TODO: delegate the decision
|
||||
self.send_command_sync(
|
||||
HCI_LE_Remote_Connection_Parameter_Request_Reply_Command(
|
||||
connection_handle = event.connection_handle,
|
||||
interval_min = event.interval_min,
|
||||
interval_max = event.interval_max,
|
||||
latency = event.latency,
|
||||
timeout = event.timeout,
|
||||
minimum_ce_length = 0,
|
||||
maximum_ce_length = 0
|
||||
)
|
||||
)
|
||||
|
||||
def on_hci_le_long_term_key_request_event(self, event):
|
||||
if (connection := self.connections.get(event.connection_handle)) is None:
|
||||
logger.warning('!!! LE LONG TERM KEY REQUEST: unknown handle')
|
||||
return
|
||||
|
||||
async def send_long_term_key():
|
||||
if self.long_term_key_provider is None:
|
||||
logger.debug('no long term key provider')
|
||||
long_term_key = None
|
||||
else:
|
||||
long_term_key = await 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,
|
||||
long_term_key = long_term_key
|
||||
)
|
||||
else:
|
||||
response = HCI_LE_Long_Term_Key_Request_Negative_Reply_Command(
|
||||
connection_handle = event.connection_handle
|
||||
)
|
||||
|
||||
await self.send_command(response)
|
||||
|
||||
asyncio.create_task(send_long_term_key())
|
||||
|
||||
def on_hci_synchronous_connection_complete_event(self, event):
|
||||
pass
|
||||
|
||||
def on_hci_synchronous_connection_changed_event(self, event):
|
||||
pass
|
||||
|
||||
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)}')
|
||||
# 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)}')
|
||||
|
||||
def on_hci_le_data_length_change_event(self, event):
|
||||
self.emit(
|
||||
'connection_data_length_change',
|
||||
event.connection_handle,
|
||||
event.max_tx_octets,
|
||||
event.max_tx_time,
|
||||
event.max_rx_octets,
|
||||
event.max_rx_time
|
||||
)
|
||||
|
||||
def on_hci_authentication_complete_event(self, event):
|
||||
# Notify the client
|
||||
if event.status == HCI_SUCCESS:
|
||||
self.emit('connection_authentication_complete', event.connection_handle)
|
||||
else:
|
||||
self.emit('connection_authentication_failure', event.connection_handle, event.status)
|
||||
|
||||
def on_hci_encryption_change_event(self, event):
|
||||
# Notify the client
|
||||
if event.status == HCI_SUCCESS:
|
||||
self.emit('connection_encryption_change', event.connection_handle, event.encryption_enabled)
|
||||
else:
|
||||
self.emit('connection_encryption_failure', event.connection_handle, event.status)
|
||||
|
||||
def on_hci_encryption_key_refresh_complete_event(self, event):
|
||||
# Notify the client
|
||||
if event.status == HCI_SUCCESS:
|
||||
self.emit('connection_encryption_key_refresh', event.connection_handle)
|
||||
else:
|
||||
self.emit('connection_encryption_key_refresh_failure', event.connection_handle, event.status)
|
||||
|
||||
def on_hci_link_supervision_timeout_changed_event(self, event):
|
||||
pass
|
||||
|
||||
def on_hci_max_slots_change_event(self, event):
|
||||
pass
|
||||
|
||||
def on_hci_page_scan_repetition_mode_change_event(self, event):
|
||||
pass
|
||||
|
||||
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)}')
|
||||
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)}')
|
||||
|
||||
def on_hci_pin_code_request_event(self, event):
|
||||
# For now, just refuse all requests
|
||||
# TODO: delegate the decision
|
||||
self.send_command_sync(
|
||||
HCI_PIN_Code_Request_Negative_Reply_Command(
|
||||
bd_addr = event.bd_addr
|
||||
)
|
||||
)
|
||||
|
||||
def on_hci_link_key_request_event(self, event):
|
||||
async def send_link_key():
|
||||
if self.link_key_provider is None:
|
||||
logger.debug('no link key provider')
|
||||
link_key = None
|
||||
else:
|
||||
link_key = await 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
|
||||
)
|
||||
else:
|
||||
response = HCI_Link_Key_Request_Negative_Reply_Command(
|
||||
bd_addr = event.bd_addr
|
||||
)
|
||||
|
||||
await self.send_command(response)
|
||||
|
||||
asyncio.create_task(send_link_key())
|
||||
|
||||
def on_hci_io_capability_request_event(self, event):
|
||||
# For now, just return NoInputNoOutput and no MITM
|
||||
# TODO: delegate the decision
|
||||
self.send_command_sync(
|
||||
HCI_IO_Capability_Request_Reply_Command(
|
||||
bd_addr = event.bd_addr,
|
||||
io_capability = HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
|
||||
oob_data_present = 0x00,
|
||||
authentication_requirements = 0x00 # 0x02 # FIXME: testing only
|
||||
)
|
||||
)
|
||||
|
||||
def on_hci_io_capability_response_event(self, event):
|
||||
pass
|
||||
|
||||
def on_hci_user_confirmation_request_event(self, event):
|
||||
# For now, just confirm everything
|
||||
# TODO: delegate the decision
|
||||
self.send_command_sync(
|
||||
HCI_User_Confirmation_Request_Reply_Command(bd_addr = event.bd_addr)
|
||||
)
|
||||
|
||||
def on_hci_inquiry_complete_event(self, event):
|
||||
self.emit('inquiry_complete')
|
||||
|
||||
def on_hci_inquiry_result_with_rssi_event(self, event):
|
||||
for response in event.responses:
|
||||
self.emit(
|
||||
'inquiry_result',
|
||||
response.bd_addr,
|
||||
response.class_of_device,
|
||||
b'',
|
||||
response.rssi
|
||||
)
|
||||
|
||||
def on_hci_extended_inquiry_result_event(self, event):
|
||||
self.emit(
|
||||
'inquiry_result',
|
||||
event.bd_addr,
|
||||
event.class_of_device,
|
||||
event.extended_inquiry_response,
|
||||
event.rssi
|
||||
)
|
||||
273
bumble/keys.py
Normal file
273
bumble/keys.py
Normal file
@@ -0,0 +1,273 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Keys and Key Storage
|
||||
#
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import logging
|
||||
import os
|
||||
import json
|
||||
from colors import color
|
||||
|
||||
from .hci import Address
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class PairingKeys:
|
||||
class Key:
|
||||
def __init__(self, value, authenticated=False, ediv=None, rand=None):
|
||||
self.value = value
|
||||
self.authenticated = authenticated
|
||||
self.ediv = ediv
|
||||
self.rand = rand
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, key_dict):
|
||||
value = bytes.fromhex(key_dict['value'])
|
||||
authenticated = key_dict.get('authenticated', False)
|
||||
ediv = key_dict.get('ediv')
|
||||
rand = key_dict.get('rand')
|
||||
if rand is not None:
|
||||
rand = bytes.fromhex(rand)
|
||||
|
||||
return cls(value, authenticated, ediv, rand)
|
||||
|
||||
def to_dict(self):
|
||||
key_dict = {'value': self.value.hex(), 'authenticated': self.authenticated}
|
||||
if self.ediv is not None:
|
||||
key_dict['ediv'] = self.ediv
|
||||
if self.rand is not None:
|
||||
key_dict['rand'] = self.rand.hex()
|
||||
|
||||
return key_dict
|
||||
|
||||
def __init__(self):
|
||||
self.address_type = None
|
||||
self.ltk = None
|
||||
self.ltk_central = None
|
||||
self.ltk_peripheral = None
|
||||
self.irk = None
|
||||
self.csrk = None
|
||||
self.link_key = None # Classic
|
||||
|
||||
@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)
|
||||
|
||||
@staticmethod
|
||||
def from_dict(keys_dict):
|
||||
keys = PairingKeys()
|
||||
|
||||
keys.address_type = keys_dict.get('address_type')
|
||||
keys.ltk = PairingKeys.key_from_dict(keys_dict, 'ltk')
|
||||
keys.ltk_central = PairingKeys.key_from_dict(keys_dict, 'ltk_central')
|
||||
keys.ltk_peripheral = PairingKeys.key_from_dict(keys_dict, 'ltk_peripheral')
|
||||
keys.irk = PairingKeys.key_from_dict(keys_dict, 'irk')
|
||||
keys.csrk = PairingKeys.key_from_dict(keys_dict, 'csrk')
|
||||
keys.link_key = PairingKeys.key_from_dict(keys_dict, 'link_key')
|
||||
|
||||
return keys
|
||||
|
||||
def to_dict(self):
|
||||
keys = {}
|
||||
|
||||
if self.address_type is not None:
|
||||
keys['address_type'] = self.address_type
|
||||
|
||||
if self.ltk is not None:
|
||||
keys['ltk'] = self.ltk.to_dict()
|
||||
|
||||
if self.ltk_central is not None:
|
||||
keys['ltk_central'] = self.ltk_central.to_dict()
|
||||
|
||||
if self.ltk_peripheral is not None:
|
||||
keys['ltk_peripheral'] = self.ltk_peripheral.to_dict()
|
||||
|
||||
if self.irk is not None:
|
||||
keys['irk'] = self.irk.to_dict()
|
||||
|
||||
if self.csrk is not None:
|
||||
keys['csrk'] = self.csrk.to_dict()
|
||||
|
||||
if self.link_key is not None:
|
||||
keys['link_key'] = self.link_key.to_dict()
|
||||
|
||||
return keys
|
||||
|
||||
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 (key_property, key_value) in value.items():
|
||||
print(f'{prefix} {color(key_property, "green")}: {key_value}')
|
||||
else:
|
||||
print(f'{prefix}{color(property, "cyan")}: {value}')
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class KeyStore:
|
||||
async def delete(self, name):
|
||||
pass
|
||||
|
||||
async def update(self, name, keys):
|
||||
pass
|
||||
|
||||
async def get(self, name):
|
||||
return PairingKeys()
|
||||
|
||||
async def get_all(self):
|
||||
return []
|
||||
|
||||
async def get_resolving_keys(self):
|
||||
all_keys = await self.get_all()
|
||||
resolving_keys = []
|
||||
for (name, keys) in all_keys:
|
||||
if keys.irk is not None:
|
||||
if keys.address_type is None:
|
||||
address_type = Address.RANDOM_DEVICE_ADDRESS
|
||||
else:
|
||||
address_type = keys.address_type
|
||||
resolving_keys.append((keys.irk.value, Address(name, address_type)))
|
||||
|
||||
return resolving_keys
|
||||
|
||||
async def print(self, prefix=''):
|
||||
entries = await self.get_all()
|
||||
separator = ''
|
||||
for (name, keys) in entries:
|
||||
print(separator + prefix + color(name, 'yellow'))
|
||||
keys.print(prefix = prefix + ' ')
|
||||
separator = '\n'
|
||||
|
||||
@staticmethod
|
||||
def create_for_device(device_config):
|
||||
if device_config.keystore is None:
|
||||
return None
|
||||
|
||||
keystore_type = device_config.keystore.split(':', 1)[0]
|
||||
if keystore_type == 'JsonKeyStore':
|
||||
return JsonKeyStore.from_device_config(device_config)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class JsonKeyStore(KeyStore):
|
||||
APP_NAME = 'Bumble'
|
||||
APP_AUTHOR = 'Google'
|
||||
KEYS_DIR = 'Pairing'
|
||||
DEFAULT_NAMESPACE = '__DEFAULT__'
|
||||
|
||||
def __init__(self, namespace, filename=None):
|
||||
self.namespace = namespace if namespace is not None else self.DEFAULT_NAMESPACE
|
||||
|
||||
if filename is None:
|
||||
# Use a default for the current user
|
||||
import appdirs
|
||||
self.directory_name = os.path.join(
|
||||
appdirs.user_data_dir(self.APP_NAME, self.APP_AUTHOR),
|
||||
self.KEYS_DIR
|
||||
)
|
||||
json_filename = f'{self.namespace}.json'.lower().replace(':', '-')
|
||||
self.filename = os.path.join(self.directory_name, json_filename)
|
||||
else:
|
||||
self.filename = filename
|
||||
self.directory_name = os.path.dirname(os.path.abspath(self.filename))
|
||||
|
||||
logger.debug(f'JSON keystore: {self.filename}')
|
||||
|
||||
@staticmethod
|
||||
def from_device_config(device_config):
|
||||
params = device_config.keystore.split(':', 1)[1:]
|
||||
namespace = str(device_config.address)
|
||||
if params:
|
||||
filename = params[1]
|
||||
else:
|
||||
filename = None
|
||||
|
||||
return JsonKeyStore(namespace, filename)
|
||||
|
||||
async def load(self):
|
||||
try:
|
||||
with open(self.filename, 'r') as json_file:
|
||||
return json.load(json_file)
|
||||
except FileNotFoundError:
|
||||
return {}
|
||||
|
||||
async def save(self, db):
|
||||
# Create the directory if it doesn't exist
|
||||
if not os.path.exists(self.directory_name):
|
||||
os.makedirs(self.directory_name, exist_ok=True)
|
||||
|
||||
# Save to a temporary file
|
||||
temp_filename = self.filename + '.tmp'
|
||||
with open(temp_filename, 'w') as output:
|
||||
json.dump(db, output, sort_keys=True, indent=4)
|
||||
|
||||
# Atomically replace the previous file
|
||||
os.rename(temp_filename, self.filename)
|
||||
|
||||
async def delete(self, name):
|
||||
db = await self.load()
|
||||
|
||||
namespace = db.get(self.namespace)
|
||||
if namespace is None:
|
||||
raise KeyError(name)
|
||||
|
||||
del namespace[name]
|
||||
await self.save(db)
|
||||
|
||||
async def update(self, name, keys):
|
||||
db = await self.load()
|
||||
|
||||
namespace = db.setdefault(self.namespace, {})
|
||||
namespace[name] = keys.to_dict()
|
||||
|
||||
await self.save(db)
|
||||
|
||||
async def get_all(self):
|
||||
db = await self.load()
|
||||
|
||||
namespace = db.get(self.namespace)
|
||||
if namespace is None:
|
||||
return []
|
||||
|
||||
return [(name, PairingKeys.from_dict(keys)) for (name, keys) in namespace.items()]
|
||||
|
||||
async def get(self, name):
|
||||
db = await self.load()
|
||||
|
||||
namespace = db.get(self.namespace)
|
||||
if namespace is None:
|
||||
return None
|
||||
|
||||
keys = namespace.get(name)
|
||||
if keys is None:
|
||||
return None
|
||||
|
||||
return PairingKeys.from_dict(keys)
|
||||
1079
bumble/l2cap.py
Normal file
1079
bumble/l2cap.py
Normal file
File diff suppressed because it is too large
Load Diff
360
bumble/link.py
Normal file
360
bumble/link.py
Normal file
@@ -0,0 +1,360 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import logging
|
||||
import asyncio
|
||||
import websockets
|
||||
from functools import partial
|
||||
from colors import color
|
||||
|
||||
from bumble.hci import (
|
||||
Address,
|
||||
HCI_SUCCESS,
|
||||
HCI_CONNECTION_ACCEPT_TIMEOUT_ERROR,
|
||||
HCI_CONNECTION_TIMEOUT_ERROR
|
||||
)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Utils
|
||||
# -----------------------------------------------------------------------------
|
||||
def parse_parameters(params_str):
|
||||
result = {}
|
||||
for param_str in params_str.split(','):
|
||||
if '=' in param_str:
|
||||
key, value = param_str.split('=')
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# TODO: add more support for various LL exchanges (see Vol 6, Part B - 2.4 DATA CHANNEL PDU)
|
||||
# -----------------------------------------------------------------------------
|
||||
class LocalLink:
|
||||
'''
|
||||
Link bus for controllers to communicate with each other
|
||||
'''
|
||||
|
||||
def __init__(self):
|
||||
self.controllers = set()
|
||||
self.pending_connection = None
|
||||
|
||||
def add_controller(self, controller):
|
||||
logger.debug(f'new controller: {controller}')
|
||||
self.controllers.add(controller)
|
||||
|
||||
def remove_controller(self, controller):
|
||||
self.controllers.remove(controller)
|
||||
|
||||
def find_controller(self, address):
|
||||
for controller in self.controllers:
|
||||
if controller.random_address == address:
|
||||
return controller
|
||||
return None
|
||||
|
||||
def on_address_changed(self, controller):
|
||||
pass
|
||||
|
||||
def get_pending_connection(self):
|
||||
return self.pending_connection
|
||||
|
||||
def send_advertising_data(self, sender_address, data):
|
||||
# Send the advertising data to all controllers, except the sender
|
||||
for controller in self.controllers:
|
||||
if controller.random_address != sender_address:
|
||||
controller.on_link_advertising_data(sender_address, data)
|
||||
|
||||
def send_acl_data(self, sender_address, destination_address, data):
|
||||
# Send the data to the first controller with a matching address
|
||||
if controller := self.find_controller(destination_address):
|
||||
controller.on_link_acl_data(sender_address, data)
|
||||
|
||||
def on_connection_complete(self):
|
||||
# Check that we expect this call
|
||||
if not self.pending_connection:
|
||||
logger.warning('on_connection_complete with no pending connection')
|
||||
return
|
||||
|
||||
central_address, le_create_connection_command = self.pending_connection
|
||||
self.pending_connection = None
|
||||
|
||||
# Find the controller that initiated the connection
|
||||
if not (central_controller := self.find_controller(central_address)):
|
||||
logger.warning('!!! Initiating controller not found')
|
||||
return
|
||||
|
||||
# Connect to the first controller with a matching address
|
||||
if peripheral_controller := self.find_controller(le_create_connection_command.peer_address):
|
||||
central_controller.on_link_peripheral_connection_complete(le_create_connection_command, HCI_SUCCESS)
|
||||
peripheral_controller.on_link_central_connected(central_address)
|
||||
return
|
||||
|
||||
# No peripheral found
|
||||
central_controller.on_link_peripheral_connection_complete(
|
||||
le_create_connection_command,
|
||||
HCI_CONNECTION_ACCEPT_TIMEOUT_ERROR
|
||||
)
|
||||
|
||||
def connect(self, central_address, le_create_connection_command):
|
||||
logger.debug(f'$$$ CONNECTION {central_address} -> {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)
|
||||
|
||||
def on_disconnection_complete(self, central_address, peripheral_address, disconnect_command):
|
||||
# Find the controller that initiated the disconnection
|
||||
if not (central_controller := self.find_controller(central_address)):
|
||||
logger.warning('!!! Initiating controller not found')
|
||||
return
|
||||
|
||||
# Disconnect from the first controller with a matching address
|
||||
if peripheral_controller := self.find_controller(peripheral_address):
|
||||
peripheral_controller.on_link_central_disconnected(central_address, disconnect_command.reason)
|
||||
|
||||
central_controller.on_link_peripheral_disconnection_complete(disconnect_command, HCI_SUCCESS)
|
||||
|
||||
def disconnect(self, central_address, peripheral_address, disconnect_command):
|
||||
logger.debug(f'$$$ DISCONNECTION {central_address} -> {peripheral_address}: reason = {disconnect_command.reason}')
|
||||
args = [central_address, peripheral_address, disconnect_command]
|
||||
asyncio.get_running_loop().call_soon(self.on_disconnection_complete, *args)
|
||||
|
||||
def on_connection_encrypted(self, central_address, peripheral_address, rand, ediv, ltk):
|
||||
logger.debug(f'*** ENCRYPTION {central_address} -> {peripheral_address}')
|
||||
|
||||
if central_controller := self.find_controller(central_address):
|
||||
central_controller.on_link_encrypted(peripheral_address, rand, ediv, ltk)
|
||||
|
||||
if peripheral_controller := self.find_controller(peripheral_address):
|
||||
peripheral_controller.on_link_encrypted(central_address, rand, ediv, ltk)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class RemoteLink:
|
||||
'''
|
||||
A Link implementation that communicates with other virtual controllers via a
|
||||
WebSocket relay
|
||||
'''
|
||||
def __init__(self, uri):
|
||||
self.controller = None
|
||||
self.uri = uri
|
||||
self.execution_queue = asyncio.Queue()
|
||||
self.websocket = asyncio.get_running_loop().create_future()
|
||||
self.rpc_result = None
|
||||
self.pending_connection = None
|
||||
self.central_connections = set() # List of addresses that we have connected to
|
||||
self.peripheral_connections = set() # List of addresses that have connected to us
|
||||
|
||||
# Connect and run asynchronously
|
||||
asyncio.create_task(self.run_connection())
|
||||
asyncio.create_task(self.run_executor_loop())
|
||||
|
||||
def add_controller(self, controller):
|
||||
if self.controller:
|
||||
raise ValueError('controller already set')
|
||||
self.controller = controller
|
||||
|
||||
def remove_controller(self, controller):
|
||||
if self.controller != controller:
|
||||
raise ValueError('controller mismatch')
|
||||
self.controller = None
|
||||
|
||||
def get_pending_connection(self):
|
||||
return self.pending_connection
|
||||
|
||||
async def wait_until_connected(self):
|
||||
await self.websocket
|
||||
|
||||
def execute(self, async_function):
|
||||
self.execution_queue.put_nowait(async_function())
|
||||
|
||||
async def run_executor_loop(self):
|
||||
logger.debug('executor loop starting')
|
||||
while True:
|
||||
item = await self.execution_queue.get()
|
||||
try:
|
||||
await item
|
||||
except Exception as error:
|
||||
logger.warning(f'{color("!!! Exception in async handler:", "red")} {error}')
|
||||
|
||||
async def run_connection(self):
|
||||
# Connect to the relay
|
||||
logger.debug(f'connecting to {self.uri}')
|
||||
websocket = await websockets.connect(self.uri)
|
||||
self.websocket.set_result(websocket)
|
||||
logger.debug(f'connected to {self.uri}')
|
||||
|
||||
while True:
|
||||
message = await websocket.recv()
|
||||
logger.debug(f'received message: {message}')
|
||||
keyword, *payload = message.split(':', 1)
|
||||
|
||||
handler_name = f'on_{keyword}_received'
|
||||
handler = getattr(self, handler_name, None)
|
||||
if handler:
|
||||
await handler(payload[0] if payload else None)
|
||||
|
||||
def close(self):
|
||||
if self.websocket.done():
|
||||
logger.debug('closing websocket')
|
||||
websocket = self.websocket.result()
|
||||
asyncio.create_task(websocket.close())
|
||||
|
||||
async def on_result_received(self, result):
|
||||
if self.rpc_result:
|
||||
self.rpc_result.set_result(result)
|
||||
|
||||
async def on_left_received(self, address):
|
||||
if address in self.central_connections:
|
||||
self.controller.on_link_peripheral_disconnected(Address(address))
|
||||
self.central_connections.remove(address)
|
||||
|
||||
if address in self.peripheral_connections:
|
||||
self.controller.on_link_central_disconnected(address, HCI_CONNECTION_TIMEOUT_ERROR)
|
||||
self.peripheral_connections.remove(address)
|
||||
|
||||
async def on_unreachable_received(self, target):
|
||||
await self.on_left_received(target)
|
||||
|
||||
async def on_message_received(self, message):
|
||||
sender, *payload = message.split('/', 1)
|
||||
if payload:
|
||||
keyword, *payload = payload[0].split(':', 1)
|
||||
handler_name = f'on_{keyword}_message_received'
|
||||
handler = getattr(self, handler_name, None)
|
||||
if handler:
|
||||
await handler(sender, payload[0] if payload else None)
|
||||
|
||||
async def on_advertisement_message_received(self, sender, advertisement):
|
||||
try:
|
||||
self.controller.on_link_advertising_data(Address(sender), bytes.fromhex(advertisement))
|
||||
except Exception:
|
||||
logger.exception('exception')
|
||||
|
||||
async def on_acl_message_received(self, sender, acl_data):
|
||||
try:
|
||||
self.controller.on_link_acl_data(Address(sender), bytes.fromhex(acl_data))
|
||||
except Exception:
|
||||
logger.exception('exception')
|
||||
|
||||
async def on_connect_message_received(self, sender, _):
|
||||
# Remember the connection
|
||||
self.peripheral_connections.add(sender)
|
||||
|
||||
# Notify the controller
|
||||
logger.debug(f'connection from central {sender}')
|
||||
self.controller.on_link_central_connected(Address(sender))
|
||||
|
||||
# Accept the connection by responding to it
|
||||
await self.send_targetted_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')
|
||||
return
|
||||
|
||||
# Remember the connection
|
||||
self.central_connections.add(sender)
|
||||
|
||||
# Notify the controller
|
||||
logger.debug(f'connected to peripheral {self.pending_connection.peer_address}')
|
||||
self.controller.on_link_peripheral_connection_complete(self.pending_connection, HCI_SUCCESS)
|
||||
|
||||
async def on_disconnect_message_received(self, sender, message):
|
||||
# Notify the controller
|
||||
params = parse_parameters(message)
|
||||
reason = int(params.get('reason', str(HCI_CONNECTION_TIMEOUT_ERROR)))
|
||||
self.controller.on_link_central_disconnected(Address(sender), reason)
|
||||
|
||||
# Forget the connection
|
||||
if sender in self.peripheral_connections:
|
||||
self.peripheral_connections.remove(sender)
|
||||
|
||||
async def on_encrypted_message_received(self, sender, message):
|
||||
# TODO parse params to get real args
|
||||
self.controller.on_link_encrypted(Address(sender), bytes(8), 0, bytes(16))
|
||||
|
||||
async def send_rpc_command(self, command):
|
||||
# Ensure we have a connection
|
||||
websocket = await self.websocket
|
||||
|
||||
# Create a future value to hold the eventual result
|
||||
assert(self.rpc_result is None)
|
||||
self.rpc_result = asyncio.get_running_loop().create_future()
|
||||
|
||||
# Send the command
|
||||
await websocket.send(command)
|
||||
|
||||
# Wait for the result
|
||||
rpc_result = await self.rpc_result
|
||||
self.rpc_result = None
|
||||
logger.debug(f'rpc_result: {rpc_result}')
|
||||
|
||||
# TODO: parse the result
|
||||
|
||||
async def send_targetted_message(self, target, message):
|
||||
# Ensure we have a connection
|
||||
websocket = await self.websocket
|
||||
|
||||
# Send the message
|
||||
await websocket.send(f'@{target} {message}')
|
||||
|
||||
async def notify_address_changed(self):
|
||||
await self.send_rpc_command(f'/set-address {self.controller.random_address}')
|
||||
|
||||
def on_address_changed(self, controller):
|
||||
logger.info(f'address changed for {controller}: {controller.random_address}')
|
||||
|
||||
# Notify the relay of the change
|
||||
self.execute(self.notify_address_changed)
|
||||
|
||||
async def send_advertising_data_to_relay(self, data):
|
||||
await self.send_targetted_message('*', f'advertisement:{data.hex()}')
|
||||
|
||||
def send_advertising_data(self, sender_address, 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()}')
|
||||
|
||||
def send_acl_data(self, sender_address, 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')
|
||||
|
||||
def connect(self, central_address, le_create_connection_command):
|
||||
if self.pending_connection:
|
||||
logger.warn('connection already pending')
|
||||
return
|
||||
self.pending_connection = le_create_connection_command
|
||||
self.execute(partial(self.send_connection_request_to_relay, str(le_create_connection_command.peer_address)))
|
||||
|
||||
def on_disconnection_complete(self, disconnect_command):
|
||||
self.controller.on_link_peripheral_disconnection_complete(disconnect_command, HCI_SUCCESS)
|
||||
|
||||
def disconnect(self, central_address, peripheral_address, disconnect_command):
|
||||
logger.debug(f'disconnect {central_address} -> {peripheral_address}: reason = {disconnect_command.reason}')
|
||||
self.execute(partial(self.send_targetted_message, peripheral_address, f'disconnect:reason={disconnect_command.reason}'))
|
||||
asyncio.get_running_loop().call_soon(self.on_disconnection_complete, disconnect_command)
|
||||
|
||||
def on_connection_encrypted(self, central_address, 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, peripheral_address, f'encrypted:ltk={ltk.hex()}'))
|
||||
840
bumble/rfcomm.py
Normal file
840
bumble/rfcomm.py
Normal file
@@ -0,0 +1,840 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import logging
|
||||
import asyncio
|
||||
from colors import color
|
||||
|
||||
from .utils import EventEmitter
|
||||
from .core import InvalidStateError, ProtocolError, ConnectionError
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
RFCOMM_PSM = 0x0003
|
||||
|
||||
|
||||
# Frame types
|
||||
RFCOMM_SABM_FRAME = 0x2F # Control field [1,1,1,1,_,1,0,0] LSB-first
|
||||
RFCOMM_UA_FRAME = 0x63 # Control field [0,1,1,0,_,0,1,1] LSB-first
|
||||
RFCOMM_DM_FRAME = 0x0F # Control field [1,1,1,1,_,0,0,0] LSB-first
|
||||
RFCOMM_DISC_FRAME = 0x43 # Control field [0,1,0,_,0,0,1,1] LSB-first
|
||||
RFCOMM_UIH_FRAME = 0xEF # Control field [1,1,1,_,1,1,1,1] LSB-first
|
||||
RFCOMM_UI_FRAME = 0x03 # Control field [0,0,0,_,0,0,1,1] LSB-first
|
||||
|
||||
RFCOMM_FRAME_TYPE_NAMES = {
|
||||
RFCOMM_SABM_FRAME: 'SABM',
|
||||
RFCOMM_UA_FRAME: 'UA',
|
||||
RFCOMM_DM_FRAME: 'DM',
|
||||
RFCOMM_DISC_FRAME: 'DISC',
|
||||
RFCOMM_UIH_FRAME: 'UIH',
|
||||
RFCOMM_UI_FRAME: 'UI'
|
||||
}
|
||||
|
||||
# MCC Types
|
||||
RFCOMM_MCC_PN_TYPE = 0x20
|
||||
RFCOMM_MCC_MSC_TYPE = 0x38
|
||||
|
||||
# FCS CRC
|
||||
CRC_TABLE = bytes([
|
||||
0X00, 0X91, 0XE3, 0X72, 0X07, 0X96, 0XE4, 0X75,
|
||||
0X0E, 0X9F, 0XED, 0X7C, 0X09, 0X98, 0XEA, 0X7B,
|
||||
0X1C, 0X8D, 0XFF, 0X6E, 0X1B, 0X8A, 0XF8, 0X69,
|
||||
0X12, 0X83, 0XF1, 0X60, 0X15, 0X84, 0XF6, 0X67,
|
||||
0X38, 0XA9, 0XDB, 0X4A, 0X3F, 0XAE, 0XDC, 0X4D,
|
||||
0X36, 0XA7, 0XD5, 0X44, 0X31, 0XA0, 0XD2, 0X43,
|
||||
0X24, 0XB5, 0XC7, 0X56, 0X23, 0XB2, 0XC0, 0X51,
|
||||
0X2A, 0XBB, 0XC9, 0X58, 0X2D, 0XBC, 0XCE, 0X5F,
|
||||
0X70, 0XE1, 0X93, 0X02, 0X77, 0XE6, 0X94, 0X05,
|
||||
0X7E, 0XEF, 0X9D, 0X0C, 0X79, 0XE8, 0X9A, 0X0B,
|
||||
0X6C, 0XFD, 0X8F, 0X1E, 0X6B, 0XFA, 0X88, 0X19,
|
||||
0X62, 0XF3, 0X81, 0X10, 0X65, 0XF4, 0X86, 0X17,
|
||||
0X48, 0XD9, 0XAB, 0X3A, 0X4F, 0XDE, 0XAC, 0X3D,
|
||||
0X46, 0XD7, 0XA5, 0X34, 0X41, 0XD0, 0XA2, 0X33,
|
||||
0X54, 0XC5, 0XB7, 0X26, 0X53, 0XC2, 0XB0, 0X21,
|
||||
0X5A, 0XCB, 0XB9, 0X28, 0X5D, 0XCC, 0XBE, 0X2F,
|
||||
0XE0, 0X71, 0X03, 0X92, 0XE7, 0X76, 0X04, 0X95,
|
||||
0XEE, 0X7F, 0X0D, 0X9C, 0XE9, 0X78, 0X0A, 0X9B,
|
||||
0XFC, 0X6D, 0X1F, 0X8E, 0XFB, 0X6A, 0X18, 0X89,
|
||||
0XF2, 0X63, 0X11, 0X80, 0XF5, 0X64, 0X16, 0X87,
|
||||
0XD8, 0X49, 0X3B, 0XAA, 0XDF, 0X4E, 0X3C, 0XAD,
|
||||
0XD6, 0X47, 0X35, 0XA4, 0XD1, 0X40, 0X32, 0XA3,
|
||||
0XC4, 0X55, 0X27, 0XB6, 0XC3, 0X52, 0X20, 0XB1,
|
||||
0XCA, 0X5B, 0X29, 0XB8, 0XCD, 0X5C, 0X2E, 0XBF,
|
||||
0X90, 0X01, 0X73, 0XE2, 0X97, 0X06, 0X74, 0XE5,
|
||||
0X9E, 0X0F, 0X7D, 0XEC, 0X99, 0X08, 0X7A, 0XEB,
|
||||
0X8C, 0X1D, 0X6F, 0XFE, 0X8B, 0X1A, 0X68, 0XF9,
|
||||
0X82, 0X13, 0X61, 0XF0, 0X85, 0X14, 0X66, 0XF7,
|
||||
0XA8, 0X39, 0X4B, 0XDA, 0XAF, 0X3E, 0X4C, 0XDD,
|
||||
0XA6, 0X37, 0X45, 0XD4, 0XA1, 0X30, 0X42, 0XD3,
|
||||
0XB4, 0X25, 0X57, 0XC6, 0XB3, 0X22, 0X50, 0XC1,
|
||||
0XBA, 0X2B, 0X59, 0XC8, 0XBD, 0X2C, 0X5E, 0XCF
|
||||
])
|
||||
|
||||
RFCOMM_DEFAULT_INITIAL_RX_CREDITS = 7
|
||||
RFCOMM_DEFAULT_PREFERRED_MTU = 1280
|
||||
|
||||
RFCOMM_DYNAMIC_CHANNEL_NUMBER_START = 1
|
||||
RFCOMM_DYNAMIC_CHANNEL_NUMBER_END = 30
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def fcs(buffer):
|
||||
fcs = 0xFF
|
||||
for byte in buffer:
|
||||
fcs = CRC_TABLE[fcs ^ byte]
|
||||
return 0xFF - fcs
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class RFCOMM_Frame:
|
||||
def __init__(self, type, c_r, dlci, p_f, information = b'', with_credits = False):
|
||||
self.type = type
|
||||
self.c_r = c_r
|
||||
self.dlci = dlci
|
||||
self.p_f = p_f
|
||||
self.information = information
|
||||
length = len(information)
|
||||
if with_credits:
|
||||
length -= 1
|
||||
if length > 0x7F:
|
||||
# 2-byte length indicator
|
||||
self.length = bytes([(length & 0x7F) << 1, (length >> 7) & 0xFF])
|
||||
else:
|
||||
# 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]))
|
||||
else:
|
||||
self.fcs = 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
|
||||
c_r = (data[0] >> 1) & 1
|
||||
length = data[1]
|
||||
if data[1] & 1:
|
||||
length >>= 1
|
||||
value = data[2:]
|
||||
else:
|
||||
length = (data[3] << 7) & (length >> 1)
|
||||
value = data[3:3 + length]
|
||||
|
||||
return (type, c_r, value)
|
||||
|
||||
@staticmethod
|
||||
def make_mcc(type, c_r, data):
|
||||
return bytes([(type << 2 | c_r << 1 | 1) & 0xFF, (len(data) & 0x7F) << 1 | 1]) + data
|
||||
|
||||
@staticmethod
|
||||
def sabm(c_r, dlci):
|
||||
return RFCOMM_Frame(RFCOMM_SABM_FRAME, c_r, dlci, 1)
|
||||
|
||||
@staticmethod
|
||||
def ua(c_r, dlci):
|
||||
return RFCOMM_Frame(RFCOMM_UA_FRAME, c_r, dlci, 1)
|
||||
|
||||
@staticmethod
|
||||
def dm(c_r, dlci):
|
||||
return RFCOMM_Frame(RFCOMM_DM_FRAME, c_r, dlci, 1)
|
||||
|
||||
@staticmethod
|
||||
def disc(c_r, dlci):
|
||||
return RFCOMM_Frame(RFCOMM_DISC_FRAME, c_r, dlci, 1)
|
||||
|
||||
@staticmethod
|
||||
def uih(c_r, dlci, information, p_f = 0):
|
||||
return RFCOMM_Frame(RFCOMM_UIH_FRAME, c_r, dlci, p_f, information, with_credits = (p_f == 1))
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(data):
|
||||
# Extract fields
|
||||
dlci = (data[0] >> 2) & 0x3F
|
||||
c_r = (data[0] >> 1) & 0x01
|
||||
type = data[1] & 0xEF
|
||||
p_f = (data[1] >> 4) & 0x01
|
||||
length = data[2]
|
||||
if length & 0x01:
|
||||
length >>= 1
|
||||
information = data[3:-1]
|
||||
else:
|
||||
length = (data[3] << 7) & (length >> 1)
|
||||
information = data[4:-1]
|
||||
fcs = data[-1]
|
||||
|
||||
# Construct the frame and check the CRC
|
||||
frame = RFCOMM_Frame(type, c_r, dlci, p_f, information)
|
||||
if frame.fcs != fcs:
|
||||
logger.warn(f'FCS mismatch: got {fcs:02X}, expected {frame.fcs:02X}')
|
||||
raise ValueError('fcs mismatch')
|
||||
|
||||
return frame
|
||||
|
||||
def __bytes__(self):
|
||||
return bytes([self.address, self.control]) + self.length + self.information + bytes([self.fcs])
|
||||
|
||||
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})'
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class RFCOMM_MCC_PN:
|
||||
def __init__(self, dlci, cl, priority, ack_timer, max_frame_size, max_retransmissions, window_size):
|
||||
self.dlci = dlci
|
||||
self.cl = cl
|
||||
self.priority = priority
|
||||
self.ack_timer = ack_timer
|
||||
self.max_frame_size = max_frame_size
|
||||
self.max_retransmissions = max_retransmissions
|
||||
self.window_size = window_size
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(data):
|
||||
return RFCOMM_MCC_PN(
|
||||
dlci = data[0],
|
||||
cl = data[1],
|
||||
priority = data[2],
|
||||
ack_timer = data[3],
|
||||
max_frame_size = data[4] | data[5] << 8,
|
||||
max_retransmissions = data[6],
|
||||
window_size = data[7]
|
||||
)
|
||||
|
||||
def __bytes__(self):
|
||||
return bytes([
|
||||
self.dlci & 0xFF,
|
||||
self.cl & 0xFF,
|
||||
self.priority & 0xFF,
|
||||
self.ack_timer & 0xFF,
|
||||
self.max_frame_size & 0xFF,
|
||||
(self.max_frame_size >> 8) & 0xFF,
|
||||
self.max_retransmissions & 0xFF,
|
||||
self.window_size & 0xFF
|
||||
])
|
||||
|
||||
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})'
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class RFCOMM_MCC_MSC:
|
||||
def __init__(self, dlci, fc, rtc, rtr, ic, dv):
|
||||
self.dlci = dlci
|
||||
self.fc = fc
|
||||
self.rtc = rtc
|
||||
self.rtr = rtr
|
||||
self.ic = ic
|
||||
self.dv = dv
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(data):
|
||||
return RFCOMM_MCC_MSC(
|
||||
dlci = data[0] >> 2,
|
||||
fc = data[1] >> 1 & 1,
|
||||
rtc = data[1] >> 2 & 1,
|
||||
rtr = data[1] >> 3 & 1,
|
||||
ic = data[1] >> 6 & 1,
|
||||
dv = data[1] >> 7 & 1
|
||||
)
|
||||
|
||||
def __bytes__(self):
|
||||
return bytes([
|
||||
(self.dlci << 2) | 3,
|
||||
1 | self.fc << 1 | self.rtc << 2 | self.rtr << 3 | self.ic << 6 | self.dv << 7
|
||||
])
|
||||
|
||||
def __str__(self):
|
||||
return f'MSC(dlci={self.dlci},fc={self.fc},rtc={self.rtc},rtr={self.rtr},ic={self.ic},dv={self.dv})'
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class DLC(EventEmitter):
|
||||
# States
|
||||
INIT = 0x00
|
||||
CONNECTING = 0x01
|
||||
CONNECTED = 0x02
|
||||
DISCONNECTING = 0x03
|
||||
DISCONNECTED = 0x04
|
||||
RESET = 0x05
|
||||
|
||||
STATE_NAMES = {
|
||||
INIT: 'INIT',
|
||||
CONNECTING: 'CONNECTING',
|
||||
CONNECTED: 'CONNECTED',
|
||||
DISCONNECTING: 'DISCONNECTING',
|
||||
DISCONNECTED: 'DISCONNECTED',
|
||||
RESET: 'RESET'
|
||||
}
|
||||
|
||||
def __init__(self, multiplexer, dlci, max_frame_size, initial_tx_credits):
|
||||
super().__init__()
|
||||
self.multiplexer = multiplexer
|
||||
self.dlci = dlci
|
||||
self.rx_credits = RFCOMM_DEFAULT_INITIAL_RX_CREDITS
|
||||
self.rx_threshold = self.rx_credits // 2
|
||||
self.tx_credits = initial_tx_credits
|
||||
self.tx_buffer = b''
|
||||
self.state = DLC.INIT
|
||||
self.role = multiplexer.role
|
||||
self.c_r = 1 if self.role == Multiplexer.INITIATOR else 0
|
||||
self.sink = None
|
||||
|
||||
# Compute the MTU
|
||||
max_overhead = 4 + 1 # header with 2-byte length + fcs
|
||||
self.mtu = min(max_frame_size, self.multiplexer.l2cap_channel.mtu - max_overhead)
|
||||
|
||||
@staticmethod
|
||||
def state_name(state):
|
||||
return DLC.STATE_NAMES[state]
|
||||
|
||||
def change_state(self, new_state):
|
||||
logger.debug(f'{self} state change -> {color(self.state_name(new_state), "magenta")}')
|
||||
self.state = new_state
|
||||
|
||||
def send_frame(self, frame):
|
||||
self.multiplexer.send_frame(frame)
|
||||
|
||||
def on_frame(self, frame):
|
||||
handler = getattr(self, f'on_{frame.type_name()}_frame'.lower())
|
||||
handler(frame)
|
||||
|
||||
def on_sabm_frame(self, frame):
|
||||
if self.state != DLC.CONNECTING:
|
||||
logger.warn(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))
|
||||
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):
|
||||
if self.state != DLC.CONNECTING:
|
||||
logger.warn(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))
|
||||
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.multiplexer.on_dlc_open_complete(self)
|
||||
|
||||
def on_dm_frame(self, frame):
|
||||
# TODO: handle all states
|
||||
pass
|
||||
|
||||
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))
|
||||
|
||||
def on_uih_frame(self, frame):
|
||||
data = frame.information
|
||||
if frame.p_f == 1:
|
||||
# With credits
|
||||
credits = frame.information[0]
|
||||
self.tx_credits += credits
|
||||
|
||||
logger.debug(f'<<< Credits [{self.dlci}]: 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()}')
|
||||
if len(data) and self.sink:
|
||||
self.sink(data)
|
||||
|
||||
# Update the credits
|
||||
if self.rx_credits > 0:
|
||||
self.rx_credits -= 1
|
||||
else:
|
||||
logger.warn(color('!!! received frame with no rx credits', 'red'))
|
||||
|
||||
# Check if there's anything to send (including credits)
|
||||
self.process_tx()
|
||||
|
||||
def on_ui_frame(self, frame):
|
||||
pass
|
||||
|
||||
def on_mcc_msc(self, c_r, msc):
|
||||
if c_r:
|
||||
# Command
|
||||
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))
|
||||
logger.debug(f'>>> MCC MSC Response: {msc}')
|
||||
self.send_frame(
|
||||
RFCOMM_Frame.uih(
|
||||
c_r = self.c_r,
|
||||
dlci = 0,
|
||||
information = mcc
|
||||
)
|
||||
)
|
||||
else:
|
||||
# Response
|
||||
logger.debug(f'<<< MCC MSC Response: {msc}')
|
||||
|
||||
def connect(self):
|
||||
if not self.state == DLC.INIT:
|
||||
raise InvalidStateError('invalid state')
|
||||
|
||||
self.change_state(DLC.CONNECTING)
|
||||
self.connection_result = asyncio.get_running_loop().create_future()
|
||||
self.send_frame(
|
||||
RFCOMM_Frame.sabm(
|
||||
c_r = self.c_r,
|
||||
dlci = self.dlci
|
||||
)
|
||||
)
|
||||
|
||||
def accept(self):
|
||||
if not self.state == DLC.INIT:
|
||||
raise InvalidStateError('invalid state')
|
||||
|
||||
pn = RFCOMM_MCC_PN(
|
||||
dlci = self.dlci,
|
||||
cl = 0xE0,
|
||||
priority = 7,
|
||||
ack_timer = 0,
|
||||
max_frame_size = RFCOMM_DEFAULT_PREFERRED_MTU,
|
||||
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))
|
||||
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)
|
||||
|
||||
def rx_credits_needed(self):
|
||||
if self.rx_credits <= self.rx_threshold:
|
||||
return RFCOMM_DEFAULT_INITIAL_RX_CREDITS - self.rx_credits
|
||||
else:
|
||||
return 0
|
||||
|
||||
def process_tx(self):
|
||||
# Send anything we can (or an empty frame if we need to send rx credits)
|
||||
rx_credits_needed = self.rx_credits_needed()
|
||||
while (self.tx_buffer and self.tx_credits > 0) or rx_credits_needed > 0:
|
||||
# Get the next chunk, up to MTU size
|
||||
if rx_credits_needed > 0:
|
||||
chunk = bytes([rx_credits_needed]) + self.tx_buffer[:self.mtu - 1]
|
||||
self.tx_buffer = self.tx_buffer[len(chunk) - 1:]
|
||||
self.rx_credits += rx_credits_needed
|
||||
tx_credit_spent = (len(chunk) > 1)
|
||||
else:
|
||||
chunk = self.tx_buffer[:self.mtu]
|
||||
self.tx_buffer = self.tx_buffer[len(chunk):]
|
||||
tx_credit_spent = True
|
||||
|
||||
# Update the tx credits
|
||||
# (no tx credit spent for empty frames that only contain rx credits)
|
||||
if tx_credit_spent:
|
||||
self.tx_credits -= 1
|
||||
|
||||
# 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}')
|
||||
self.send_frame(
|
||||
RFCOMM_Frame.uih(
|
||||
c_r = self.c_r,
|
||||
dlci = self.dlci,
|
||||
information = chunk,
|
||||
p_f = 1 if rx_credits_needed > 0 else 0
|
||||
)
|
||||
)
|
||||
|
||||
rx_credits_needed = 0
|
||||
|
||||
# Stream protocol
|
||||
def write(self, data):
|
||||
# We can only send bytes
|
||||
if type(data) != bytes:
|
||||
if type(data) == str:
|
||||
# Automatically convert strings to bytes using UTF-8
|
||||
data = data.encode('utf-8')
|
||||
else:
|
||||
raise ValueError('write only accept bytes or strings')
|
||||
|
||||
self.tx_buffer += data
|
||||
self.process_tx()
|
||||
|
||||
def drain(self):
|
||||
# TODO
|
||||
pass
|
||||
|
||||
def __str__(self):
|
||||
return f'DLC(dlci={self.dlci},state={self.state_name(self.state)})'
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Multiplexer(EventEmitter):
|
||||
# Roles
|
||||
INITIATOR = 0x00
|
||||
RESPONDER = 0x01
|
||||
|
||||
# States
|
||||
INIT = 0x00
|
||||
CONNECTING = 0x01
|
||||
CONNECTED = 0x02
|
||||
OPENING = 0x03
|
||||
DISCONNECTING = 0x04
|
||||
DISCONNECTED = 0x05
|
||||
RESET = 0x06
|
||||
|
||||
STATE_NAMES = {
|
||||
INIT: 'INIT',
|
||||
CONNECTING: 'CONNECTING',
|
||||
CONNECTED: 'CONNECTED',
|
||||
OPENING: 'OPENING',
|
||||
DISCONNECTING: 'DISCONNECTING',
|
||||
DISCONNECTED: 'DISCONNECTED',
|
||||
RESET: 'RESET'
|
||||
}
|
||||
|
||||
def __init__(self, l2cap_channel, role):
|
||||
super().__init__()
|
||||
self.role = role
|
||||
self.l2cap_channel = l2cap_channel
|
||||
self.state = Multiplexer.INIT
|
||||
self.dlcs = {} # DLCs, by DLCI
|
||||
self.connection_result = None
|
||||
self.disconnection_result = None
|
||||
self.open_result = None
|
||||
self.acceptor = None
|
||||
|
||||
# Become a sink for the L2CAP channel
|
||||
l2cap_channel.sink = self.on_pdu
|
||||
|
||||
@staticmethod
|
||||
def state_name(state):
|
||||
return Multiplexer.STATE_NAMES[state]
|
||||
|
||||
def change_state(self, new_state):
|
||||
logger.debug(f'{self} state change -> {color(self.state_name(new_state), "cyan")}')
|
||||
self.state = new_state
|
||||
|
||||
def send_frame(self, frame):
|
||||
logger.debug(f'>>> Multiplexer sending {frame}')
|
||||
self.l2cap_channel.send_pdu(frame)
|
||||
|
||||
def on_pdu(self, pdu):
|
||||
frame = RFCOMM_Frame.from_bytes(pdu)
|
||||
logger.debug(f'<<< Multiplexer received {frame}')
|
||||
|
||||
# Dispatch to this multiplexer or to a dlc, depending on the address
|
||||
if frame.dlci == 0:
|
||||
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
|
||||
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}')
|
||||
return
|
||||
dlc.on_frame(frame)
|
||||
|
||||
def on_frame(self, frame):
|
||||
handler = getattr(self, f'on_{frame.type_name()}_frame'.lower())
|
||||
handler(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):
|
||||
if self.state == Multiplexer.CONNECTING:
|
||||
self.change_state(Multiplexer.CONNECTED)
|
||||
if self.connection_result:
|
||||
self.connection_result.set_result(0)
|
||||
self.connection_result = None
|
||||
elif self.state == Multiplexer.DISCONNECTING:
|
||||
self.change_state(Multiplexer.DISCONNECTED)
|
||||
if self.disconnection_result:
|
||||
self.disconnection_result.set_result(None)
|
||||
self.disconnection_result = None
|
||||
|
||||
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))
|
||||
else:
|
||||
logger.warn(f'unexpected state for DM: {self}')
|
||||
|
||||
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)
|
||||
|
||||
if 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:
|
||||
mcs = RFCOMM_MCC_MSC.from_bytes(value)
|
||||
self.on_mcc_msc(c_r, mcs)
|
||||
|
||||
def on_ui_frame(self, frame):
|
||||
pass
|
||||
|
||||
def on_mcc_pn(self, c_r, pn):
|
||||
if c_r == 1:
|
||||
# Command
|
||||
logger.debug(f'<<< PN Command: {pn}')
|
||||
|
||||
# Check with the multiplexer if there's an acceptor for this channel
|
||||
if pn.dlci & 1:
|
||||
# Not expected, this is an initiator-side number
|
||||
# TODO: error out
|
||||
logger.warn(f'invalid DLCI: {pn.dlci}')
|
||||
else:
|
||||
if self.acceptor:
|
||||
channel_number = pn.dlci >> 1
|
||||
if self.acceptor(channel_number):
|
||||
# Create a new DLC
|
||||
dlc = DLC(self, pn.dlci, pn.max_frame_size, pn.window_size)
|
||||
self.dlcs[pn.dlci] = dlc
|
||||
|
||||
# Re-emit the handshake completion event
|
||||
dlc.on('open', lambda: self.emit('dlc', dlc))
|
||||
|
||||
# Respond to complete the handshake
|
||||
dlc.accept()
|
||||
else:
|
||||
# No acceptor, we're in Disconnected Mode
|
||||
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'))
|
||||
else:
|
||||
# Response
|
||||
logger.debug(f'>>> PN Response: {pn}')
|
||||
if self.state == Multiplexer.OPENING:
|
||||
dlc = DLC(self, pn.dlci, pn.max_frame_size, pn.window_size)
|
||||
self.dlcs[pn.dlci] = dlc
|
||||
dlc.connect()
|
||||
else:
|
||||
logger.warn('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}')
|
||||
return
|
||||
dlc.on_mcc_msc(c_r, msc)
|
||||
|
||||
async def connect(self):
|
||||
if self.state != Multiplexer.INIT:
|
||||
raise InvalidStateError('invalid state')
|
||||
|
||||
self.change_state(Multiplexer.CONNECTING)
|
||||
self.connection_result = asyncio.get_running_loop().create_future()
|
||||
self.send_frame(RFCOMM_Frame.sabm(c_r = 1, dlci = 0))
|
||||
return await self.connection_result
|
||||
|
||||
async def disconnect(self):
|
||||
if self.state != Multiplexer.CONNECTED:
|
||||
return
|
||||
|
||||
self.disconnection_result = asyncio.get_running_loop().create_future()
|
||||
self.change_state(Multiplexer.DISCONNECTING)
|
||||
self.send_frame(RFCOMM_Frame.disc(c_r = 1 if self.role == Multiplexer.INITIATOR else 0, dlci = 0))
|
||||
await self.disconnection_result
|
||||
|
||||
async def open_dlc(self, channel):
|
||||
if self.state != Multiplexer.CONNECTED:
|
||||
if self.state == Multiplexer.OPENING:
|
||||
raise InvalidStateError('open already in progress')
|
||||
else:
|
||||
raise InvalidStateError('not connected')
|
||||
|
||||
pn = RFCOMM_MCC_PN(
|
||||
dlci = channel << 1,
|
||||
cl = 0xF0,
|
||||
priority = 7,
|
||||
ack_timer = 0,
|
||||
max_frame_size = RFCOMM_DEFAULT_PREFERRED_MTU,
|
||||
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))
|
||||
logger.debug(f'>>> Sending MCC: {pn}')
|
||||
self.open_result = asyncio.get_running_loop().create_future()
|
||||
self.change_state(Multiplexer.OPENING)
|
||||
self.send_frame(
|
||||
RFCOMM_Frame.uih(
|
||||
c_r = 1 if self.role == Multiplexer.INITIATOR else 0,
|
||||
dlci = 0,
|
||||
information = mcc
|
||||
)
|
||||
)
|
||||
result = await self.open_result
|
||||
self.open_result = None
|
||||
return result
|
||||
|
||||
def on_dlc_open_complete(self, dlc):
|
||||
logger.debug(f'DLC [{dlc.dlci}] open complete')
|
||||
self.change_state(Multiplexer.CONNECTED)
|
||||
if self.open_result:
|
||||
self.open_result.set_result(dlc)
|
||||
|
||||
def __str__(self):
|
||||
return f'Multiplexer(state={self.state_name(self.state)})'
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Client:
|
||||
def __init__(self, device, connection):
|
||||
self.device = device
|
||||
self.connection = connection
|
||||
self.l2cap_channel = None
|
||||
self.multiplexer = None
|
||||
|
||||
async def start(self):
|
||||
# Create a new L2CAP connection
|
||||
try:
|
||||
self.l2cap_channel = await self.device.l2cap_channel_manager.connect(self.connection, RFCOMM_PSM)
|
||||
except ProtocolError as error:
|
||||
logger.warn(f'L2CAP connection failed: {error}')
|
||||
raise
|
||||
|
||||
# Create a mutliplexer to manage DLCs with the server
|
||||
self.multiplexer = Multiplexer(self.l2cap_channel, Multiplexer.INITIATOR)
|
||||
|
||||
# Connect the multiplexer
|
||||
await self.multiplexer.connect()
|
||||
|
||||
return self.multiplexer
|
||||
|
||||
async def shutdown(self):
|
||||
# Disconnect the multiplexer
|
||||
await self.multiplexer.disconnect()
|
||||
self.multiplexer = None
|
||||
|
||||
# Close the L2CAP channel
|
||||
# TODO
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Server(EventEmitter):
|
||||
def __init__(self, device):
|
||||
super().__init__()
|
||||
self.device = device
|
||||
self.multiplexer = None
|
||||
self.acceptors = {}
|
||||
|
||||
# Register ourselves with the L2CAP channel manager
|
||||
device.register_l2cap_server(RFCOMM_PSM, self.on_connection)
|
||||
|
||||
def listen(self, acceptor):
|
||||
# Find a free channel number
|
||||
for channel in range(RFCOMM_DYNAMIC_CHANNEL_NUMBER_START, RFCOMM_DYNAMIC_CHANNEL_NUMBER_END + 1):
|
||||
if channel not in self.acceptors:
|
||||
self.acceptors[channel] = acceptor
|
||||
return channel
|
||||
|
||||
# All channels used...
|
||||
return 0
|
||||
|
||||
def on_connection(self, l2cap_channel):
|
||||
logger.debug(f'+++ new L2CAP connection: {l2cap_channel}')
|
||||
l2cap_channel.on('open', lambda: self.on_l2cap_channel_open(l2cap_channel))
|
||||
|
||||
def on_l2cap_channel_open(self, l2cap_channel):
|
||||
logger.debug(f'$$$ L2CAP channel open: {l2cap_channel}')
|
||||
|
||||
# Create a new multiplexer for the channel
|
||||
multiplexer = Multiplexer(l2cap_channel, Multiplexer.RESPONDER)
|
||||
multiplexer.acceptor = self.accept_dlc
|
||||
multiplexer.on('dlc', self.on_dlc)
|
||||
|
||||
# Notify
|
||||
self.emit('start', multiplexer)
|
||||
|
||||
def accept_dlc(self, channel_number):
|
||||
return channel_number in self.acceptors
|
||||
|
||||
def on_dlc(self, dlc):
|
||||
logger.debug(f'@@@ new DLC connected: {dlc}')
|
||||
|
||||
# Let the acceptor know
|
||||
acceptor = self.acceptors.get(dlc.dlci >> 1)
|
||||
if acceptor:
|
||||
acceptor(dlc)
|
||||
1021
bumble/sdp.py
Normal file
1021
bumble/sdp.py
Normal file
File diff suppressed because it is too large
Load Diff
1514
bumble/smp.py
Normal file
1514
bumble/smp.py
Normal file
File diff suppressed because it is too large
Load Diff
95
bumble/transport/__init__.py
Normal file
95
bumble/transport/__init__.py
Normal file
@@ -0,0 +1,95 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import logging
|
||||
|
||||
from .common import Transport, AsyncPipeSink
|
||||
from ..link import RemoteLink
|
||||
from ..controller import Controller
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_transport(name):
|
||||
'''
|
||||
Open a transport by name.
|
||||
The name must be <type>:<parameters>
|
||||
Where <parameters> depend on the type (and may be empty for some types).
|
||||
The supported types are: serial,udp,tcp,pty,usb
|
||||
'''
|
||||
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:
|
||||
from .udp import open_udp_transport
|
||||
return await open_udp_transport(spec[0])
|
||||
elif 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:
|
||||
from .tcp_server import open_tcp_server_transport
|
||||
return await open_tcp_server_transport(spec[0])
|
||||
elif 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:
|
||||
from .ws_server import open_ws_server_transport
|
||||
return await open_ws_server_transport(spec[0])
|
||||
elif scheme == 'pty':
|
||||
from .pty import open_pty_transport
|
||||
return await open_pty_transport(spec[0] if spec else None)
|
||||
elif scheme == 'file':
|
||||
from .file import open_file_transport
|
||||
return await open_file_transport(spec[0] if spec else None)
|
||||
elif scheme == 'vhci':
|
||||
from .vhci import open_vhci_transport
|
||||
return await open_vhci_transport(spec[0] if spec else None)
|
||||
elif 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':
|
||||
from .usb import open_usb_transport
|
||||
return await open_usb_transport(spec[0] if spec else None)
|
||||
elif scheme == 'pyusb':
|
||||
from .pyusb import open_pyusb_transport
|
||||
return await open_pyusb_transport(spec[0] if spec else None)
|
||||
elif 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')
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_transport_or_link(name):
|
||||
if name.startswith('link-relay:'):
|
||||
link = RemoteLink(name[11:])
|
||||
await link.wait_until_connected()
|
||||
controller = Controller('remote', link = link)
|
||||
|
||||
class LinkTransport(Transport):
|
||||
async def close(self):
|
||||
link.close()
|
||||
|
||||
return LinkTransport(controller, AsyncPipeSink(controller))
|
||||
else:
|
||||
return await open_transport(name)
|
||||
107
bumble/transport/android_emulator.py
Normal file
107
bumble/transport/android_emulator.py
Normal file
@@ -0,0 +1,107 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import logging
|
||||
import grpc
|
||||
|
||||
from .common import PumpedTransport, PumpedPacketSource, PumpedPacketSink
|
||||
from .emulated_bluetooth_pb2_grpc import EmulatedBluetoothServiceStub
|
||||
from .emulated_bluetooth_packets_pb2 import HCIPacket
|
||||
from .emulated_bluetooth_vhci_pb2_grpc import VhciForwardingServiceStub
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_android_emulator_transport(spec):
|
||||
'''
|
||||
Open a transport connection to an Android emulator via its gRPC interface.
|
||||
The parameter string has this syntax:
|
||||
[<remote-host>:<remote-port>][,mode=<host|controller>]
|
||||
The <remote-host>:<remote-port> part is optional, it defaults to localhost:8554
|
||||
The mode=<mode> part is optional, it defaults to mode=host
|
||||
When the mode is set to 'controller', the connection is for a controller (i.e the
|
||||
Android Bluetooth stack will use the connected endpoint as its controller). When
|
||||
the mode is set to 'host', the connection is to the 'Root Canal' virtual controller
|
||||
that runs as part of the emulator, and used by the Android Bluetooth stack.
|
||||
|
||||
Examples:
|
||||
(empty string) --> connect as a host to the emulator on localhost:8554
|
||||
localhost:8555 --> connect as a host to the emulator on localhost:8555
|
||||
mode=controller --> connect as a controller to the emulator on localhost:8554
|
||||
'''
|
||||
|
||||
# Wrapper for I/O operations
|
||||
class HciDevice:
|
||||
def __init__(self, hci_device):
|
||||
self.hci_device = hci_device
|
||||
|
||||
async def read(self):
|
||||
packet = await self.hci_device.read()
|
||||
return bytes([packet.type]) + packet.packet
|
||||
|
||||
async def write(self, packet):
|
||||
await self.hci_device.write(
|
||||
HCIPacket(
|
||||
type = packet[0],
|
||||
packet = packet[1:]
|
||||
)
|
||||
)
|
||||
|
||||
# Parse the parameters
|
||||
mode = 'host'
|
||||
server_host = 'localhost'
|
||||
server_port = 8554
|
||||
if spec is not None:
|
||||
params = spec.split(',')
|
||||
for param in params:
|
||||
if param.startswith('mode='):
|
||||
mode = param.split('=')[1]
|
||||
elif ':' in param:
|
||||
server_host, server_port = param.split(':')
|
||||
else:
|
||||
raise ValueError('invalid parameter')
|
||||
|
||||
# Connect to the gRPC server
|
||||
server_address = f'{server_host}:{server_port}'
|
||||
logger.debug(f'connecting to gRPC server at {server_address}')
|
||||
channel = grpc.aio.insecure_channel(server_address)
|
||||
|
||||
if mode == 'host':
|
||||
# Connect as a host
|
||||
service = EmulatedBluetoothServiceStub(channel)
|
||||
hci_device = HciDevice(service.registerHCIDevice())
|
||||
elif mode == 'controller':
|
||||
# Connect as a controller
|
||||
service = VhciForwardingServiceStub(channel)
|
||||
hci_device = HciDevice(service.attachVhci())
|
||||
else:
|
||||
raise ValueError('invalid mode')
|
||||
|
||||
# Create the transport object
|
||||
transport = PumpedTransport(
|
||||
PumpedPacketSource(hci_device.read),
|
||||
PumpedPacketSink(hci_device.write),
|
||||
channel.close
|
||||
)
|
||||
transport.start()
|
||||
|
||||
return transport
|
||||
326
bumble/transport/common.py
Normal file
326
bumble/transport/common.py
Normal file
@@ -0,0 +1,326 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import struct
|
||||
import asyncio
|
||||
import logging
|
||||
from colors import color
|
||||
|
||||
from .. import hci
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Information needed to parse HCI packets with a generic parser:
|
||||
# For each packet type, the info represents:
|
||||
# (length-size, length-offset, unpack-type)
|
||||
HCI_PACKET_INFO = {
|
||||
hci.HCI_COMMAND_PACKET: (1, 2, 'B'),
|
||||
hci.HCI_ACL_DATA_PACKET: (2, 2, 'H'),
|
||||
hci.HCI_SYNCHRONOUS_DATA_PACKET: (1, 2, 'B'),
|
||||
hci.HCI_EVENT_PACKET: (1, 1, 'B')
|
||||
}
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class PacketPump:
|
||||
'''
|
||||
Pump HCI packets from a reader to a sink
|
||||
'''
|
||||
|
||||
def __init__(self, reader, sink):
|
||||
self.reader = reader
|
||||
self.sink = sink
|
||||
|
||||
async def run(self):
|
||||
while True:
|
||||
try:
|
||||
# Get a packet from the source
|
||||
packet = hci.HCI_Packet.from_bytes(await self.reader.next_packet())
|
||||
|
||||
# Deliver the packet to the sink
|
||||
self.sink.on_packet(packet)
|
||||
except Exception as error:
|
||||
logger.warning(f'!!! {error}')
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class PacketParser:
|
||||
'''
|
||||
In-line parser that accepts data and emits 'on_packet' when a full packet has been parsed
|
||||
'''
|
||||
NEED_TYPE = 0
|
||||
NEED_LENGTH = 1
|
||||
NEED_BODY = 2
|
||||
|
||||
def __init__(self, sink = None):
|
||||
self.sink = sink
|
||||
self.extended_packet_info = {}
|
||||
self.reset()
|
||||
|
||||
def reset(self):
|
||||
self.state = PacketParser.NEED_TYPE
|
||||
self.bytes_needed = 1
|
||||
self.packet = bytearray()
|
||||
self.packet_info = None
|
||||
|
||||
def feed_data(self, data):
|
||||
data_offset = 0
|
||||
data_left = len(data)
|
||||
while data_left and self.bytes_needed:
|
||||
consumed = min(self.bytes_needed, data_left)
|
||||
self.packet.extend(data[data_offset:data_offset + consumed])
|
||||
data_offset += consumed
|
||||
data_left -= consumed
|
||||
self.bytes_needed -= consumed
|
||||
|
||||
if self.bytes_needed == 0:
|
||||
if self.state == PacketParser.NEED_TYPE:
|
||||
packet_type = self.packet[0]
|
||||
self.packet_info = HCI_PACKET_INFO.get(packet_type) or self.extended_packet_info.get(packet_type)
|
||||
if self.packet_info is None:
|
||||
raise ValueError(f'invalid packet type {packet_type}')
|
||||
self.state = PacketParser.NEED_LENGTH
|
||||
self.bytes_needed = self.packet_info[0] + self.packet_info[1]
|
||||
elif self.state == PacketParser.NEED_LENGTH:
|
||||
body_length = struct.unpack_from(self.packet_info[2], self.packet, 1 + self.packet_info[1])[0]
|
||||
self.bytes_needed = body_length
|
||||
self.state = PacketParser.NEED_BODY
|
||||
|
||||
# Emit a packet if one is complete
|
||||
if self.state == PacketParser.NEED_BODY and not self.bytes_needed:
|
||||
if self.sink:
|
||||
try:
|
||||
self.sink.on_packet(bytes(self.packet))
|
||||
except Exception as error:
|
||||
logger.warning(color(f'!!! Exception in on_packet: {error}', 'red'))
|
||||
self.reset()
|
||||
|
||||
def set_packet_sink(self, sink):
|
||||
self.sink = sink
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class PacketReader:
|
||||
'''
|
||||
Reader that reads HCI packets from a sync source
|
||||
'''
|
||||
|
||||
def __init__(self, source):
|
||||
self.source = source
|
||||
|
||||
def next_packet(self):
|
||||
# Get the packet type
|
||||
packet_type = self.source.read(1)
|
||||
if len(packet_type) != 1:
|
||||
return None
|
||||
|
||||
# Get the packet info based on its type
|
||||
packet_info = HCI_PACKET_INFO.get(packet_type[0])
|
||||
if packet_info is None:
|
||||
raise ValueError(f'invalid packet type {packet_type} found')
|
||||
|
||||
# Read the header (that includes the length)
|
||||
header_size = packet_info[0] + packet_info[1]
|
||||
header = self.source.read(header_size)
|
||||
if len(header) != header_size:
|
||||
raise ValueError('packet too short')
|
||||
|
||||
# Read the body
|
||||
body_length = struct.unpack_from(packet_info[2], header, packet_info[1])[0]
|
||||
body = self.source.read(body_length)
|
||||
if len(body) != body_length:
|
||||
raise ValueError('packet too short')
|
||||
|
||||
return packet_type + header + body
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class AsyncPacketReader:
|
||||
'''
|
||||
Reader that reads HCI packets from an async source
|
||||
'''
|
||||
|
||||
def __init__(self, source):
|
||||
self.source = source
|
||||
|
||||
async def next_packet(self):
|
||||
# Get the packet type
|
||||
packet_type = await self.source.readexactly(1)
|
||||
|
||||
# Get the packet info based on its type
|
||||
packet_info = HCI_PACKET_INFO.get(packet_type[0])
|
||||
if packet_info is None:
|
||||
raise ValueError(f'invalid packet type {packet_type} found')
|
||||
|
||||
# Read the header (that includes the length)
|
||||
header_size = packet_info[0] + packet_info[1]
|
||||
header = await self.source.readexactly(header_size)
|
||||
|
||||
# Read the body
|
||||
body_length = struct.unpack_from(packet_info[2], header, packet_info[1])[0]
|
||||
body = await self.source.readexactly(body_length)
|
||||
|
||||
return packet_type + header + body
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class AsyncPipeSink:
|
||||
'''
|
||||
Sink that forwards packets asynchronously to another sink
|
||||
'''
|
||||
def __init__(self, sink):
|
||||
self.sink = sink
|
||||
self.loop = asyncio.get_running_loop()
|
||||
|
||||
def on_packet(self, packet):
|
||||
self.loop.call_soon(self.sink.on_packet, packet)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class ParserSource:
|
||||
"""
|
||||
Base class designed to be subclassed by transport-specific source classes
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.parser = PacketParser()
|
||||
self.terminated = asyncio.get_running_loop().create_future()
|
||||
|
||||
def set_packet_sink(self, sink):
|
||||
self.parser.set_packet_sink(sink)
|
||||
|
||||
async def wait_for_termination(self):
|
||||
return await self.terminated
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class StreamPacketSource(asyncio.Protocol, ParserSource):
|
||||
def data_received(self, data):
|
||||
self.parser.feed_data(data)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class StreamPacketSink:
|
||||
def __init__(self, transport):
|
||||
self.transport = transport
|
||||
|
||||
def on_packet(self, packet):
|
||||
self.transport.write(packet)
|
||||
|
||||
def close(self):
|
||||
self.transport.close()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Transport:
|
||||
def __init__(self, source, sink):
|
||||
self.source = source
|
||||
self.sink = sink
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *args):
|
||||
await self.close()
|
||||
|
||||
def __iter__(self):
|
||||
return iter((self.source, self.sink))
|
||||
|
||||
async def close(self):
|
||||
self.source.close()
|
||||
self.sink.close()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class PumpedPacketSource(ParserSource):
|
||||
def __init__(self, receive):
|
||||
super().__init__()
|
||||
self.receive_function = receive
|
||||
self.pump_task = None
|
||||
|
||||
def start(self):
|
||||
async def pump_packets():
|
||||
while True:
|
||||
try:
|
||||
packet = await self.receive_function()
|
||||
self.parser.feed_data(packet)
|
||||
except asyncio.exceptions.CancelledError:
|
||||
logger.debug('source pump task done')
|
||||
break
|
||||
except Exception as error:
|
||||
logger.warn(f'exception while waiting for packet: {error}')
|
||||
self.terminated.set_result(error)
|
||||
break
|
||||
|
||||
self.pump_task = asyncio.get_running_loop().create_task(pump_packets())
|
||||
|
||||
def close(self):
|
||||
if self.pump_task:
|
||||
self.pump_task.cancel()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class PumpedPacketSink:
|
||||
def __init__(self, send):
|
||||
self.send_function = send
|
||||
self.packet_queue = asyncio.Queue()
|
||||
self.pump_task = None
|
||||
|
||||
def on_packet(self, packet):
|
||||
self.packet_queue.put_nowait(packet)
|
||||
|
||||
def start(self):
|
||||
async def pump_packets():
|
||||
while True:
|
||||
try:
|
||||
packet = await self.packet_queue.get()
|
||||
await self.send_function(packet)
|
||||
except asyncio.exceptions.CancelledError:
|
||||
logger.debug('sink pump task done')
|
||||
break
|
||||
except Exception as error:
|
||||
logger.warn(f'exception while sending packet: {error}')
|
||||
break
|
||||
|
||||
self.pump_task = asyncio.get_running_loop().create_task(pump_packets())
|
||||
|
||||
def close(self):
|
||||
if self.pump_task:
|
||||
self.pump_task.cancel()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class PumpedTransport(Transport):
|
||||
def __init__(self, source, sink, close_function):
|
||||
super().__init__(source, sink)
|
||||
self.close_function = close_function
|
||||
|
||||
def start(self):
|
||||
self.source.start()
|
||||
self.sink.start()
|
||||
|
||||
async def close(self):
|
||||
await super().close()
|
||||
await self.close_function()
|
||||
52
bumble/transport/emulated_bluetooth_packets_pb2.py
Normal file
52
bumble/transport/emulated_bluetooth_packets_pb2.py
Normal file
@@ -0,0 +1,52 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: emulated_bluetooth_packets.proto
|
||||
"""Generated protocol buffer code."""
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||
from google.protobuf import message as _message
|
||||
from google.protobuf import reflection as _reflection
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n emulated_bluetooth_packets.proto\x12\x1b\x61ndroid.emulation.bluetooth\"\xfb\x01\n\tHCIPacket\x12?\n\x04type\x18\x01 \x01(\x0e\x32\x31.android.emulation.bluetooth.HCIPacket.PacketType\x12\x0e\n\x06packet\x18\x02 \x01(\x0c\"\x9c\x01\n\nPacketType\x12\x1b\n\x17PACKET_TYPE_UNSPECIFIED\x10\x00\x12\x1b\n\x17PACKET_TYPE_HCI_COMMAND\x10\x01\x12\x13\n\x0fPACKET_TYPE_ACL\x10\x02\x12\x13\n\x0fPACKET_TYPE_SCO\x10\x03\x12\x15\n\x11PACKET_TYPE_EVENT\x10\x04\x12\x13\n\x0fPACKET_TYPE_ISO\x10\x05\x42J\n\x1f\x63om.android.emulation.bluetoothP\x01\xf8\x01\x01\xa2\x02\x03\x41\x45\x42\xaa\x02\x1b\x41ndroid.Emulation.Bluetoothb\x06proto3')
|
||||
|
||||
|
||||
|
||||
_HCIPACKET = DESCRIPTOR.message_types_by_name['HCIPacket']
|
||||
_HCIPACKET_PACKETTYPE = _HCIPACKET.enum_types_by_name['PacketType']
|
||||
HCIPacket = _reflection.GeneratedProtocolMessageType('HCIPacket', (_message.Message,), {
|
||||
'DESCRIPTOR' : _HCIPACKET,
|
||||
'__module__' : 'emulated_bluetooth_packets_pb2'
|
||||
# @@protoc_insertion_point(class_scope:android.emulation.bluetooth.HCIPacket)
|
||||
})
|
||||
_sym_db.RegisterMessage(HCIPacket)
|
||||
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
|
||||
DESCRIPTOR._options = None
|
||||
DESCRIPTOR._serialized_options = b'\n\037com.android.emulation.bluetoothP\001\370\001\001\242\002\003AEB\252\002\033Android.Emulation.Bluetooth'
|
||||
_HCIPACKET._serialized_start=66
|
||||
_HCIPACKET._serialized_end=317
|
||||
_HCIPACKET_PACKETTYPE._serialized_start=161
|
||||
_HCIPACKET_PACKETTYPE._serialized_end=317
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
53
bumble/transport/emulated_bluetooth_pb2.py
Normal file
53
bumble/transport/emulated_bluetooth_pb2.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: emulated_bluetooth.proto
|
||||
"""Generated protocol buffer code."""
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||
from google.protobuf import message as _message
|
||||
from google.protobuf import reflection as _reflection
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
from . import emulated_bluetooth_packets_pb2 as emulated__bluetooth__packets__pb2
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18\x65mulated_bluetooth.proto\x12\x1b\x61ndroid.emulation.bluetooth\x1a emulated_bluetooth_packets.proto\"\x19\n\x07RawData\x12\x0e\n\x06packet\x18\x01 \x01(\x0c\x32\xcb\x02\n\x18\x45mulatedBluetoothService\x12\x64\n\x12registerClassicPhy\x12$.android.emulation.bluetooth.RawData\x1a$.android.emulation.bluetooth.RawData(\x01\x30\x01\x12`\n\x0eregisterBlePhy\x12$.android.emulation.bluetooth.RawData\x1a$.android.emulation.bluetooth.RawData(\x01\x30\x01\x12g\n\x11registerHCIDevice\x12&.android.emulation.bluetooth.HCIPacket\x1a&.android.emulation.bluetooth.HCIPacket(\x01\x30\x01\x42\"\n\x1e\x63om.android.emulator.bluetoothP\x01\x62\x06proto3')
|
||||
|
||||
|
||||
|
||||
_RAWDATA = DESCRIPTOR.message_types_by_name['RawData']
|
||||
RawData = _reflection.GeneratedProtocolMessageType('RawData', (_message.Message,), {
|
||||
'DESCRIPTOR' : _RAWDATA,
|
||||
'__module__' : 'emulated_bluetooth_pb2'
|
||||
# @@protoc_insertion_point(class_scope:android.emulation.bluetooth.RawData)
|
||||
})
|
||||
_sym_db.RegisterMessage(RawData)
|
||||
|
||||
_EMULATEDBLUETOOTHSERVICE = DESCRIPTOR.services_by_name['EmulatedBluetoothService']
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
|
||||
DESCRIPTOR._options = None
|
||||
DESCRIPTOR._serialized_options = b'\n\036com.android.emulator.bluetoothP\001'
|
||||
_RAWDATA._serialized_start=91
|
||||
_RAWDATA._serialized_end=116
|
||||
_EMULATEDBLUETOOTHSERVICE._serialized_start=119
|
||||
_EMULATEDBLUETOOTHSERVICE._serialized_end=450
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
207
bumble/transport/emulated_bluetooth_pb2_grpc.py
Normal file
207
bumble/transport/emulated_bluetooth_pb2_grpc.py
Normal file
@@ -0,0 +1,207 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
||||
"""Client and server classes corresponding to protobuf-defined services."""
|
||||
import grpc
|
||||
|
||||
from . import emulated_bluetooth_packets_pb2 as emulated__bluetooth__packets__pb2
|
||||
from . import emulated_bluetooth_pb2 as emulated__bluetooth__pb2
|
||||
|
||||
|
||||
class EmulatedBluetoothServiceStub(object):
|
||||
"""An Emulated Bluetooth Service exposes the emulated bluetooth chip from the
|
||||
android emulator. It allows you to register emulated bluetooth devices and
|
||||
control the packets that are exchanged between the device and the world.
|
||||
|
||||
This service enables you to establish a "virtual network" of emulated
|
||||
bluetooth devices that can interact with each other.
|
||||
|
||||
Note: This is not yet finalized, it is likely that these definitions will
|
||||
evolve.
|
||||
"""
|
||||
|
||||
def __init__(self, channel):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
channel: A grpc.Channel.
|
||||
"""
|
||||
self.registerClassicPhy = channel.stream_stream(
|
||||
'/android.emulation.bluetooth.EmulatedBluetoothService/registerClassicPhy',
|
||||
request_serializer=emulated__bluetooth__pb2.RawData.SerializeToString,
|
||||
response_deserializer=emulated__bluetooth__pb2.RawData.FromString,
|
||||
)
|
||||
self.registerBlePhy = channel.stream_stream(
|
||||
'/android.emulation.bluetooth.EmulatedBluetoothService/registerBlePhy',
|
||||
request_serializer=emulated__bluetooth__pb2.RawData.SerializeToString,
|
||||
response_deserializer=emulated__bluetooth__pb2.RawData.FromString,
|
||||
)
|
||||
self.registerHCIDevice = channel.stream_stream(
|
||||
'/android.emulation.bluetooth.EmulatedBluetoothService/registerHCIDevice',
|
||||
request_serializer=emulated__bluetooth__packets__pb2.HCIPacket.SerializeToString,
|
||||
response_deserializer=emulated__bluetooth__packets__pb2.HCIPacket.FromString,
|
||||
)
|
||||
|
||||
|
||||
class EmulatedBluetoothServiceServicer(object):
|
||||
"""An Emulated Bluetooth Service exposes the emulated bluetooth chip from the
|
||||
android emulator. It allows you to register emulated bluetooth devices and
|
||||
control the packets that are exchanged between the device and the world.
|
||||
|
||||
This service enables you to establish a "virtual network" of emulated
|
||||
bluetooth devices that can interact with each other.
|
||||
|
||||
Note: This is not yet finalized, it is likely that these definitions will
|
||||
evolve.
|
||||
"""
|
||||
|
||||
def registerClassicPhy(self, request_iterator, context):
|
||||
"""Connect device to link layer. This will establish a direct connection
|
||||
to the emulated bluetooth chip and configure the following:
|
||||
|
||||
- Each connection creates a new device and attaches it to the link layer
|
||||
- Link Layer packets are transmitted directly to the phy
|
||||
|
||||
This should be used for classic connections.
|
||||
|
||||
This is used to directly connect various android emulators together.
|
||||
For example a wear device can connect to an android emulator through
|
||||
this.
|
||||
"""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def registerBlePhy(self, request_iterator, context):
|
||||
"""Connect device to link layer. This will establish a direct connection
|
||||
to root canal and execute the following:
|
||||
|
||||
- Each connection creates a new device and attaches it to the link layer
|
||||
- Link Layer packets are transmitted directly to the phy
|
||||
|
||||
This should be used for BLE connections.
|
||||
|
||||
This is used to directly connect various android emulators together.
|
||||
For example a wear device can connect to an android emulator through
|
||||
this.
|
||||
"""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def registerHCIDevice(self, request_iterator, context):
|
||||
"""Connect the device to the emulated bluetooth chip. The device will
|
||||
participate in the network. You can configure the chip to scan, advertise
|
||||
and setup connections with other devices that are connected to the
|
||||
network.
|
||||
|
||||
This is usually used when you have a need for an emulated bluetooth chip
|
||||
and have a bluetooth stack that can interpret and handle the packets
|
||||
correctly.
|
||||
|
||||
For example the apache nimble stack can use this endpoint as the
|
||||
transport layer.
|
||||
"""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
|
||||
def add_EmulatedBluetoothServiceServicer_to_server(servicer, server):
|
||||
rpc_method_handlers = {
|
||||
'registerClassicPhy': grpc.stream_stream_rpc_method_handler(
|
||||
servicer.registerClassicPhy,
|
||||
request_deserializer=emulated__bluetooth__pb2.RawData.FromString,
|
||||
response_serializer=emulated__bluetooth__pb2.RawData.SerializeToString,
|
||||
),
|
||||
'registerBlePhy': grpc.stream_stream_rpc_method_handler(
|
||||
servicer.registerBlePhy,
|
||||
request_deserializer=emulated__bluetooth__pb2.RawData.FromString,
|
||||
response_serializer=emulated__bluetooth__pb2.RawData.SerializeToString,
|
||||
),
|
||||
'registerHCIDevice': grpc.stream_stream_rpc_method_handler(
|
||||
servicer.registerHCIDevice,
|
||||
request_deserializer=emulated__bluetooth__packets__pb2.HCIPacket.FromString,
|
||||
response_serializer=emulated__bluetooth__packets__pb2.HCIPacket.SerializeToString,
|
||||
),
|
||||
}
|
||||
generic_handler = grpc.method_handlers_generic_handler(
|
||||
'android.emulation.bluetooth.EmulatedBluetoothService', rpc_method_handlers)
|
||||
server.add_generic_rpc_handlers((generic_handler,))
|
||||
|
||||
|
||||
# This class is part of an EXPERIMENTAL API.
|
||||
class EmulatedBluetoothService(object):
|
||||
"""An Emulated Bluetooth Service exposes the emulated bluetooth chip from the
|
||||
android emulator. It allows you to register emulated bluetooth devices and
|
||||
control the packets that are exchanged between the device and the world.
|
||||
|
||||
This service enables you to establish a "virtual network" of emulated
|
||||
bluetooth devices that can interact with each other.
|
||||
|
||||
Note: This is not yet finalized, it is likely that these definitions will
|
||||
evolve.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def registerClassicPhy(request_iterator,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.stream_stream(request_iterator, target, '/android.emulation.bluetooth.EmulatedBluetoothService/registerClassicPhy',
|
||||
emulated__bluetooth__pb2.RawData.SerializeToString,
|
||||
emulated__bluetooth__pb2.RawData.FromString,
|
||||
options, channel_credentials,
|
||||
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
|
||||
|
||||
@staticmethod
|
||||
def registerBlePhy(request_iterator,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.stream_stream(request_iterator, target, '/android.emulation.bluetooth.EmulatedBluetoothService/registerBlePhy',
|
||||
emulated__bluetooth__pb2.RawData.SerializeToString,
|
||||
emulated__bluetooth__pb2.RawData.FromString,
|
||||
options, channel_credentials,
|
||||
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
|
||||
|
||||
@staticmethod
|
||||
def registerHCIDevice(request_iterator,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.stream_stream(request_iterator, target, '/android.emulation.bluetooth.EmulatedBluetoothService/registerHCIDevice',
|
||||
emulated__bluetooth__packets__pb2.HCIPacket.SerializeToString,
|
||||
emulated__bluetooth__packets__pb2.HCIPacket.FromString,
|
||||
options, channel_credentials,
|
||||
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
|
||||
43
bumble/transport/emulated_bluetooth_vhci_pb2.py
Normal file
43
bumble/transport/emulated_bluetooth_vhci_pb2.py
Normal file
@@ -0,0 +1,43 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: emulated_bluetooth_vhci.proto
|
||||
"""Generated protocol buffer code."""
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||
from google.protobuf import message as _message
|
||||
from google.protobuf import reflection as _reflection
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
import emulated_bluetooth_packets_pb2 as emulated__bluetooth__packets__pb2
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1d\x65mulated_bluetooth_vhci.proto\x12\x1b\x61ndroid.emulation.bluetooth\x1a emulated_bluetooth_packets.proto2y\n\x15VhciForwardingService\x12`\n\nattachVhci\x12&.android.emulation.bluetooth.HCIPacket\x1a&.android.emulation.bluetooth.HCIPacket(\x01\x30\x01\x42J\n\x1f\x63om.android.emulation.bluetoothP\x01\xf8\x01\x01\xa2\x02\x03\x41\x45\x42\xaa\x02\x1b\x41ndroid.Emulation.Bluetoothb\x06proto3')
|
||||
|
||||
|
||||
|
||||
_VHCIFORWARDINGSERVICE = DESCRIPTOR.services_by_name['VhciForwardingService']
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
|
||||
DESCRIPTOR._options = None
|
||||
DESCRIPTOR._serialized_options = b'\n\037com.android.emulation.bluetoothP\001\370\001\001\242\002\003AEB\252\002\033Android.Emulation.Bluetooth'
|
||||
_VHCIFORWARDINGSERVICE._serialized_start=96
|
||||
_VHCIFORWARDINGSERVICE._serialized_end=217
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
114
bumble/transport/emulated_bluetooth_vhci_pb2_grpc.py
Normal file
114
bumble/transport/emulated_bluetooth_vhci_pb2_grpc.py
Normal file
@@ -0,0 +1,114 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
||||
"""Client and server classes corresponding to protobuf-defined services."""
|
||||
import grpc
|
||||
|
||||
from . import emulated_bluetooth_packets_pb2 as emulated__bluetooth__packets__pb2
|
||||
|
||||
|
||||
class VhciForwardingServiceStub(object):
|
||||
"""This is a service which allows you to directly intercept the VHCI packets
|
||||
that are coming and going to the device before they are delivered to
|
||||
the rootcanal service described below.
|
||||
|
||||
This service is usually not available on the emulator, and must be explictly
|
||||
requested from the commandline.
|
||||
"""
|
||||
|
||||
def __init__(self, channel):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
channel: A grpc.Channel.
|
||||
"""
|
||||
self.attachVhci = channel.stream_stream(
|
||||
'/android.emulation.bluetooth.VhciForwardingService/attachVhci',
|
||||
request_serializer=emulated__bluetooth__packets__pb2.HCIPacket.SerializeToString,
|
||||
response_deserializer=emulated__bluetooth__packets__pb2.HCIPacket.FromString,
|
||||
)
|
||||
|
||||
|
||||
class VhciForwardingServiceServicer(object):
|
||||
"""This is a service which allows you to directly intercept the VHCI packets
|
||||
that are coming and going to the device before they are delivered to
|
||||
the rootcanal service described below.
|
||||
|
||||
This service is usually not available on the emulator, and must be explictly
|
||||
requested from the commandline.
|
||||
"""
|
||||
|
||||
def attachVhci(self, request_iterator, context):
|
||||
"""This attach directly to /dev/vhci inside the android guest if available
|
||||
|
||||
- This will disable root canal.
|
||||
- You will have to provide your own virtual bluetooth chip.
|
||||
|
||||
Some things to be aware of:
|
||||
- Only one client can be active.
|
||||
- Registering when bluetooth is active in android can result in
|
||||
undefined behavior.
|
||||
- If a client disconnects, rootcanal will be activated again
|
||||
|
||||
Status codes:
|
||||
- FAILED_PRECONDITION (code 9) If another client is controlling /dev/vhci.
|
||||
|
||||
This is an internal testing only interface, and is NOT publicly
|
||||
supported.
|
||||
"""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
|
||||
def add_VhciForwardingServiceServicer_to_server(servicer, server):
|
||||
rpc_method_handlers = {
|
||||
'attachVhci': grpc.stream_stream_rpc_method_handler(
|
||||
servicer.attachVhci,
|
||||
request_deserializer=emulated__bluetooth__packets__pb2.HCIPacket.FromString,
|
||||
response_serializer=emulated__bluetooth__packets__pb2.HCIPacket.SerializeToString,
|
||||
),
|
||||
}
|
||||
generic_handler = grpc.method_handlers_generic_handler(
|
||||
'android.emulation.bluetooth.VhciForwardingService', rpc_method_handlers)
|
||||
server.add_generic_rpc_handlers((generic_handler,))
|
||||
|
||||
|
||||
# This class is part of an EXPERIMENTAL API.
|
||||
class VhciForwardingService(object):
|
||||
"""This is a service which allows you to directly intercept the VHCI packets
|
||||
that are coming and going to the device before they are delivered to
|
||||
the rootcanal service described below.
|
||||
|
||||
This service is usually not available on the emulator, and must be explictly
|
||||
requested from the commandline.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def attachVhci(request_iterator,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.stream_stream(request_iterator, target, '/android.emulation.bluetooth.VhciForwardingService/attachVhci',
|
||||
emulated__bluetooth__packets__pb2.HCIPacket.SerializeToString,
|
||||
emulated__bluetooth__packets__pb2.HCIPacket.FromString,
|
||||
options, channel_credentials,
|
||||
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
|
||||
60
bumble/transport/file.py
Normal file
60
bumble/transport/file.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import io
|
||||
import logging
|
||||
|
||||
from .common import Transport, StreamPacketSource, StreamPacketSink
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
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 the file
|
||||
file = io.open(spec, 'r+b', buffering=0)
|
||||
|
||||
# Setup reading
|
||||
read_transport, packet_source = await asyncio.get_running_loop().connect_read_pipe(
|
||||
lambda: StreamPacketSource(),
|
||||
file
|
||||
)
|
||||
|
||||
# Setup writing
|
||||
write_transport, _ = await asyncio.get_running_loop().connect_write_pipe(
|
||||
lambda: asyncio.BaseProtocol(),
|
||||
file
|
||||
)
|
||||
packet_sink = StreamPacketSink(write_transport)
|
||||
|
||||
class FileTransport(Transport):
|
||||
async def close(self):
|
||||
read_transport.close()
|
||||
write_transport.close()
|
||||
file.close()
|
||||
|
||||
return FileTransport(packet_source, packet_sink)
|
||||
|
||||
146
bumble/transport/hci_socket.py
Normal file
146
bumble/transport/hci_socket.py
Normal file
@@ -0,0 +1,146 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import logging
|
||||
import struct
|
||||
import os
|
||||
import socket
|
||||
import ctypes
|
||||
import collections
|
||||
|
||||
from .common import Transport, ParserSource
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_hci_socket_transport(spec):
|
||||
'''
|
||||
Open an HCI Socket (only available on some platforms).
|
||||
The parameter string is either empty (to use the first/default Bluetooth adapter)
|
||||
or a 0-based integer to indicate the adapter number.
|
||||
'''
|
||||
|
||||
HCI_CHANNEL_USER = 1
|
||||
|
||||
# Create a raw HCI socket
|
||||
try:
|
||||
hci_socket = socket.socket(socket.AF_BLUETOOTH, socket.SOCK_RAW | socket.SOCK_NONBLOCK, socket.BTPROTO_HCI)
|
||||
except AttributeError:
|
||||
# Not supported on this platform
|
||||
logger.info("HCI sockets not supported on this platform")
|
||||
raise Exception('Bluetooth HCI sockets not supported on this platform')
|
||||
|
||||
# Compute the adapter index
|
||||
if spec is None:
|
||||
adapter_index = 0
|
||||
else:
|
||||
adapter_index = int(spec)
|
||||
|
||||
# Bind the socket
|
||||
# NOTE: since Python doesn't support binding with the required address format (yet),
|
||||
# we need to go directly to the C runtime...
|
||||
try:
|
||||
ctypes.cdll.LoadLibrary('libc.so.6')
|
||||
libc = ctypes.CDLL('libc.so.6', use_errno=True)
|
||||
except OSError:
|
||||
logger.info("HCI sockets not supported on this platform")
|
||||
raise Exception('Bluetooth HCI sockets not supported on this platform')
|
||||
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)
|
||||
if libc.bind(hci_socket.fileno(), ctypes.create_string_buffer(bind_address), len(bind_address)) != 0:
|
||||
raise IOError(ctypes.get_errno(), os.strerror(ctypes.get_errno()))
|
||||
|
||||
class HciSocketSource(ParserSource):
|
||||
def __init__(self, socket):
|
||||
super().__init__()
|
||||
self.socket = socket
|
||||
asyncio.get_running_loop().add_reader(socket.fileno(), self.recv_until_would_block)
|
||||
|
||||
def recv_until_would_block(self):
|
||||
logger.debug('recv until would block +++')
|
||||
while True:
|
||||
try:
|
||||
packet = self.socket.recv(4096)
|
||||
logger.debug(f'received packet {len(packet)} bytes')
|
||||
self.parser.feed_data(packet)
|
||||
except BlockingIOError:
|
||||
logger.debug('recv would block')
|
||||
break
|
||||
|
||||
def close(self):
|
||||
asyncio.get_running_loop().remove_reader(self.socket.fileno())
|
||||
|
||||
class HciSocketSink:
|
||||
def __init__(self, socket):
|
||||
self.socket = socket
|
||||
self.packets = collections.deque()
|
||||
self.writer_added = False
|
||||
|
||||
def send_until_would_block(self):
|
||||
logger.debug('send until would block ---')
|
||||
while self.packets:
|
||||
packet = self.packets.pop()
|
||||
logger.debug('sending packet')
|
||||
try:
|
||||
bytes_written = self.socket.send(packet)
|
||||
except BlockingIOError:
|
||||
bytes_written = 0
|
||||
if bytes_written != len(packet):
|
||||
# Note: we assume here that there are no partial writes
|
||||
logger.debug('send would block')
|
||||
break
|
||||
|
||||
if self.packets:
|
||||
# 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)
|
||||
self.writer_added = True
|
||||
else:
|
||||
# Nothing left to send, stop monitoring the socket
|
||||
if self.writer_added:
|
||||
asyncio.get_running_loop().remove_writer(self.socket.fileno())
|
||||
self.writer_added = False
|
||||
|
||||
def on_packet(self, packet):
|
||||
self.packets.appendleft(packet)
|
||||
self.send_until_would_block()
|
||||
|
||||
def close(self):
|
||||
if self.writer_added:
|
||||
asyncio.get_running_loop().remove_writer(self.socket.fileno())
|
||||
|
||||
class HciSocketTransport(Transport):
|
||||
def __init__(self, socket, source, sink):
|
||||
super().__init__(source, sink)
|
||||
self.socket = socket
|
||||
|
||||
async def close(self):
|
||||
logger.debug('closing HCI socket transport')
|
||||
self.source.close()
|
||||
self.sink.close()
|
||||
self.socket.close()
|
||||
|
||||
packet_source = HciSocketSource(hci_socket)
|
||||
packet_sink = HciSocketSink(hci_socket)
|
||||
return HciSocketTransport(hci_socket, packet_source, packet_sink)
|
||||
82
bumble/transport/pty.py
Normal file
82
bumble/transport/pty.py
Normal file
@@ -0,0 +1,82 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import pty
|
||||
import tty
|
||||
import io
|
||||
import atexit
|
||||
import os
|
||||
import logging
|
||||
|
||||
from .common import Transport, StreamPacketSource, StreamPacketSink
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_pty_transport(spec):
|
||||
'''
|
||||
Open a PTY transport.
|
||||
The parameter string may be empty, or a path name where a symbolic link
|
||||
to the PTY will be created (the link will be removed when the transport
|
||||
is closed or when the process exits)
|
||||
'''
|
||||
|
||||
primary, replica = pty.openpty()
|
||||
replica_path = os.ttyname(replica)
|
||||
logger.debug(f'pty open at {replica_path}')
|
||||
tty.setraw(primary)
|
||||
tty.setraw(replica)
|
||||
|
||||
read_transport, packet_source = await asyncio.get_running_loop().connect_read_pipe(
|
||||
lambda: 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)
|
||||
)
|
||||
packet_sink = StreamPacketSink(write_transport)
|
||||
|
||||
def cleanup():
|
||||
if spec:
|
||||
try:
|
||||
os.unlink(spec)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
# If required, create a symbolic link to the replica
|
||||
# NOTE: the link will be removed when this process exits
|
||||
if spec:
|
||||
os.symlink(replica_path, spec)
|
||||
logger.debug(f'linked pty at {spec}')
|
||||
atexit.register(cleanup)
|
||||
|
||||
class PtyTransport(Transport):
|
||||
async def close(self):
|
||||
write_transport.close()
|
||||
read_transport.close()
|
||||
os.close(primary)
|
||||
os.close(replica)
|
||||
cleanup()
|
||||
|
||||
return PtyTransport(packet_source, packet_sink)
|
||||
276
bumble/transport/pyusb.py
Normal file
276
bumble/transport/pyusb.py
Normal file
@@ -0,0 +1,276 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import logging
|
||||
import usb.core
|
||||
import usb.util
|
||||
import threading
|
||||
import time
|
||||
from colors import color
|
||||
|
||||
from .common import Transport, ParserSource
|
||||
from .. import hci
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_pyusb_transport(spec):
|
||||
'''
|
||||
Open a USB transport. [Implementation based on PyUSB]
|
||||
The parameter string has this syntax:
|
||||
either <index> or <vendor>:<product>
|
||||
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.
|
||||
|
||||
Examples:
|
||||
0 --> the first BT USB dongle
|
||||
04b4:f901 --> the BT USB dongle with vendor=04b4 and product=f901
|
||||
'''
|
||||
|
||||
USB_RECIPIENT_DEVICE = 0x00
|
||||
USB_REQUEST_TYPE_CLASS = 0x01 << 5
|
||||
USB_ENDPOINT_EVENTS_IN = 0x81
|
||||
USB_ENDPOINT_ACL_IN = 0x82
|
||||
USB_ENDPOINT_SCO_IN = 0x83
|
||||
USB_ENDPOINT_ACL_OUT = 0x02
|
||||
# USB_ENDPOINT_SCO_OUT = 0x03
|
||||
USB_DEVICE_CLASS_WIRELESS_CONTROLLER = 0xE0
|
||||
USB_DEVICE_SUBCLASS_RF_CONTROLLER = 0x01
|
||||
USB_DEVICE_PROTOCOL_BLUETOOTH_PRIMARY_CONTROLLER = 0x01
|
||||
|
||||
READ_SIZE = 1024
|
||||
READ_TIMEOUT = 1000
|
||||
|
||||
class UsbPacketSink:
|
||||
def __init__(self, device):
|
||||
self.device = device
|
||||
self.thread = threading.Thread(target=self.run)
|
||||
self.loop = asyncio.get_running_loop()
|
||||
self.stop_event = None
|
||||
|
||||
def on_packet(self, packet):
|
||||
# TODO: don't block here, just queue for the write thread
|
||||
if len(packet) == 0:
|
||||
logger.warning('packet too short')
|
||||
return
|
||||
|
||||
packet_type = packet[0]
|
||||
try:
|
||||
if packet_type == hci.HCI_ACL_DATA_PACKET:
|
||||
self.device.write(USB_ENDPOINT_ACL_OUT, packet[1:])
|
||||
elif packet_type == hci.HCI_COMMAND_PACKET:
|
||||
self.device.ctrl_transfer(USB_RECIPIENT_DEVICE | USB_REQUEST_TYPE_CLASS, 0, 0, 0, packet[1:])
|
||||
else:
|
||||
logger.warning(color(f'unsupported packet type {packet_type}', 'red'))
|
||||
except usb.core.USBTimeoutError:
|
||||
logger.warning('USB Write Timeout')
|
||||
except usb.core.USBError as error:
|
||||
logger.warning(f'USB write error: {error}')
|
||||
time.sleep(1) # Sleep one second to avoid busy looping
|
||||
|
||||
def start(self):
|
||||
self.thread.start()
|
||||
|
||||
async def stop(self):
|
||||
# Create stop events and wait for them to be signaled
|
||||
self.stop_event = asyncio.Event()
|
||||
await self.stop_event.wait()
|
||||
|
||||
def run(self):
|
||||
while self.stop_event is None:
|
||||
time.sleep(1)
|
||||
self.loop.call_soon_threadsafe(lambda: self.stop_event.set())
|
||||
|
||||
class UsbPacketSource(asyncio.Protocol, ParserSource):
|
||||
def __init__(self, device, sco_enabled):
|
||||
super().__init__()
|
||||
self.device = device
|
||||
self.loop = asyncio.get_running_loop()
|
||||
self.queue = asyncio.Queue()
|
||||
self.event_thread = threading.Thread(
|
||||
target=self.run,
|
||||
args=(USB_ENDPOINT_EVENTS_IN, hci.HCI_EVENT_PACKET)
|
||||
)
|
||||
self.event_thread.stop_event = None
|
||||
self.acl_thread = threading.Thread(
|
||||
target=self.run,
|
||||
args=(USB_ENDPOINT_ACL_IN, hci.HCI_ACL_DATA_PACKET)
|
||||
)
|
||||
self.acl_thread.stop_event = None
|
||||
|
||||
# SCO support is optional
|
||||
self.sco_enabled = sco_enabled
|
||||
if sco_enabled:
|
||||
self.sco_thread = threading.Thread(
|
||||
target=self.run,
|
||||
args=(USB_ENDPOINT_SCO_IN, hci.HCI_SYNCHRONOUS_DATA_PACKET)
|
||||
)
|
||||
self.sco_thread.stop_event = None
|
||||
|
||||
def data_received(self, packet):
|
||||
self.parser.feed_data(packet)
|
||||
|
||||
def enqueue(self, packet):
|
||||
self.queue.put_nowait(packet)
|
||||
|
||||
async def dequeue(self):
|
||||
while True:
|
||||
try:
|
||||
data = await self.queue.get()
|
||||
except asyncio.CancelledError:
|
||||
return
|
||||
self.data_received(data)
|
||||
|
||||
def start(self):
|
||||
self.dequeue_task = self.loop.create_task(self.dequeue())
|
||||
self.event_thread.start()
|
||||
self.acl_thread.start()
|
||||
if self.sco_enabled:
|
||||
self.sco_thread.start()
|
||||
|
||||
async def stop(self):
|
||||
# Stop the dequeuing task
|
||||
self.dequeue_task.cancel()
|
||||
|
||||
# Create stop events and wait for them to be signaled
|
||||
self.event_thread.stop_event = asyncio.Event()
|
||||
self.acl_thread.stop_event = asyncio.Event()
|
||||
await self.event_thread.stop_event.wait()
|
||||
await self.acl_thread.stop_event.wait()
|
||||
if self.sco_enabled:
|
||||
await self.sco_thread.stop_event.wait()
|
||||
|
||||
def run(self, endpoint, packet_type):
|
||||
# Read until asked to stop
|
||||
current_thread = threading.current_thread()
|
||||
while current_thread.stop_event is None:
|
||||
try:
|
||||
# Read, with a timeout of 1 second
|
||||
data = self.device.read(endpoint, READ_SIZE, timeout=READ_TIMEOUT)
|
||||
packet = bytes([packet_type]) + data.tobytes()
|
||||
self.loop.call_soon_threadsafe(self.enqueue, packet)
|
||||
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.
|
||||
# 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())
|
||||
|
||||
class UsbTransport(Transport):
|
||||
def __init__(self, device, source, sink):
|
||||
super().__init__(source, sink)
|
||||
self.device = device
|
||||
|
||||
async def close(self):
|
||||
await self.source.stop()
|
||||
await self.sink.stop()
|
||||
usb.util.release_interface(self.device, 0)
|
||||
|
||||
# Find the device according to the spec moniker
|
||||
if ':' in spec:
|
||||
vendor_id, product_id = spec.split(':')
|
||||
device = usb.core.find(idVendor=int(vendor_id, 16), idProduct=int(product_id, 16))
|
||||
else:
|
||||
device_index = int(spec)
|
||||
devices = list(usb.core.find(
|
||||
find_all = 1,
|
||||
bDeviceClass = USB_DEVICE_CLASS_WIRELESS_CONTROLLER,
|
||||
bDeviceSubClass = USB_DEVICE_SUBCLASS_RF_CONTROLLER,
|
||||
bDeviceProtocol = USB_DEVICE_PROTOCOL_BLUETOOTH_PRIMARY_CONTROLLER
|
||||
))
|
||||
if len(devices) > device_index:
|
||||
device = devices[device_index]
|
||||
else:
|
||||
device = None
|
||||
|
||||
if device is None:
|
||||
raise ValueError('device not found')
|
||||
logger.debug(f'USB Device: {device}')
|
||||
|
||||
# Detach the kernel driver if needed
|
||||
if device.is_kernel_driver_active(0):
|
||||
logger.debug("detaching kernel driver")
|
||||
device.detach_kernel_driver(0)
|
||||
|
||||
# Set configuration, if needed
|
||||
try:
|
||||
configuration = device.get_active_configuration()
|
||||
except usb.core.USBError:
|
||||
device.set_configuration()
|
||||
configuration = device.get_active_configuration()
|
||||
interface = configuration[(0, 0)]
|
||||
logger.debug(f'USB Interface: {interface}')
|
||||
usb.util.claim_interface(device, 0)
|
||||
|
||||
# Select an alternate setting for SCO, if available
|
||||
sco_enabled = False
|
||||
# NOTE: this is disabled for now, because SCO with alternate settings is broken,
|
||||
# see: https://github.com/libusb/libusb/issues/36
|
||||
#
|
||||
# best_packet_size = 0
|
||||
# best_interface = None
|
||||
# sco_enabled = False
|
||||
# for interface in configuration:
|
||||
# iso_in_endpoint = None
|
||||
# iso_out_endpoint = None
|
||||
# for endpoint in interface:
|
||||
# if (endpoint.bEndpointAddress == USB_ENDPOINT_SCO_IN and
|
||||
# usb.util.endpoint_direction(endpoint.bEndpointAddress) == usb.util.ENDPOINT_IN and
|
||||
# usb.util.endpoint_type(endpoint.bmAttributes) == usb.util.ENDPOINT_TYPE_ISO):
|
||||
# iso_in_endpoint = endpoint
|
||||
# continue
|
||||
# if (endpoint.bEndpointAddress == USB_ENDPOINT_SCO_OUT and
|
||||
# usb.util.endpoint_direction(endpoint.bEndpointAddress) == usb.util.ENDPOINT_OUT and
|
||||
# usb.util.endpoint_type(endpoint.bmAttributes) == usb.util.ENDPOINT_TYPE_ISO):
|
||||
# iso_out_endpoint = endpoint
|
||||
|
||||
# if iso_in_endpoint is not None and iso_out_endpoint is not None:
|
||||
# if iso_out_endpoint.wMaxPacketSize > best_packet_size:
|
||||
# best_packet_size = iso_out_endpoint.wMaxPacketSize
|
||||
# best_interface = interface
|
||||
|
||||
# if best_interface is not None:
|
||||
# logger.debug(f'SCO enabled, selecting alternate setting (wMaxPacketSize={best_packet_size}): {best_interface}')
|
||||
# sco_enabled = True
|
||||
# try:
|
||||
# device.set_interface_altsetting(
|
||||
# interface = best_interface.bInterfaceNumber,
|
||||
# alternate_setting = best_interface.bAlternateSetting
|
||||
# )
|
||||
# except usb.USBError:
|
||||
# logger.warning('failed to set alternate setting')
|
||||
|
||||
packet_source = UsbPacketSource(device, sco_enabled)
|
||||
packet_sink = UsbPacketSink(device)
|
||||
packet_source.start()
|
||||
packet_sink.start()
|
||||
|
||||
return UsbTransport(device, packet_source, packet_sink)
|
||||
72
bumble/transport/serial.py
Normal file
72
bumble/transport/serial.py
Normal file
@@ -0,0 +1,72 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import logging
|
||||
import serial_asyncio
|
||||
|
||||
from .common import Transport, StreamPacketSource, StreamPacketSink
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_serial_transport(spec):
|
||||
'''
|
||||
Open a serial port transport.
|
||||
The parameter string has this syntax:
|
||||
<device-path>[,<speed>][,rtscts][,dsrdtr]
|
||||
When <speed> is omitted, the default value of 1000000 is used
|
||||
When "rtscts" is specified, RTS/CTS hardware flow control is enabled
|
||||
When "dsrdtr" is specified, DSR/DTR hardware flow control is enabled
|
||||
|
||||
Examples:
|
||||
/dev/tty.usbmodem0006839912172
|
||||
/dev/tty.usbmodem0006839912172,1000000
|
||||
/dev/tty.usbmodem0006839912172,rtscts
|
||||
'''
|
||||
|
||||
speed = 1000000
|
||||
rtscts = False
|
||||
dsrdtr = False
|
||||
if ',' in spec:
|
||||
parts = spec.split(',')
|
||||
device = parts[0]
|
||||
for part in parts[1:]:
|
||||
if part == 'rtscts':
|
||||
rtscts = True
|
||||
elif part == 'dsrdtr':
|
||||
dsrdtr = True
|
||||
elif part.isnumeric():
|
||||
speed = int(part)
|
||||
else:
|
||||
device = spec
|
||||
serial_transport, packet_source = await serial_asyncio.create_serial_connection(
|
||||
asyncio.get_running_loop(),
|
||||
lambda: StreamPacketSource(),
|
||||
device,
|
||||
baudrate=speed,
|
||||
rtscts=rtscts,
|
||||
dsrdtr=dsrdtr
|
||||
)
|
||||
packet_sink = StreamPacketSink(serial_transport)
|
||||
|
||||
return Transport(packet_source, packet_sink)
|
||||
|
||||
52
bumble/transport/tcp_client.py
Normal file
52
bumble/transport/tcp_client.py
Normal file
@@ -0,0 +1,52 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from .common import Transport, StreamPacketSource, StreamPacketSink
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_tcp_client_transport(spec):
|
||||
'''
|
||||
Open a TCP client transport.
|
||||
The parameter string has this syntax:
|
||||
<remote-host>:<remote-port>
|
||||
|
||||
Example: 127.0.0.1:9001
|
||||
'''
|
||||
|
||||
class TcpPacketSource(StreamPacketSource):
|
||||
def connection_lost(self, error):
|
||||
logger.debug(f'connection lost: {error}')
|
||||
self.terminated.set_result(error)
|
||||
|
||||
remote_host, remote_port = spec.split(':')
|
||||
tcp_transport, packet_source = await asyncio.get_running_loop().create_connection(
|
||||
lambda: TcpPacketSource(),
|
||||
host=remote_host,
|
||||
port=int(remote_port),
|
||||
)
|
||||
packet_sink = StreamPacketSink(tcp_transport)
|
||||
|
||||
return Transport(packet_source, packet_sink)
|
||||
88
bumble/transport/tcp_server.py
Normal file
88
bumble/transport/tcp_server.py
Normal file
@@ -0,0 +1,88 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from .common import Transport, StreamPacketSource
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_tcp_server_transport(spec):
|
||||
'''
|
||||
Open a TCP server transport.
|
||||
The parameter string has this syntax:
|
||||
<local-host>:<local-port>
|
||||
Where <local-host> may be the address of a local network interface, or '_'
|
||||
to accept connections on all local network interfaces.
|
||||
|
||||
Example: _:9001
|
||||
'''
|
||||
|
||||
class TcpServerTransport(Transport):
|
||||
async def close(self):
|
||||
await super().close()
|
||||
|
||||
class TcpServerProtocol:
|
||||
def __init__(self, packet_source, packet_sink):
|
||||
self.packet_source = packet_source
|
||||
self.packet_sink = packet_sink
|
||||
|
||||
# Called when a new connection is established
|
||||
def connection_made(self, transport):
|
||||
peername = transport.get_extra_info('peername')
|
||||
logger.debug('connection from {}'.format(peername))
|
||||
self.packet_sink.transport = transport
|
||||
|
||||
# Called when the client is disconnected
|
||||
def connection_lost(self, error):
|
||||
logger.debug(f'connection lost: {error}')
|
||||
self.packet_sink.transport = None
|
||||
|
||||
def eof_received(self):
|
||||
logger.debug('connection end')
|
||||
self.packet_sink.transport = None
|
||||
|
||||
# Called when data is received on the socket
|
||||
def data_received(self, data):
|
||||
self.packet_source.data_received(data)
|
||||
|
||||
class TcpServerPacketSink:
|
||||
def __init__(self):
|
||||
self.transport = None
|
||||
|
||||
def on_packet(self, packet):
|
||||
if self.transport:
|
||||
self.transport.write(packet)
|
||||
else:
|
||||
logger.debug('no client, dropping packet')
|
||||
|
||||
local_host, local_port = spec.split(':')
|
||||
packet_source = StreamPacketSource()
|
||||
packet_sink = TcpServerPacketSink()
|
||||
await asyncio.get_running_loop().create_server(
|
||||
lambda: TcpServerProtocol(packet_source, packet_sink),
|
||||
host=local_host if local_host != '_' else None,
|
||||
port=int(local_port),
|
||||
)
|
||||
|
||||
return TcpServerTransport(packet_source, packet_sink)
|
||||
63
bumble/transport/udp.py
Normal file
63
bumble/transport/udp.py
Normal file
@@ -0,0 +1,63 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from .common import Transport, ParserSource
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_udp_transport(spec):
|
||||
'''
|
||||
Open a UDP transport.
|
||||
The parameter string has this syntax:
|
||||
<local-host>:<local-port>,<remote-host>:<remote-port>
|
||||
|
||||
Example: 0.0.0.0:9000,127.0.0.1:9001
|
||||
'''
|
||||
|
||||
class UdpPacketSource(asyncio.DatagramProtocol, ParserSource):
|
||||
def datagram_received(self, data, addr):
|
||||
self.parser.feed_data(data)
|
||||
|
||||
class UdpPacketSink:
|
||||
def __init__(self, transport):
|
||||
self.transport = transport
|
||||
|
||||
def on_packet(self, packet):
|
||||
self.transport.sendto(packet)
|
||||
|
||||
def close(self):
|
||||
self.transport.close()
|
||||
|
||||
local, remote = spec.split(',')
|
||||
local_host, local_port = local.split(':')
|
||||
remote_host, remote_port = remote.split(':')
|
||||
udp_transport, packet_source = await asyncio.get_running_loop().create_datagram_endpoint(
|
||||
lambda: UdpPacketSource(),
|
||||
local_addr=(local_host, int(local_port)),
|
||||
remote_addr=(remote_host, int(remote_port))
|
||||
)
|
||||
packet_sink = UdpPacketSink(udp_transport)
|
||||
|
||||
return Transport(packet_source, packet_sink)
|
||||
324
bumble/transport/usb.py
Normal file
324
bumble/transport/usb.py
Normal file
@@ -0,0 +1,324 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import logging
|
||||
import usb1
|
||||
import threading
|
||||
import collections
|
||||
from colors import color
|
||||
|
||||
from .common import Transport, ParserSource
|
||||
from .. import hci
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_usb_transport(spec):
|
||||
'''
|
||||
Open a USB transport.
|
||||
The parameter string has this syntax:
|
||||
either <index> or <vendor>:<product>
|
||||
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.
|
||||
|
||||
Examples:
|
||||
0 --> the first BT USB dongle
|
||||
04b4:f901 --> the BT USB dongle with vendor=04b4 and product=f901
|
||||
'''
|
||||
|
||||
USB_RECIPIENT_DEVICE = 0x00
|
||||
USB_REQUEST_TYPE_CLASS = 0x01 << 5
|
||||
USB_ENDPOINT_EVENTS_IN = 0x81
|
||||
USB_ENDPOINT_ACL_IN = 0x82
|
||||
USB_ENDPOINT_ACL_OUT = 0x02
|
||||
USB_DEVICE_CLASS_WIRELESS_CONTROLLER = 0xE0
|
||||
USB_DEVICE_SUBCLASS_RF_CONTROLLER = 0x01
|
||||
USB_DEVICE_PROTOCOL_BLUETOOTH_PRIMARY_CONTROLLER = 0x01
|
||||
|
||||
READ_SIZE = 1024
|
||||
|
||||
class UsbPacketSink:
|
||||
def __init__(self, device):
|
||||
self.device = device
|
||||
self.transfer = device.getTransfer()
|
||||
self.packets = collections.deque() # Queue of packets waiting to be sent
|
||||
self.loop = asyncio.get_running_loop()
|
||||
self.cancel_done = self.loop.create_future()
|
||||
self.closed = False
|
||||
|
||||
def start(self):
|
||||
pass
|
||||
|
||||
def on_packet(self, packet):
|
||||
# Ignore packets if we're closed
|
||||
if self.closed:
|
||||
return
|
||||
|
||||
if len(packet) == 0:
|
||||
logger.warning('packet too short')
|
||||
return
|
||||
|
||||
# Queue the packet
|
||||
self.packets.append(packet)
|
||||
if len(self.packets) == 1:
|
||||
# The queue was previously empty, re-prime the pump
|
||||
self.process_queue()
|
||||
|
||||
def on_packet_sent(self, transfer):
|
||||
status = transfer.getStatus()
|
||||
# logger.debug(f'<<< USB out transfer callback: status={status}')
|
||||
|
||||
if status == usb1.TRANSFER_COMPLETED:
|
||||
self.loop.call_soon_threadsafe(self.on_packet_sent_)
|
||||
elif status == usb1.TRANSFER_CANCELLED:
|
||||
self.loop.call_soon_threadsafe(self.cancel_done.set_result, None)
|
||||
else:
|
||||
logger.warning(color(f'!!! out transfer not completed: status={status}', 'red'))
|
||||
|
||||
def on_packet_sent_(self):
|
||||
if self.packets:
|
||||
self.packets.popleft()
|
||||
self.process_queue()
|
||||
|
||||
def process_queue(self):
|
||||
if len(self.packets) == 0:
|
||||
return # Nothing to do
|
||||
|
||||
packet = self.packets[0]
|
||||
packet_type = packet[0]
|
||||
if packet_type == hci.HCI_ACL_DATA_PACKET:
|
||||
self.transfer.setBulk(
|
||||
USB_ENDPOINT_ACL_OUT,
|
||||
packet[1:],
|
||||
callback=self.on_packet_sent
|
||||
)
|
||||
logger.debug('submit ACL')
|
||||
self.transfer.submit()
|
||||
elif packet_type == hci.HCI_COMMAND_PACKET:
|
||||
self.transfer.setControl(
|
||||
USB_RECIPIENT_DEVICE | USB_REQUEST_TYPE_CLASS, 0, 0, 0,
|
||||
packet[1:],
|
||||
callback=self.on_packet_sent
|
||||
)
|
||||
logger.debug('submit COMMAND')
|
||||
self.transfer.submit()
|
||||
else:
|
||||
logger.warning(color(f'unsupported packet type {packet_type}', 'red'))
|
||||
|
||||
async def close(self):
|
||||
self.closed = True
|
||||
|
||||
# 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:
|
||||
self.transfer.cancel()
|
||||
|
||||
logger.debug('waiting for OUT transfer cancellation to be done...')
|
||||
await self.cancel_done
|
||||
logger.debug('OUT transfer cancellation done')
|
||||
except usb1.USBError:
|
||||
logger.debug('OUT transfer likely already completed')
|
||||
|
||||
class UsbPacketSource(asyncio.Protocol, ParserSource):
|
||||
def __init__(self, context, device):
|
||||
super().__init__()
|
||||
self.context = context
|
||||
self.device = device
|
||||
self.loop = asyncio.get_running_loop()
|
||||
self.queue = asyncio.Queue()
|
||||
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()
|
||||
}
|
||||
|
||||
# Create a thread to process events
|
||||
self.event_thread = threading.Thread(target=self.run)
|
||||
|
||||
def start(self):
|
||||
# Set up transfer objects for input
|
||||
self.events_in_transfer = device.getTransfer()
|
||||
self.events_in_transfer.setInterrupt(
|
||||
USB_ENDPOINT_EVENTS_IN,
|
||||
READ_SIZE,
|
||||
callback=self.on_packet_received,
|
||||
user_data=hci.HCI_EVENT_PACKET
|
||||
)
|
||||
self.events_in_transfer.submit()
|
||||
|
||||
self.acl_in_transfer = device.getTransfer()
|
||||
self.acl_in_transfer.setBulk(
|
||||
USB_ENDPOINT_ACL_IN,
|
||||
READ_SIZE,
|
||||
callback=self.on_packet_received,
|
||||
user_data=hci.HCI_ACL_DATA_PACKET
|
||||
)
|
||||
self.acl_in_transfer.submit()
|
||||
|
||||
self.dequeue_task = self.loop.create_task(self.dequeue())
|
||||
self.event_thread.start()
|
||||
|
||||
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}')
|
||||
|
||||
if status == usb1.TRANSFER_COMPLETED:
|
||||
packet = bytes([packet_type]) + transfer.getBuffer()[:transfer.getActualLength()]
|
||||
self.loop.call_soon_threadsafe(self.queue.put_nowait, packet)
|
||||
elif status == usb1.TRANSFER_CANCELLED:
|
||||
self.loop.call_soon_threadsafe(self.cancel_done[packet_type].set_result, None)
|
||||
return
|
||||
else:
|
||||
logger.warning(color(f'!!! transfer not completed: status={status}', 'red'))
|
||||
|
||||
# Re-submit the transfer so we can receive more data
|
||||
transfer.submit()
|
||||
|
||||
async def dequeue(self):
|
||||
while not self.closed:
|
||||
try:
|
||||
packet = await self.queue.get()
|
||||
except asyncio.CancelledError:
|
||||
return
|
||||
self.parser.feed_data(packet)
|
||||
|
||||
def run(self):
|
||||
logger.debug('starting USB event loop')
|
||||
while self.events_in_transfer.isSubmitted() or self.acl_in_transfer.isSubmitted():
|
||||
try:
|
||||
self.context.handleEvents()
|
||||
except usb1.USBErrorInterrupted:
|
||||
pass
|
||||
|
||||
logger.debug('USB event loop done')
|
||||
self.event_loop_done.set_result(None)
|
||||
|
||||
async def close(self):
|
||||
self.closed = True
|
||||
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
|
||||
packet_type = transfer.getUserData()
|
||||
try:
|
||||
transfer.cancel()
|
||||
logger.debug(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')
|
||||
except usb1.USBError:
|
||||
logger.debug(f'IN[{packet_type}] transfer likely already completed')
|
||||
|
||||
# Wait for the thread to terminate
|
||||
await self.event_loop_done
|
||||
|
||||
class UsbTransport(Transport):
|
||||
def __init__(self, context, device, interface, source, sink):
|
||||
super().__init__(source, sink)
|
||||
self.context = context
|
||||
self.device = device
|
||||
self.interface = interface
|
||||
|
||||
# Get exclusive access
|
||||
device.claimInterface(interface)
|
||||
|
||||
# The source and sink can now start
|
||||
source.start()
|
||||
sink.start()
|
||||
|
||||
async def close(self):
|
||||
await self.source.close()
|
||||
await self.sink.close()
|
||||
self.device.releaseInterface(self.interface)
|
||||
self.device.close()
|
||||
self.context.close()
|
||||
|
||||
# Find the device according to the spec moniker
|
||||
context = usb1.USBContext()
|
||||
context.open()
|
||||
try:
|
||||
found = None
|
||||
if ':' in spec:
|
||||
vendor_id, product_id = spec.split(':')
|
||||
found = context.getByVendorIDAndProductID(int(vendor_id, 16), int(product_id, 16), skip_on_error=True)
|
||||
else:
|
||||
device_index = int(spec)
|
||||
device_iterator = context.getDeviceIterator(skip_on_error=True)
|
||||
try:
|
||||
for device in device_iterator:
|
||||
if device.getDeviceClass() == USB_DEVICE_CLASS_WIRELESS_CONTROLLER and \
|
||||
device.getDeviceSubClass() == USB_DEVICE_SUBCLASS_RF_CONTROLLER and \
|
||||
device.getDeviceProtocol() == USB_DEVICE_PROTOCOL_BLUETOOTH_PRIMARY_CONTROLLER:
|
||||
if device_index == 0:
|
||||
found = device
|
||||
break
|
||||
device_index -= 1
|
||||
device.close()
|
||||
finally:
|
||||
device_iterator.close()
|
||||
|
||||
if found is None:
|
||||
context.close()
|
||||
raise ValueError('device not found')
|
||||
|
||||
logger.debug(f'USB Device: {found}')
|
||||
device = found.open()
|
||||
|
||||
# Set the configuration if needed
|
||||
try:
|
||||
configuration = device.getConfiguration()
|
||||
logger.debug(f'current configuration = {configuration}')
|
||||
except usb1.USBError:
|
||||
try:
|
||||
logger.debug('setting configuration 1')
|
||||
device.setConfiguration(1)
|
||||
except usb1.USBError:
|
||||
logger.debug('failed to set configuration 1')
|
||||
|
||||
# Use the first interface
|
||||
interface = 0
|
||||
|
||||
# Detach the kernel driver if supported and needed
|
||||
if usb1.hasCapability(usb1.CAP_SUPPORTS_DETACH_KERNEL_DRIVER):
|
||||
try:
|
||||
if device.kernelDriverActive(interface):
|
||||
logger.debug("detaching kernel driver")
|
||||
device.detachKernelDriver(interface)
|
||||
except usb1.USBError:
|
||||
pass
|
||||
|
||||
source = UsbPacketSource(context, device)
|
||||
sink = UsbPacketSink(device)
|
||||
return UsbTransport(context, device, interface, source, sink)
|
||||
except usb1.USBError as error:
|
||||
logger.warning(color(f'!!! failed to open USB device: {error}', 'red'))
|
||||
context.close()
|
||||
raise
|
||||
59
bumble/transport/vhci.py
Normal file
59
bumble/transport/vhci.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import logging
|
||||
|
||||
from .file import open_file_transport
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_vhci_transport(spec):
|
||||
'''
|
||||
Open a VHCI transport (only available on some platforms).
|
||||
The parameter string is either empty (to use the default VHCI device
|
||||
path at /dev/vhci), or the path of a VHCI device
|
||||
'''
|
||||
|
||||
HCI_VENDOR_PKT = 0xff
|
||||
HCI_BREDR = 0x00 # Controller type
|
||||
|
||||
# Open the VHCI device
|
||||
transport = await open_file_transport(spec or '/dev/vhci')
|
||||
|
||||
# Override the source's `data_received` method so that we can
|
||||
# filter out the vendor packet that is received just after the
|
||||
# initial open
|
||||
def vhci_data_received(data):
|
||||
if len(data) > 0 and data[0] == HCI_VENDOR_PKT:
|
||||
if len(data) == 4:
|
||||
hci_index = data[2] << 8 | data[3]
|
||||
logger.info(f'HCI index {hci_index}')
|
||||
else:
|
||||
transport.source.parser.feed_data(data)
|
||||
|
||||
transport.source.data_received = vhci_data_received
|
||||
|
||||
# Write the initial config
|
||||
transport.sink.on_packet(bytes([HCI_VENDOR_PKT, HCI_BREDR]))
|
||||
|
||||
return transport
|
||||
|
||||
49
bumble/transport/ws_client.py
Normal file
49
bumble/transport/ws_client.py
Normal file
@@ -0,0 +1,49 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import logging
|
||||
import websockets
|
||||
|
||||
from .common import PumpedPacketSource, PumpedPacketSink, PumpedTransport
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_ws_client_transport(spec):
|
||||
'''
|
||||
Open a WebSocket client transport.
|
||||
The parameter string has this syntax:
|
||||
<remote-host>:<remote-port>
|
||||
|
||||
Example: 127.0.0.1:9001
|
||||
'''
|
||||
|
||||
remote_host, remote_port = spec.split(':')
|
||||
uri = f'ws://{remote_host}:{remote_port}'
|
||||
websocket = await websockets.connect(uri)
|
||||
|
||||
transport = PumpedTransport(
|
||||
PumpedPacketSource(websocket.recv),
|
||||
PumpedPacketSink(websocket.send),
|
||||
websocket.close
|
||||
)
|
||||
transport.start()
|
||||
return transport
|
||||
81
bumble/transport/ws_server.py
Normal file
81
bumble/transport/ws_server.py
Normal file
@@ -0,0 +1,81 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import logging
|
||||
import websockets
|
||||
|
||||
from .common import Transport, ParserSource, PumpedPacketSink
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_ws_server_transport(spec):
|
||||
'''
|
||||
Open a WebSocket server transport.
|
||||
The parameter string has this syntax:
|
||||
<local-host>:<local-port>
|
||||
Where <local-host> may be the address of a local network interface, or '_'
|
||||
to accept connections on all local network interfaces.
|
||||
|
||||
Example: _:9001
|
||||
'''
|
||||
|
||||
class WsServerTransport(Transport):
|
||||
def __init__(self):
|
||||
source = ParserSource()
|
||||
sink = PumpedPacketSink(self.send_packet)
|
||||
self.connection = asyncio.get_running_loop().create_future()
|
||||
|
||||
super().__init__(source, sink)
|
||||
|
||||
async def serve(self, local_host, local_port):
|
||||
self.sink.start()
|
||||
self.server = await websockets.serve(
|
||||
ws_handler = self.on_connection,
|
||||
host = local_host if local_host != '_' else None,
|
||||
port = int(local_port)
|
||||
)
|
||||
logger.debug(f'websocket server ready on port {local_port}')
|
||||
|
||||
async def on_connection(self, connection):
|
||||
logger.debug(f'new connection on {connection.local_address} from {connection.remote_address}')
|
||||
self.connection.set_result(connection)
|
||||
try:
|
||||
async for packet in connection:
|
||||
if type(packet) is bytes:
|
||||
self.source.parser.feed_data(packet)
|
||||
else:
|
||||
logger.warn('discarding packet: not a BINARY frame')
|
||||
except websockets.WebSocketException as error:
|
||||
logger.debug(f'exception while receiving packet: {error}')
|
||||
|
||||
# Wait for a new connection
|
||||
self.connection = asyncio.get_running_loop().create_future()
|
||||
|
||||
async def send_packet(self, packet):
|
||||
connection = await self.connection
|
||||
return await connection.send(packet)
|
||||
|
||||
local_host, local_port = spec.split(':')
|
||||
transport = WsServerTransport()
|
||||
await transport.serve(local_host, local_port)
|
||||
return transport
|
||||
142
bumble/utils.py
Normal file
142
bumble/utils.py
Normal file
@@ -0,0 +1,142 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import logging
|
||||
import traceback
|
||||
from functools import wraps
|
||||
from colors import color
|
||||
from pyee import EventEmitter
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def setup_event_forwarding(emitter, forwarder, event_name):
|
||||
def emit(*args, **kwargs):
|
||||
forwarder.emit(event_name, *args, **kwargs)
|
||||
emitter.on(event_name, emit)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def composite_listener(cls):
|
||||
"""
|
||||
Decorator that adds a `register` and `deregister` method to a class, which
|
||||
registers/deregisters all methods named `on_<event_name>` as a listener for
|
||||
the <event_name> event with an emitter.
|
||||
"""
|
||||
def register(self, emitter):
|
||||
for method_name in dir(cls):
|
||||
if method_name.startswith('on_'):
|
||||
emitter.on(method_name[3:], getattr(self, method_name))
|
||||
|
||||
def deregister(self, emitter):
|
||||
for method_name in dir(cls):
|
||||
if method_name.startswith('on_'):
|
||||
emitter.remove_listener(method_name[3:], getattr(self, method_name))
|
||||
|
||||
cls._bumble_register_composite = register
|
||||
cls._bumble_deregister_composite = deregister
|
||||
return cls
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class CompositeEventEmitter(EventEmitter):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._listener = None
|
||||
|
||||
@property
|
||||
def listener(self):
|
||||
return self._listener
|
||||
|
||||
@listener.setter
|
||||
def listener(self, listener):
|
||||
if self._listener:
|
||||
# Call the deregistration methods for each base class that has them
|
||||
for cls in self._listener.__class__.mro():
|
||||
if hasattr(cls, '_bumble_register_composite'):
|
||||
cls._bumble_deregister_composite(listener, self)
|
||||
self._listener = listener
|
||||
if listener:
|
||||
# Call the registration methods for each base class that has them
|
||||
for cls in listener.__class__.mro():
|
||||
if hasattr(cls, '_bumble_deregister_composite'):
|
||||
cls._bumble_register_composite(listener, self)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class AsyncRunner:
|
||||
class WorkQueue:
|
||||
def __init__(self, create_task=True):
|
||||
self.queue = None
|
||||
self.task = None
|
||||
self.create_task = create_task
|
||||
|
||||
def enqueue(self, coroutine):
|
||||
# Create a task now if we need to and haven't done so already
|
||||
if self.create_task and self.task is None:
|
||||
self.task = asyncio.create_task(self.run())
|
||||
|
||||
# Lazy-create the coroutine queue
|
||||
if self.queue is None:
|
||||
self.queue = asyncio.Queue()
|
||||
|
||||
# Enqueue the work
|
||||
self.queue.put_nowait(coroutine)
|
||||
|
||||
async def run(self):
|
||||
while True:
|
||||
item = await self.queue.get()
|
||||
try:
|
||||
await item
|
||||
except Exception as error:
|
||||
logger.warning(f'{color("!!! Exception in work queue:", "red")} {error}')
|
||||
|
||||
# Shared default queue
|
||||
default_queue = WorkQueue()
|
||||
|
||||
@staticmethod
|
||||
def run_in_task(queue=None):
|
||||
"""
|
||||
Function decorator used to adapt an async function into a sync function
|
||||
"""
|
||||
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
coroutine = func(*args, **kwargs)
|
||||
if queue is None:
|
||||
# Create a task to run the coroutine
|
||||
async def run():
|
||||
try:
|
||||
await coroutine
|
||||
except Exception:
|
||||
logger.warning(f'{color("!!! Exception in wrapper:", "red")} {traceback.format_exc()}')
|
||||
|
||||
asyncio.create_task(run())
|
||||
else:
|
||||
# Queue the coroutine to be awaited by the work queue
|
||||
queue.enqueue(coroutine)
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
Reference in New Issue
Block a user