forked from auracaster/bumble_mirror
Compare commits
16 Commits
gbg/async-
...
gbg/scan-w
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d43281c57e | ||
|
|
6810865670 | ||
|
|
3e9e06a02c | ||
|
|
ccd12f6591 | ||
|
|
f9a7843f7e | ||
|
|
210c334db7 | ||
|
|
f297cdfcce | ||
|
|
5b536d00ab | ||
|
|
b4af46ebd5 | ||
|
|
c08da3193e | ||
|
|
fd4d68e5c0 | ||
|
|
b90d0f8710 | ||
|
|
afc6d19e04 | ||
|
|
c05f073b33 | ||
|
|
2b4c2a22f4 | ||
|
|
47fe93a148 |
191
apps/bench.py
191
apps/bench.py
@@ -82,10 +82,11 @@ SPEED_RX_UUID = '016A2CC7-E14B-4819-935F-1F56EAE4098D'
|
||||
DEFAULT_RFCOMM_UUID = 'E6D55659-C8B4-4B85-96BB-B1143AF6D3AE'
|
||||
DEFAULT_L2CAP_PSM = 1234
|
||||
DEFAULT_L2CAP_MAX_CREDITS = 128
|
||||
DEFAULT_L2CAP_MTU = 1022
|
||||
DEFAULT_L2CAP_MPS = 1024
|
||||
DEFAULT_L2CAP_MTU = 1024
|
||||
DEFAULT_L2CAP_MPS = 1022
|
||||
|
||||
DEFAULT_LINGER_TIME = 1.0
|
||||
DEFAULT_POST_CONNECTION_WAIT_TIME = 1.0
|
||||
|
||||
DEFAULT_RFCOMM_CHANNEL = 8
|
||||
|
||||
@@ -95,7 +96,7 @@ DEFAULT_RFCOMM_CHANNEL = 8
|
||||
# -----------------------------------------------------------------------------
|
||||
def parse_packet(packet):
|
||||
if len(packet) < 1:
|
||||
print(
|
||||
logging.info(
|
||||
color(f'!!! Packet too short (got {len(packet)} bytes, need >= 1)', 'red')
|
||||
)
|
||||
raise ValueError('packet too short')
|
||||
@@ -103,7 +104,7 @@ def parse_packet(packet):
|
||||
try:
|
||||
packet_type = PacketType(packet[0])
|
||||
except ValueError:
|
||||
print(color(f'!!! Invalid packet type 0x{packet[0]:02X}', 'red'))
|
||||
logging.info(color(f'!!! Invalid packet type 0x{packet[0]:02X}', 'red'))
|
||||
raise
|
||||
|
||||
return (packet_type, packet[1:])
|
||||
@@ -111,7 +112,7 @@ def parse_packet(packet):
|
||||
|
||||
def parse_packet_sequence(packet_data):
|
||||
if len(packet_data) < 5:
|
||||
print(
|
||||
logging.info(
|
||||
color(
|
||||
f'!!!Packet too short (got {len(packet_data)} bytes, need >= 5)',
|
||||
'red',
|
||||
@@ -155,7 +156,7 @@ def print_connection(connection):
|
||||
|
||||
mtu = connection.att_mtu
|
||||
|
||||
print(
|
||||
logging.info(
|
||||
f'{color("@@@ Connection:", "yellow")} '
|
||||
f'{connection_parameters} '
|
||||
f'{data_length} '
|
||||
@@ -266,15 +267,15 @@ class Sender:
|
||||
pass
|
||||
|
||||
async def run(self):
|
||||
print(color('--- Waiting for I/O to be ready...', 'blue'))
|
||||
logging.info(color('--- Waiting for I/O to be ready...', 'blue'))
|
||||
await self.packet_io.ready.wait()
|
||||
print(color('--- Go!', 'blue'))
|
||||
logging.info(color('--- Go!', 'blue'))
|
||||
|
||||
if self.tx_start_delay:
|
||||
print(color(f'*** Startup delay: {self.tx_start_delay}', 'blue'))
|
||||
logging.info(color(f'*** Startup delay: {self.tx_start_delay}', 'blue'))
|
||||
await asyncio.sleep(self.tx_start_delay)
|
||||
|
||||
print(color('=== Sending RESET', 'magenta'))
|
||||
logging.info(color('=== Sending RESET', 'magenta'))
|
||||
await self.packet_io.send_packet(bytes([PacketType.RESET]))
|
||||
self.start_time = time.time()
|
||||
for tx_i in range(self.tx_packet_count):
|
||||
@@ -285,12 +286,12 @@ class Sender:
|
||||
packet_flags,
|
||||
tx_i,
|
||||
) + bytes(self.tx_packet_size - 6)
|
||||
print(color(f'Sending packet {tx_i}: {len(packet)} bytes', 'yellow'))
|
||||
logging.info(color(f'Sending packet {tx_i}: {len(packet)} bytes', 'yellow'))
|
||||
self.bytes_sent += len(packet)
|
||||
await self.packet_io.send_packet(packet)
|
||||
|
||||
await self.done.wait()
|
||||
print(color('=== Done!', 'magenta'))
|
||||
logging.info(color('=== Done!', 'magenta'))
|
||||
|
||||
def on_packet_received(self, packet):
|
||||
try:
|
||||
@@ -301,7 +302,7 @@ class Sender:
|
||||
if packet_type == PacketType.ACK:
|
||||
elapsed = time.time() - self.start_time
|
||||
average_tx_speed = self.bytes_sent / elapsed
|
||||
print(
|
||||
logging.info(
|
||||
color(
|
||||
f'@@@ Received ACK. Speed: average={average_tx_speed:.4f}'
|
||||
f' ({self.bytes_sent} bytes in {elapsed:.2f} seconds)',
|
||||
@@ -315,6 +316,10 @@ class Sender:
|
||||
# Receiver
|
||||
# -----------------------------------------------------------------------------
|
||||
class Receiver:
|
||||
expected_packet_index: int
|
||||
start_timestamp: float
|
||||
last_timestamp: float
|
||||
|
||||
def __init__(self, packet_io):
|
||||
self.reset()
|
||||
self.packet_io = packet_io
|
||||
@@ -336,7 +341,7 @@ class Receiver:
|
||||
now = time.time()
|
||||
|
||||
if packet_type == PacketType.RESET:
|
||||
print(color('=== Received RESET', 'magenta'))
|
||||
logging.info(color('=== Received RESET', 'magenta'))
|
||||
self.reset()
|
||||
self.start_timestamp = now
|
||||
return
|
||||
@@ -345,13 +350,13 @@ class Receiver:
|
||||
packet_flags, packet_index = parse_packet_sequence(packet_data)
|
||||
except ValueError:
|
||||
return
|
||||
print(
|
||||
logging.info(
|
||||
f'<<< Received packet {packet_index}: '
|
||||
f'flags=0x{packet_flags:02X}, {len(packet)} bytes'
|
||||
)
|
||||
|
||||
if packet_index != self.expected_packet_index:
|
||||
print(
|
||||
logging.info(
|
||||
color(
|
||||
f'!!! Unexpected packet, expected {self.expected_packet_index} '
|
||||
f'but received {packet_index}'
|
||||
@@ -363,7 +368,7 @@ class Receiver:
|
||||
self.bytes_received += len(packet)
|
||||
instant_rx_speed = len(packet) / elapsed_since_last
|
||||
average_rx_speed = self.bytes_received / elapsed_since_start
|
||||
print(
|
||||
logging.info(
|
||||
color(
|
||||
f'Speed: instant={instant_rx_speed:.4f}, average={average_rx_speed:.4f}',
|
||||
'yellow',
|
||||
@@ -379,12 +384,12 @@ class Receiver:
|
||||
struct.pack('>bbI', PacketType.ACK, packet_flags, packet_index)
|
||||
)
|
||||
)
|
||||
print(color('@@@ Received last packet', 'green'))
|
||||
logging.info(color('@@@ Received last packet', 'green'))
|
||||
self.done.set()
|
||||
|
||||
async def run(self):
|
||||
await self.done.wait()
|
||||
print(color('=== Done!', 'magenta'))
|
||||
logging.info(color('=== Done!', 'magenta'))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -406,23 +411,23 @@ class Ping:
|
||||
pass
|
||||
|
||||
async def run(self):
|
||||
print(color('--- Waiting for I/O to be ready...', 'blue'))
|
||||
logging.info(color('--- Waiting for I/O to be ready...', 'blue'))
|
||||
await self.packet_io.ready.wait()
|
||||
print(color('--- Go!', 'blue'))
|
||||
logging.info(color('--- Go!', 'blue'))
|
||||
|
||||
if self.tx_start_delay:
|
||||
print(color(f'*** Startup delay: {self.tx_start_delay}', 'blue'))
|
||||
logging.info(color(f'*** Startup delay: {self.tx_start_delay}', 'blue'))
|
||||
await asyncio.sleep(self.tx_start_delay)
|
||||
|
||||
print(color('=== Sending RESET', 'magenta'))
|
||||
logging.info(color('=== Sending RESET', 'magenta'))
|
||||
await self.packet_io.send_packet(bytes([PacketType.RESET]))
|
||||
|
||||
await self.send_next_ping()
|
||||
|
||||
await self.done.wait()
|
||||
average_latency = sum(self.latencies) / len(self.latencies)
|
||||
print(color(f'@@@ Average latency: {average_latency:.2f}'))
|
||||
print(color('=== Done!', 'magenta'))
|
||||
logging.info(color(f'@@@ Average latency: {average_latency:.2f}'))
|
||||
logging.info(color('=== Done!', 'magenta'))
|
||||
|
||||
async def send_next_ping(self):
|
||||
packet = struct.pack(
|
||||
@@ -433,7 +438,7 @@ class Ping:
|
||||
else 0,
|
||||
self.current_packet_index,
|
||||
) + bytes(self.tx_packet_size - 6)
|
||||
print(color(f'Sending packet {self.current_packet_index}', 'yellow'))
|
||||
logging.info(color(f'Sending packet {self.current_packet_index}', 'yellow'))
|
||||
self.ping_sent_time = time.time()
|
||||
await self.packet_io.send_packet(packet)
|
||||
|
||||
@@ -453,7 +458,7 @@ class Ping:
|
||||
if packet_type == PacketType.ACK:
|
||||
latency = elapsed * 1000
|
||||
self.latencies.append(latency)
|
||||
print(
|
||||
logging.info(
|
||||
color(
|
||||
f'<<< Received ACK [{packet_index}], latency={latency:.2f}ms',
|
||||
'green',
|
||||
@@ -463,7 +468,7 @@ class Ping:
|
||||
if packet_index == self.current_packet_index:
|
||||
self.current_packet_index += 1
|
||||
else:
|
||||
print(
|
||||
logging.info(
|
||||
color(
|
||||
f'!!! Unexpected packet, expected {self.current_packet_index} '
|
||||
f'but received {packet_index}'
|
||||
@@ -481,6 +486,8 @@ class Ping:
|
||||
# Pong
|
||||
# -----------------------------------------------------------------------------
|
||||
class Pong:
|
||||
expected_packet_index: int
|
||||
|
||||
def __init__(self, packet_io):
|
||||
self.reset()
|
||||
self.packet_io = packet_io
|
||||
@@ -497,7 +504,7 @@ class Pong:
|
||||
return
|
||||
|
||||
if packet_type == PacketType.RESET:
|
||||
print(color('=== Received RESET', 'magenta'))
|
||||
logging.info(color('=== Received RESET', 'magenta'))
|
||||
self.reset()
|
||||
return
|
||||
|
||||
@@ -505,7 +512,7 @@ class Pong:
|
||||
packet_flags, packet_index = parse_packet_sequence(packet_data)
|
||||
except ValueError:
|
||||
return
|
||||
print(
|
||||
logging.info(
|
||||
color(
|
||||
f'<<< Received packet {packet_index}: '
|
||||
f'flags=0x{packet_flags:02X}, {len(packet)} bytes',
|
||||
@@ -514,7 +521,7 @@ class Pong:
|
||||
)
|
||||
|
||||
if packet_index != self.expected_packet_index:
|
||||
print(
|
||||
logging.info(
|
||||
color(
|
||||
f'!!! Unexpected packet, expected {self.expected_packet_index} '
|
||||
f'but received {packet_index}'
|
||||
@@ -534,7 +541,7 @@ class Pong:
|
||||
|
||||
async def run(self):
|
||||
await self.done.wait()
|
||||
print(color('=== Done!', 'magenta'))
|
||||
logging.info(color('=== Done!', 'magenta'))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -552,36 +559,36 @@ class GattClient:
|
||||
peer = Peer(connection)
|
||||
|
||||
if self.att_mtu:
|
||||
print(color(f'*** Requesting MTU update: {self.att_mtu}', 'blue'))
|
||||
logging.info(color(f'*** Requesting MTU update: {self.att_mtu}', 'blue'))
|
||||
await peer.request_mtu(self.att_mtu)
|
||||
|
||||
print(color('*** Discovering services...', 'blue'))
|
||||
logging.info(color('*** Discovering services...', 'blue'))
|
||||
await peer.discover_services()
|
||||
|
||||
speed_services = peer.get_services_by_uuid(SPEED_SERVICE_UUID)
|
||||
if not speed_services:
|
||||
print(color('!!! Speed Service not found', 'red'))
|
||||
logging.info(color('!!! Speed Service not found', 'red'))
|
||||
return
|
||||
speed_service = speed_services[0]
|
||||
print(color('*** Discovering characteristics...', 'blue'))
|
||||
logging.info(color('*** Discovering characteristics...', 'blue'))
|
||||
await speed_service.discover_characteristics()
|
||||
|
||||
speed_txs = speed_service.get_characteristics_by_uuid(SPEED_TX_UUID)
|
||||
if not speed_txs:
|
||||
print(color('!!! Speed TX not found', 'red'))
|
||||
logging.info(color('!!! Speed TX not found', 'red'))
|
||||
return
|
||||
self.speed_tx = speed_txs[0]
|
||||
|
||||
speed_rxs = speed_service.get_characteristics_by_uuid(SPEED_RX_UUID)
|
||||
if not speed_rxs:
|
||||
print(color('!!! Speed RX not found', 'red'))
|
||||
logging.info(color('!!! Speed RX not found', 'red'))
|
||||
return
|
||||
self.speed_rx = speed_rxs[0]
|
||||
|
||||
print(color('*** Subscribing to RX', 'blue'))
|
||||
logging.info(color('*** Subscribing to RX', 'blue'))
|
||||
await self.speed_rx.subscribe(self.on_packet_received)
|
||||
|
||||
print(color('*** Discovery complete', 'blue'))
|
||||
logging.info(color('*** Discovery complete', 'blue'))
|
||||
|
||||
connection.on('disconnection', self.on_disconnection)
|
||||
self.ready.set()
|
||||
@@ -633,10 +640,10 @@ class GattServer:
|
||||
|
||||
def on_rx_subscription(self, _connection, notify_enabled, _indicate_enabled):
|
||||
if notify_enabled:
|
||||
print(color('*** RX subscription', 'blue'))
|
||||
logging.info(color('*** RX subscription', 'blue'))
|
||||
self.ready.set()
|
||||
else:
|
||||
print(color('*** RX un-subscription', 'blue'))
|
||||
logging.info(color('*** RX un-subscription', 'blue'))
|
||||
self.ready.clear()
|
||||
|
||||
def on_tx_write(self, _, value):
|
||||
@@ -684,7 +691,7 @@ class StreamedPacketIO:
|
||||
|
||||
async def send_packet(self, packet):
|
||||
if not self.io_sink:
|
||||
print(color('!!! No sink, dropping packet', 'red'))
|
||||
logging.info(color('!!! No sink, dropping packet', 'red'))
|
||||
return
|
||||
|
||||
# pylint: disable-next=not-callable
|
||||
@@ -714,7 +721,7 @@ class L2capClient(StreamedPacketIO):
|
||||
connection.on('disconnection', self.on_disconnection)
|
||||
|
||||
# Connect a new L2CAP channel
|
||||
print(color(f'>>> Opening L2CAP channel on PSM = {self.psm}', 'yellow'))
|
||||
logging.info(color(f'>>> Opening L2CAP channel on PSM = {self.psm}', 'yellow'))
|
||||
try:
|
||||
l2cap_channel = await connection.create_l2cap_channel(
|
||||
spec=l2cap.LeCreditBasedChannelSpec(
|
||||
@@ -724,9 +731,9 @@ class L2capClient(StreamedPacketIO):
|
||||
mps=self.mps,
|
||||
)
|
||||
)
|
||||
print(color('*** L2CAP channel:', 'cyan'), l2cap_channel)
|
||||
logging.info(color(f'*** L2CAP channel: {l2cap_channel}', 'cyan'))
|
||||
except Exception as error:
|
||||
print(color(f'!!! Connection failed: {error}', 'red'))
|
||||
logging.info(color(f'!!! Connection failed: {error}', 'red'))
|
||||
return
|
||||
|
||||
l2cap_channel.sink = self.on_packet
|
||||
@@ -739,7 +746,7 @@ class L2capClient(StreamedPacketIO):
|
||||
pass
|
||||
|
||||
def on_l2cap_close(self):
|
||||
print(color('*** L2CAP channel closed', 'red'))
|
||||
logging.info(color('*** L2CAP channel closed', 'red'))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -765,7 +772,9 @@ class L2capServer(StreamedPacketIO):
|
||||
),
|
||||
handler=self.on_l2cap_channel,
|
||||
)
|
||||
print(color(f'### Listening for L2CAP connection on PSM {psm}', 'yellow'))
|
||||
logging.info(
|
||||
color(f'### Listening for L2CAP connection on PSM {psm}', 'yellow')
|
||||
)
|
||||
|
||||
async def on_connection(self, connection):
|
||||
connection.on('disconnection', self.on_disconnection)
|
||||
@@ -774,7 +783,7 @@ class L2capServer(StreamedPacketIO):
|
||||
pass
|
||||
|
||||
def on_l2cap_channel(self, l2cap_channel):
|
||||
print(color('*** L2CAP channel:', 'cyan'), l2cap_channel)
|
||||
logging.info(color(f'*** L2CAP channel: {l2cap_channel}', 'cyan'))
|
||||
|
||||
self.io_sink = l2cap_channel.write
|
||||
l2cap_channel.on('close', self.on_l2cap_close)
|
||||
@@ -783,7 +792,7 @@ class L2capServer(StreamedPacketIO):
|
||||
self.ready.set()
|
||||
|
||||
def on_l2cap_close(self):
|
||||
print(color('*** L2CAP channel closed', 'red'))
|
||||
logging.info(color('*** L2CAP channel closed', 'red'))
|
||||
self.l2cap_channel = None
|
||||
|
||||
|
||||
@@ -804,28 +813,28 @@ class RfcommClient(StreamedPacketIO):
|
||||
# Find the channel number if not specified
|
||||
channel = self.channel
|
||||
if channel == 0:
|
||||
print(
|
||||
logging.info(
|
||||
color(f'@@@ Discovering channel number from UUID {self.uuid}', 'cyan')
|
||||
)
|
||||
channel = await find_rfcomm_channel_with_uuid(connection, self.uuid)
|
||||
print(color(f'@@@ Channel number = {channel}', 'cyan'))
|
||||
logging.info(color(f'@@@ Channel number = {channel}', 'cyan'))
|
||||
if channel == 0:
|
||||
print(color('!!! No RFComm service with this UUID found', 'red'))
|
||||
logging.info(color('!!! No RFComm service with this UUID found', 'red'))
|
||||
await connection.disconnect()
|
||||
return
|
||||
|
||||
# Create a client and start it
|
||||
print(color('*** Starting RFCOMM client...', 'blue'))
|
||||
logging.info(color('*** Starting RFCOMM client...', 'blue'))
|
||||
rfcomm_client = bumble.rfcomm.Client(connection)
|
||||
rfcomm_mux = await rfcomm_client.start()
|
||||
print(color('*** Started', 'blue'))
|
||||
logging.info(color('*** Started', 'blue'))
|
||||
|
||||
print(color(f'### Opening session for channel {channel}...', 'yellow'))
|
||||
logging.info(color(f'### Opening session for channel {channel}...', 'yellow'))
|
||||
try:
|
||||
rfcomm_session = await rfcomm_mux.open_dlc(channel)
|
||||
print(color('### Session open', 'yellow'), rfcomm_session)
|
||||
logging.info(color(f'### Session open: {rfcomm_session}', 'yellow'))
|
||||
except bumble.core.ConnectionError as error:
|
||||
print(color(f'!!! Session open failed: {error}', 'red'))
|
||||
logging.info(color(f'!!! Session open failed: {error}', 'red'))
|
||||
await rfcomm_mux.disconnect()
|
||||
return
|
||||
|
||||
@@ -855,7 +864,7 @@ class RfcommServer(StreamedPacketIO):
|
||||
# Setup the SDP to advertise this channel
|
||||
device.sdp_service_records = make_sdp_records(channel_number)
|
||||
|
||||
print(
|
||||
logging.info(
|
||||
color(
|
||||
f'### Listening for RFComm connection on channel {channel_number}',
|
||||
'yellow',
|
||||
@@ -869,7 +878,7 @@ class RfcommServer(StreamedPacketIO):
|
||||
pass
|
||||
|
||||
def on_dlc(self, dlc):
|
||||
print(color('*** DLC connected:', 'blue'), dlc)
|
||||
logging.info(color(f'*** DLC connected: {dlc}', 'blue'))
|
||||
dlc.sink = self.on_packet
|
||||
self.io_sink = dlc.write
|
||||
|
||||
@@ -935,12 +944,12 @@ class Central(Connection.Listener):
|
||||
self.connection_parameter_preferences = None
|
||||
|
||||
async def run(self):
|
||||
print(color('>>> Connecting to HCI...', 'green'))
|
||||
logging.info(color('>>> Connecting to HCI...', 'green'))
|
||||
async with await open_transport_or_link(self.transport) as (
|
||||
hci_source,
|
||||
hci_sink,
|
||||
):
|
||||
print(color('>>> Connected', 'green'))
|
||||
logging.info(color('>>> Connected', 'green'))
|
||||
|
||||
central_address = DEFAULT_CENTRAL_ADDRESS
|
||||
self.device = Device.with_hci(
|
||||
@@ -952,7 +961,13 @@ class Central(Connection.Listener):
|
||||
|
||||
await self.device.power_on()
|
||||
|
||||
print(color(f'### Connecting to {self.peripheral_address}...', 'cyan'))
|
||||
if self.classic:
|
||||
await self.device.set_discoverable(False)
|
||||
await self.device.set_connectable(False)
|
||||
|
||||
logging.info(
|
||||
color(f'### Connecting to {self.peripheral_address}...', 'cyan')
|
||||
)
|
||||
try:
|
||||
self.connection = await self.device.connect(
|
||||
self.peripheral_address,
|
||||
@@ -960,21 +975,26 @@ class Central(Connection.Listener):
|
||||
transport=BT_BR_EDR_TRANSPORT if self.classic else BT_LE_TRANSPORT,
|
||||
)
|
||||
except CommandTimeoutError:
|
||||
print(color('!!! Connection timed out', 'red'))
|
||||
logging.info(color('!!! Connection timed out', 'red'))
|
||||
return
|
||||
except bumble.core.ConnectionError as error:
|
||||
print(color(f'!!! Connection error: {error}', 'red'))
|
||||
logging.info(color(f'!!! Connection error: {error}', 'red'))
|
||||
return
|
||||
except HCI_StatusError as error:
|
||||
print(color(f'!!! Connection failed: {error.error_name}'))
|
||||
logging.info(color(f'!!! Connection failed: {error.error_name}'))
|
||||
return
|
||||
print(color('### Connected', 'cyan'))
|
||||
logging.info(color('### Connected', 'cyan'))
|
||||
self.connection.listener = self
|
||||
print_connection(self.connection)
|
||||
|
||||
# Wait a bit after the connection, some controllers aren't very good when
|
||||
# we start sending data right away while some connection parameters are
|
||||
# updated post connection
|
||||
await asyncio.sleep(DEFAULT_POST_CONNECTION_WAIT_TIME)
|
||||
|
||||
# Request a new data length if requested
|
||||
if self.extended_data_length:
|
||||
print(color('+++ Requesting extended data length', 'cyan'))
|
||||
logging.info(color('+++ Requesting extended data length', 'cyan'))
|
||||
await self.connection.set_data_length(
|
||||
self.extended_data_length[0], self.extended_data_length[1]
|
||||
)
|
||||
@@ -982,16 +1002,16 @@ class Central(Connection.Listener):
|
||||
# Authenticate if requested
|
||||
if self.authenticate:
|
||||
# Request authentication
|
||||
print(color('*** Authenticating...', 'cyan'))
|
||||
logging.info(color('*** Authenticating...', 'cyan'))
|
||||
await self.connection.authenticate()
|
||||
print(color('*** Authenticated', 'cyan'))
|
||||
logging.info(color('*** Authenticated', 'cyan'))
|
||||
|
||||
# Encrypt if requested
|
||||
if self.encrypt:
|
||||
# Enable encryption
|
||||
print(color('*** Enabling encryption...', 'cyan'))
|
||||
logging.info(color('*** Enabling encryption...', 'cyan'))
|
||||
await self.connection.encrypt()
|
||||
print(color('*** Encryption on', 'cyan'))
|
||||
logging.info(color('*** Encryption on', 'cyan'))
|
||||
|
||||
# Set the PHY if requested
|
||||
if self.phy is not None:
|
||||
@@ -1000,7 +1020,7 @@ class Central(Connection.Listener):
|
||||
tx_phys=[self.phy], rx_phys=[self.phy]
|
||||
)
|
||||
except HCI_Error as error:
|
||||
print(
|
||||
logging.info(
|
||||
color(
|
||||
f'!!! Unable to set the PHY: {error.error_name}', 'yellow'
|
||||
)
|
||||
@@ -1012,7 +1032,7 @@ class Central(Connection.Listener):
|
||||
await asyncio.sleep(DEFAULT_LINGER_TIME)
|
||||
|
||||
def on_disconnection(self, reason):
|
||||
print(color(f'!!! Disconnection: reason={reason}', 'red'))
|
||||
logging.info(color(f'!!! Disconnection: reason={reason}', 'red'))
|
||||
self.connection = None
|
||||
|
||||
def on_connection_parameters_update(self):
|
||||
@@ -1047,12 +1067,12 @@ class Peripheral(Device.Listener, Connection.Listener):
|
||||
self.connected = asyncio.Event()
|
||||
|
||||
async def run(self):
|
||||
print(color('>>> Connecting to HCI...', 'green'))
|
||||
logging.info(color('>>> Connecting to HCI...', 'green'))
|
||||
async with await open_transport_or_link(self.transport) as (
|
||||
hci_source,
|
||||
hci_sink,
|
||||
):
|
||||
print(color('>>> Connected', 'green'))
|
||||
logging.info(color('>>> Connected', 'green'))
|
||||
|
||||
peripheral_address = DEFAULT_PERIPHERAL_ADDRESS
|
||||
self.device = Device.with_hci(
|
||||
@@ -1072,7 +1092,7 @@ class Peripheral(Device.Listener, Connection.Listener):
|
||||
await self.device.start_advertising(auto_restart=True)
|
||||
|
||||
if self.classic:
|
||||
print(
|
||||
logging.info(
|
||||
color(
|
||||
'### Waiting for connection on'
|
||||
f' {self.device.public_address}...',
|
||||
@@ -1080,14 +1100,14 @@ class Peripheral(Device.Listener, Connection.Listener):
|
||||
)
|
||||
)
|
||||
else:
|
||||
print(
|
||||
logging.info(
|
||||
color(
|
||||
f'### Waiting for connection on {peripheral_address}...',
|
||||
'cyan',
|
||||
)
|
||||
)
|
||||
await self.connected.wait()
|
||||
print(color('### Connected', 'cyan'))
|
||||
logging.info(color('### Connected', 'cyan'))
|
||||
|
||||
await self.mode.on_connection(self.connection)
|
||||
await self.role.run()
|
||||
@@ -1098,9 +1118,18 @@ class Peripheral(Device.Listener, Connection.Listener):
|
||||
self.connection = connection
|
||||
self.connected.set()
|
||||
|
||||
# Stop being discoverable and connectable
|
||||
if self.classic:
|
||||
|
||||
async def stop_being_discoverable_connectable():
|
||||
await self.device.set_discoverable(False)
|
||||
await self.device.set_connectable(False)
|
||||
|
||||
AsyncRunner.spawn(stop_being_discoverable_connectable())
|
||||
|
||||
# Request a new data length if needed
|
||||
if self.extended_data_length:
|
||||
print("+++ Requesting extended data length")
|
||||
logging.info("+++ Requesting extended data length")
|
||||
AsyncRunner.spawn(
|
||||
connection.set_data_length(
|
||||
self.extended_data_length[0], self.extended_data_length[1]
|
||||
@@ -1108,7 +1137,7 @@ class Peripheral(Device.Listener, Connection.Listener):
|
||||
)
|
||||
|
||||
def on_disconnection(self, reason):
|
||||
print(color(f'!!! Disconnection: reason={reason}', 'red'))
|
||||
logging.info(color(f'!!! Disconnection: reason={reason}', 'red'))
|
||||
self.connection = None
|
||||
self.role.reset()
|
||||
|
||||
|
||||
@@ -32,10 +32,14 @@ from bumble.hci import (
|
||||
HCI_Command,
|
||||
HCI_Command_Complete_Event,
|
||||
HCI_Command_Status_Event,
|
||||
HCI_READ_BUFFER_SIZE_COMMAND,
|
||||
HCI_Read_Buffer_Size_Command,
|
||||
HCI_READ_BD_ADDR_COMMAND,
|
||||
HCI_Read_BD_ADDR_Command,
|
||||
HCI_READ_LOCAL_NAME_COMMAND,
|
||||
HCI_Read_Local_Name_Command,
|
||||
HCI_LE_READ_BUFFER_SIZE_COMMAND,
|
||||
HCI_LE_Read_Buffer_Size_Command,
|
||||
HCI_LE_READ_MAXIMUM_DATA_LENGTH_COMMAND,
|
||||
HCI_LE_Read_Maximum_Data_Length_Command,
|
||||
HCI_LE_READ_NUMBER_OF_SUPPORTED_ADVERTISING_SETS_COMMAND,
|
||||
@@ -59,7 +63,7 @@ def command_succeeded(response):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def get_classic_info(host):
|
||||
async def get_classic_info(host: Host) -> None:
|
||||
if host.supports_command(HCI_READ_BD_ADDR_COMMAND):
|
||||
response = await host.send_command(HCI_Read_BD_ADDR_Command())
|
||||
if command_succeeded(response):
|
||||
@@ -80,7 +84,7 @@ async def get_classic_info(host):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def get_le_info(host):
|
||||
async def get_le_info(host: Host) -> None:
|
||||
print()
|
||||
|
||||
if host.supports_command(HCI_LE_READ_NUMBER_OF_SUPPORTED_ADVERTISING_SETS_COMMAND):
|
||||
@@ -136,6 +140,31 @@ async def get_le_info(host):
|
||||
print(' ', name_or_number(HCI_LE_SUPPORTED_FEATURES_NAMES, feature))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def get_acl_flow_control_info(host: Host) -> None:
|
||||
print()
|
||||
|
||||
if host.supports_command(HCI_READ_BUFFER_SIZE_COMMAND):
|
||||
response = await host.send_command(
|
||||
HCI_Read_Buffer_Size_Command(), check_result=True
|
||||
)
|
||||
print(
|
||||
color('ACL Flow Control:', 'yellow'),
|
||||
f'{response.return_parameters.hc_total_num_acl_data_packets} '
|
||||
f'packets of size {response.return_parameters.hc_acl_data_packet_length}',
|
||||
)
|
||||
|
||||
if host.supports_command(HCI_LE_READ_BUFFER_SIZE_COMMAND):
|
||||
response = await host.send_command(
|
||||
HCI_LE_Read_Buffer_Size_Command(), check_result=True
|
||||
)
|
||||
print(
|
||||
color('LE ACL Flow Control:', 'yellow'),
|
||||
f'{response.return_parameters.hc_total_num_le_acl_data_packets} '
|
||||
f'packets of size {response.return_parameters.hc_le_acl_data_packet_length}',
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def async_main(transport):
|
||||
print('<<< connecting to HCI...')
|
||||
@@ -168,6 +197,9 @@ async def async_main(transport):
|
||||
# Get the LE info
|
||||
await get_le_info(host)
|
||||
|
||||
# Print the ACL flow control info
|
||||
await get_acl_flow_control_info(host)
|
||||
|
||||
# Print the list of commands supported by the controller
|
||||
print()
|
||||
print(color('Supported Commands:', 'yellow'))
|
||||
|
||||
52
apps/scan.py
52
apps/scan.py
@@ -26,7 +26,7 @@ from bumble.transport import open_transport_or_link
|
||||
from bumble.keys import JsonKeyStore
|
||||
from bumble.smp import AddressResolver
|
||||
from bumble.device import Advertisement
|
||||
from bumble.hci import HCI_Constant, HCI_LE_1M_PHY, HCI_LE_CODED_PHY
|
||||
from bumble.hci import Address, HCI_Constant, HCI_LE_1M_PHY, HCI_LE_CODED_PHY
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -66,10 +66,15 @@ class AdvertisementPrinter:
|
||||
address_type_string = ('PUBLIC', 'RANDOM', 'PUBLIC_ID', 'RANDOM_ID')[
|
||||
address.address_type
|
||||
]
|
||||
if address.is_public:
|
||||
type_color = 'cyan'
|
||||
if address.address_type in (
|
||||
Address.RANDOM_IDENTITY_ADDRESS,
|
||||
Address.PUBLIC_IDENTITY_ADDRESS,
|
||||
):
|
||||
type_color = 'yellow'
|
||||
else:
|
||||
if address.is_static:
|
||||
if address.is_public:
|
||||
type_color = 'cyan'
|
||||
elif address.is_static:
|
||||
type_color = 'green'
|
||||
address_qualifier = '(static)'
|
||||
elif address.is_resolvable:
|
||||
@@ -116,6 +121,7 @@ async def scan(
|
||||
phy,
|
||||
filter_duplicates,
|
||||
raw,
|
||||
irks,
|
||||
keystore_file,
|
||||
device_config,
|
||||
transport,
|
||||
@@ -140,9 +146,21 @@ async def scan(
|
||||
|
||||
if device.keystore:
|
||||
resolving_keys = await device.keystore.get_resolving_keys()
|
||||
resolver = AddressResolver(resolving_keys)
|
||||
else:
|
||||
resolver = None
|
||||
resolving_keys = []
|
||||
|
||||
for irk_and_address in irks:
|
||||
if ':' not in irk_and_address:
|
||||
raise ValueError('invalid IRK:ADDRESS value')
|
||||
irk_hex, address_str = irk_and_address.split(':', 1)
|
||||
resolving_keys.append(
|
||||
(
|
||||
bytes.fromhex(irk_hex),
|
||||
Address(address_str, Address.RANDOM_DEVICE_ADDRESS),
|
||||
)
|
||||
)
|
||||
|
||||
resolver = AddressResolver(resolving_keys) if resolving_keys else None
|
||||
|
||||
printer = AdvertisementPrinter(min_rssi, resolver)
|
||||
if raw:
|
||||
@@ -187,8 +205,24 @@ async def scan(
|
||||
default=False,
|
||||
help='Listen for raw advertising reports instead of processed ones',
|
||||
)
|
||||
@click.option('--keystore-file', help='Keystore file to use when resolving addresses')
|
||||
@click.option('--device-config', help='Device config file for the scanning device')
|
||||
@click.option(
|
||||
'--irk',
|
||||
metavar='<IRK_HEX>:<ADDRESS>',
|
||||
help=(
|
||||
'Use this IRK for resolving private addresses ' '(may be used more than once)'
|
||||
),
|
||||
multiple=True,
|
||||
)
|
||||
@click.option(
|
||||
'--keystore-file',
|
||||
metavar='FILE_PATH',
|
||||
help='Keystore file to use when resolving addresses',
|
||||
)
|
||||
@click.option(
|
||||
'--device-config',
|
||||
metavar='FILE_PATH',
|
||||
help='Device config file for the scanning device',
|
||||
)
|
||||
@click.argument('transport')
|
||||
def main(
|
||||
min_rssi,
|
||||
@@ -198,6 +232,7 @@ def main(
|
||||
phy,
|
||||
filter_duplicates,
|
||||
raw,
|
||||
irk,
|
||||
keystore_file,
|
||||
device_config,
|
||||
transport,
|
||||
@@ -212,6 +247,7 @@ def main(
|
||||
phy,
|
||||
filter_duplicates,
|
||||
raw,
|
||||
irk,
|
||||
keystore_file,
|
||||
device_config,
|
||||
transport,
|
||||
|
||||
@@ -134,12 +134,14 @@ class Controller:
|
||||
'0000000060000000'
|
||||
) # BR/EDR Not Supported, LE Supported (Controller)
|
||||
self.manufacturer_name = 0xFFFF
|
||||
self.hc_data_packet_length = 27
|
||||
self.hc_total_num_data_packets = 64
|
||||
self.hc_le_data_packet_length = 27
|
||||
self.hc_total_num_le_data_packets = 64
|
||||
self.event_mask = 0
|
||||
self.event_mask_page_2 = 0
|
||||
self.supported_commands = bytes.fromhex(
|
||||
'2000800000c000000000e40000002822000000000000040000f7ffff7f000000'
|
||||
'2000800000c000000000e4000000a822000000000000040000f7ffff7f000000'
|
||||
'30f0f9ff01008004000000000000000000000000000000000000000000000000'
|
||||
)
|
||||
self.le_event_mask = 0
|
||||
@@ -914,6 +916,19 @@ class Controller:
|
||||
'''
|
||||
return bytes([HCI_SUCCESS]) + self.lmp_features
|
||||
|
||||
def on_hci_read_buffer_size_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 4, Part E - 7.4.5 Read Buffer Size Command
|
||||
'''
|
||||
return struct.pack(
|
||||
'<BHBHH',
|
||||
HCI_SUCCESS,
|
||||
self.hc_data_packet_length,
|
||||
0,
|
||||
self.hc_total_num_data_packets,
|
||||
0,
|
||||
)
|
||||
|
||||
def on_hci_read_bd_addr_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 4, Part E - 7.4.6 Read BD_ADDR Command
|
||||
|
||||
@@ -402,7 +402,7 @@ class Device(HID):
|
||||
report_type = pdu[0] & 0x03
|
||||
buffer_flag = (pdu[0] & 0x08) >> 3
|
||||
report_id = pdu[1]
|
||||
logger.debug("buffer_flag: " + str(buffer_flag))
|
||||
logger.debug(f"buffer_flag: {buffer_flag}")
|
||||
if buffer_flag == 1:
|
||||
buffer_size = (pdu[3] << 8) | pdu[2]
|
||||
else:
|
||||
|
||||
177
bumble/host.py
177
bumble/host.py
@@ -21,7 +21,7 @@ import collections
|
||||
import logging
|
||||
import struct
|
||||
|
||||
from typing import Any, Awaitable, Callable, Dict, Optional, Union, cast, TYPE_CHECKING
|
||||
from typing import Any, Awaitable, Callable, Deque, Dict, Optional, cast, TYPE_CHECKING
|
||||
|
||||
from bumble.colors import color
|
||||
from bumble.l2cap import L2CAP_PDU
|
||||
@@ -91,16 +91,49 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
# fmt: off
|
||||
class AclPacketQueue:
|
||||
max_packet_size: int
|
||||
|
||||
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
|
||||
def __init__(
|
||||
self,
|
||||
max_packet_size: int,
|
||||
max_in_flight: int,
|
||||
send: Callable[[HCI_Packet], None],
|
||||
) -> None:
|
||||
self.max_packet_size = max_packet_size
|
||||
self.max_in_flight = max_in_flight
|
||||
self.in_flight = 0
|
||||
self.send = send
|
||||
self.packets: Deque[HCI_AclDataPacket] = collections.deque()
|
||||
|
||||
# fmt: on
|
||||
def enqueue(self, packet: HCI_AclDataPacket) -> None:
|
||||
self.packets.appendleft(packet)
|
||||
self.check_queue()
|
||||
|
||||
if self.packets:
|
||||
logger.debug(
|
||||
f'{self.in_flight} ACL packets in flight, '
|
||||
f'{len(self.packets)} in queue'
|
||||
)
|
||||
|
||||
def check_queue(self) -> None:
|
||||
while self.packets and self.in_flight < self.max_in_flight:
|
||||
packet = self.packets.pop()
|
||||
self.send(packet)
|
||||
self.in_flight += 1
|
||||
|
||||
def on_packets_completed(self, packet_count: int) -> None:
|
||||
if packet_count > self.in_flight:
|
||||
logger.warning(
|
||||
color(
|
||||
'!!! {packet_count} completed but only '
|
||||
f'{self.in_flight} in flight'
|
||||
)
|
||||
)
|
||||
packet_count = self.in_flight
|
||||
|
||||
self.in_flight -= packet_count
|
||||
self.check_queue()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -111,6 +144,13 @@ class Connection:
|
||||
self.peer_address = peer_address
|
||||
self.assembler = HCI_AclDataPacketAssembler(self.on_acl_pdu)
|
||||
self.transport = transport
|
||||
acl_packet_queue: Optional[AclPacketQueue] = (
|
||||
host.le_acl_packet_queue
|
||||
if transport == BT_LE_TRANSPORT
|
||||
else host.acl_packet_queue
|
||||
)
|
||||
assert acl_packet_queue
|
||||
self.acl_packet_queue = acl_packet_queue
|
||||
|
||||
def on_hci_acl_data_packet(self, packet: HCI_AclDataPacket) -> None:
|
||||
self.assembler.feed_packet(packet)
|
||||
@@ -123,7 +163,8 @@ class Connection:
|
||||
# -----------------------------------------------------------------------------
|
||||
class Host(AbortableEventEmitter):
|
||||
connections: Dict[int, Connection]
|
||||
acl_packet_queue: collections.deque[HCI_AclDataPacket]
|
||||
acl_packet_queue: Optional[AclPacketQueue] = None
|
||||
le_acl_packet_queue: Optional[AclPacketQueue] = None
|
||||
hci_sink: Optional[TransportSink] = None
|
||||
hci_metadata: Dict[str, Any]
|
||||
long_term_key_provider: Optional[
|
||||
@@ -143,12 +184,6 @@ class Host(AbortableEventEmitter):
|
||||
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_version = None
|
||||
self.local_supported_commands = bytes(64)
|
||||
self.local_le_features = 0
|
||||
@@ -254,46 +289,54 @@ class Host(AbortableEventEmitter):
|
||||
response = await self.send_command(
|
||||
HCI_Read_Buffer_Size_Command(), check_result=True
|
||||
)
|
||||
self.hc_acl_data_packet_length = (
|
||||
hc_acl_data_packet_length = (
|
||||
response.return_parameters.hc_acl_data_packet_length
|
||||
)
|
||||
self.hc_total_num_acl_data_packets = (
|
||||
hc_total_num_acl_data_packets = (
|
||||
response.return_parameters.hc_total_num_acl_data_packets
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
'HCI ACL flow control: '
|
||||
f'hc_acl_data_packet_length={self.hc_acl_data_packet_length},'
|
||||
f'hc_total_num_acl_data_packets={self.hc_total_num_acl_data_packets}'
|
||||
f'hc_acl_data_packet_length={hc_acl_data_packet_length},'
|
||||
f'hc_total_num_acl_data_packets={hc_total_num_acl_data_packets}'
|
||||
)
|
||||
|
||||
self.acl_packet_queue = AclPacketQueue(
|
||||
max_packet_size=hc_acl_data_packet_length,
|
||||
max_in_flight=hc_total_num_acl_data_packets,
|
||||
send=self.send_hci_packet,
|
||||
)
|
||||
|
||||
hc_le_acl_data_packet_length = 0
|
||||
hc_total_num_le_acl_data_packets = 0
|
||||
if self.supports_command(HCI_LE_READ_BUFFER_SIZE_COMMAND):
|
||||
response = await self.send_command(
|
||||
HCI_LE_Read_Buffer_Size_Command(), check_result=True
|
||||
)
|
||||
self.hc_le_acl_data_packet_length = (
|
||||
hc_le_acl_data_packet_length = (
|
||||
response.return_parameters.hc_le_acl_data_packet_length
|
||||
)
|
||||
self.hc_total_num_le_acl_data_packets = (
|
||||
hc_total_num_le_acl_data_packets = (
|
||||
response.return_parameters.hc_total_num_le_acl_data_packets
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
'HCI LE ACL flow control: '
|
||||
f'hc_le_acl_data_packet_length={self.hc_le_acl_data_packet_length},'
|
||||
'hc_total_num_le_acl_data_packets='
|
||||
f'{self.hc_total_num_le_acl_data_packets}'
|
||||
f'hc_le_acl_data_packet_length={hc_le_acl_data_packet_length},'
|
||||
f'hc_total_num_le_acl_data_packets={hc_total_num_le_acl_data_packets}'
|
||||
)
|
||||
|
||||
if (
|
||||
response.return_parameters.hc_le_acl_data_packet_length == 0
|
||||
or response.return_parameters.hc_total_num_le_acl_data_packets == 0
|
||||
):
|
||||
# LE and Classic share the same values
|
||||
self.hc_le_acl_data_packet_length = self.hc_acl_data_packet_length
|
||||
self.hc_total_num_le_acl_data_packets = (
|
||||
self.hc_total_num_acl_data_packets
|
||||
)
|
||||
if hc_le_acl_data_packet_length == 0 or hc_total_num_le_acl_data_packets == 0:
|
||||
# LE and Classic share the same queue
|
||||
self.le_acl_packet_queue = self.acl_packet_queue
|
||||
else:
|
||||
# Create a separate queue for LE
|
||||
self.le_acl_packet_queue = AclPacketQueue(
|
||||
max_packet_size=hc_le_acl_data_packet_length,
|
||||
max_in_flight=hc_total_num_le_acl_data_packets,
|
||||
send=self.send_hci_packet,
|
||||
)
|
||||
|
||||
if self.supports_command(
|
||||
HCI_LE_READ_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND
|
||||
@@ -332,14 +375,13 @@ class Host(AbortableEventEmitter):
|
||||
self.hci_metadata = getattr(source, 'metadata', self.hci_metadata)
|
||||
|
||||
def send_hci_packet(self, packet: HCI_Packet) -> None:
|
||||
logger.debug(f'{color("### HOST -> CONTROLLER", "blue")}: {packet}')
|
||||
if self.snooper:
|
||||
self.snooper.snoop(bytes(packet), Snooper.Direction.HOST_TO_CONTROLLER)
|
||||
if self.hci_sink:
|
||||
self.hci_sink.on_packet(bytes(packet))
|
||||
|
||||
async def send_command(self, command, check_result=False):
|
||||
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
|
||||
@@ -387,6 +429,17 @@ class Host(AbortableEventEmitter):
|
||||
asyncio.create_task(send_command(command))
|
||||
|
||||
def send_l2cap_pdu(self, connection_handle: int, cid: int, pdu: bytes) -> None:
|
||||
if not (connection := self.connections.get(connection_handle)):
|
||||
logger.warning(f'connection 0x{connection_handle:04X} not found')
|
||||
return
|
||||
packet_queue = connection.acl_packet_queue
|
||||
if packet_queue is None:
|
||||
logger.warning(
|
||||
f'no ACL packet queue for connection 0x{connection_handle:04X}'
|
||||
)
|
||||
return
|
||||
|
||||
# Create a PDU
|
||||
l2cap_pdu = bytes(L2CAP_PDU(cid, pdu))
|
||||
|
||||
# Send the data to the controller via ACL packets
|
||||
@@ -394,8 +447,7 @@ class Host(AbortableEventEmitter):
|
||||
offset = 0
|
||||
pb_flag = 0
|
||||
while bytes_remaining:
|
||||
# TODO: support different LE/Classic lengths
|
||||
data_total_length = min(bytes_remaining, self.hc_le_acl_data_packet_length)
|
||||
data_total_length = min(bytes_remaining, packet_queue.max_packet_size)
|
||||
acl_packet = HCI_AclDataPacket(
|
||||
connection_handle=connection_handle,
|
||||
pb_flag=pb_flag,
|
||||
@@ -403,34 +455,12 @@ class Host(AbortableEventEmitter):
|
||||
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)
|
||||
logger.debug(f'>>> ACL packet enqueue: (CID={cid}) {acl_packet}')
|
||||
packet_queue.enqueue(acl_packet)
|
||||
pb_flag = 1
|
||||
offset += data_total_length
|
||||
bytes_remaining -= data_total_length
|
||||
|
||||
def queue_acl_packet(self, acl_packet: HCI_AclDataPacket) -> None:
|
||||
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, '
|
||||
f'{len(self.acl_packet_queue)} in queue'
|
||||
)
|
||||
|
||||
def check_acl_packet_queue(self) -> None:
|
||||
# Send all we can (TODO: support different LE/Classic limits)
|
||||
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
|
||||
|
||||
def supports_command(self, command):
|
||||
# Find the support flag position for this command
|
||||
for octet, flags in enumerate(HCI_SUPPORTED_COMMANDS_FLAGS):
|
||||
@@ -553,7 +583,7 @@ class Host(AbortableEventEmitter):
|
||||
# This is used just for the Num_HCI_Command_Packets field, not related to
|
||||
# an actual command
|
||||
logger.debug('no-command event')
|
||||
return None
|
||||
return
|
||||
|
||||
return self.on_command_processed(event)
|
||||
|
||||
@@ -561,18 +591,17 @@ class Host(AbortableEventEmitter):
|
||||
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(
|
||||
'!!! {total_packets} completed but only '
|
||||
f'{self.acl_packets_in_flight} in flight'
|
||||
for connection_handle, num_completed_packets in zip(
|
||||
event.connection_handles, event.num_completed_packets
|
||||
):
|
||||
if not (connection := self.connections.get(connection_handle)):
|
||||
logger.warning(
|
||||
'received packet completion event for unknown handle '
|
||||
f'0x{connection_handle:04X}'
|
||||
)
|
||||
)
|
||||
self.acl_packets_in_flight = 0
|
||||
continue
|
||||
|
||||
connection.acl_packet_queue.on_packets_completed(num_completed_packets)
|
||||
|
||||
# Classic only
|
||||
def on_hci_connection_request_event(self, event):
|
||||
|
||||
@@ -151,8 +151,8 @@ L2CAP_LE_CREDIT_BASED_CONNECTION_MAX_CREDITS = 65535
|
||||
L2CAP_LE_CREDIT_BASED_CONNECTION_MIN_MTU = 23
|
||||
L2CAP_LE_CREDIT_BASED_CONNECTION_MIN_MPS = 23
|
||||
L2CAP_LE_CREDIT_BASED_CONNECTION_MAX_MPS = 65533
|
||||
L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MTU = 2046
|
||||
L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MPS = 2048
|
||||
L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MTU = 2048
|
||||
L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MPS = 2046
|
||||
L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_INITIAL_CREDITS = 256
|
||||
|
||||
L2CAP_MAXIMUM_TRANSMISSION_UNIT_CONFIGURATION_OPTION_TYPE = 0x01
|
||||
|
||||
@@ -118,8 +118,8 @@ CRC_TABLE = bytes([
|
||||
0XBA, 0X2B, 0X59, 0XC8, 0XBD, 0X2C, 0X5E, 0XCF
|
||||
])
|
||||
|
||||
RFCOMM_DEFAULT_INITIAL_RX_CREDITS = 7
|
||||
RFCOMM_DEFAULT_PREFERRED_MTU = 1280
|
||||
RFCOMM_DEFAULT_WINDOW_SIZE = 16
|
||||
RFCOMM_DEFAULT_MAX_FRAME_SIZE = 2000
|
||||
|
||||
RFCOMM_DYNAMIC_CHANNEL_NUMBER_START = 1
|
||||
RFCOMM_DYNAMIC_CHANNEL_NUMBER_END = 30
|
||||
@@ -438,14 +438,16 @@ class DLC(EventEmitter):
|
||||
multiplexer: Multiplexer,
|
||||
dlci: int,
|
||||
max_frame_size: int,
|
||||
initial_tx_credits: int,
|
||||
window_size: int,
|
||||
) -> None:
|
||||
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.max_frame_size = max_frame_size
|
||||
self.window_size = window_size
|
||||
self.rx_credits = window_size
|
||||
self.rx_threshold = window_size // 2
|
||||
self.tx_credits = window_size
|
||||
self.tx_buffer = b''
|
||||
self.state = DLC.State.INIT
|
||||
self.role = multiplexer.role
|
||||
@@ -537,11 +539,11 @@ class DLC(EventEmitter):
|
||||
if len(data) and self.sink:
|
||||
self.sink(data) # pylint: disable=not-callable
|
||||
|
||||
# Update the credits
|
||||
if self.rx_credits > 0:
|
||||
self.rx_credits -= 1
|
||||
else:
|
||||
logger.warning(color('!!! received frame with no rx credits', 'red'))
|
||||
# Update the credits
|
||||
if self.rx_credits > 0:
|
||||
self.rx_credits -= 1
|
||||
else:
|
||||
logger.warning(color('!!! received frame with no rx credits', 'red'))
|
||||
|
||||
# Check if there's anything to send (including credits)
|
||||
self.process_tx()
|
||||
@@ -580,9 +582,9 @@ class DLC(EventEmitter):
|
||||
cl=0xE0,
|
||||
priority=7,
|
||||
ack_timer=0,
|
||||
max_frame_size=RFCOMM_DEFAULT_PREFERRED_MTU,
|
||||
max_frame_size=self.max_frame_size,
|
||||
max_retransmissions=0,
|
||||
window_size=RFCOMM_DEFAULT_INITIAL_RX_CREDITS,
|
||||
window_size=self.window_size,
|
||||
)
|
||||
mcc = RFCOMM_Frame.make_mcc(mcc_type=RFCOMM_MCC_PN_TYPE, c_r=0, data=bytes(pn))
|
||||
logger.debug(f'>>> PN Response: {pn}')
|
||||
@@ -591,7 +593,7 @@ class DLC(EventEmitter):
|
||||
|
||||
def rx_credits_needed(self) -> int:
|
||||
if self.rx_credits <= self.rx_threshold:
|
||||
return RFCOMM_DEFAULT_INITIAL_RX_CREDITS - self.rx_credits
|
||||
return self.window_size - self.rx_credits
|
||||
|
||||
return 0
|
||||
|
||||
@@ -843,7 +845,12 @@ class Multiplexer(EventEmitter):
|
||||
)
|
||||
await self.disconnection_result
|
||||
|
||||
async def open_dlc(self, channel: int) -> DLC:
|
||||
async def open_dlc(
|
||||
self,
|
||||
channel: int,
|
||||
max_frame_size: int = RFCOMM_DEFAULT_MAX_FRAME_SIZE,
|
||||
window_size: int = RFCOMM_DEFAULT_WINDOW_SIZE,
|
||||
) -> DLC:
|
||||
if self.state != Multiplexer.State.CONNECTED:
|
||||
if self.state == Multiplexer.State.OPENING:
|
||||
raise InvalidStateError('open already in progress')
|
||||
@@ -855,9 +862,9 @@ class Multiplexer(EventEmitter):
|
||||
cl=0xF0,
|
||||
priority=7,
|
||||
ack_timer=0,
|
||||
max_frame_size=RFCOMM_DEFAULT_PREFERRED_MTU,
|
||||
max_frame_size=max_frame_size,
|
||||
max_retransmissions=0,
|
||||
window_size=RFCOMM_DEFAULT_INITIAL_RX_CREDITS,
|
||||
window_size=window_size,
|
||||
)
|
||||
mcc = RFCOMM_Frame.make_mcc(mcc_type=RFCOMM_MCC_PN_TYPE, c_r=1, data=bytes(pn))
|
||||
logger.debug(f'>>> Sending MCC: {pn}')
|
||||
|
||||
@@ -108,7 +108,7 @@ async def open_usb_transport(spec: str) -> Transport:
|
||||
USB_DEVICE_PROTOCOL_BLUETOOTH_PRIMARY_CONTROLLER,
|
||||
)
|
||||
|
||||
READ_SIZE = 1024
|
||||
READ_SIZE = 4096
|
||||
|
||||
class UsbPacketSink:
|
||||
def __init__(self, device, acl_out):
|
||||
|
||||
@@ -590,12 +590,12 @@ async def main():
|
||||
def on_set_protocol_cb(protocol: int):
|
||||
retValue = hid_device.GetSetStatus()
|
||||
# We do not support SET_PROTOCOL.
|
||||
print("SET_PROTOCOL report_id: " + str(protocol))
|
||||
print(f"SET_PROTOCOL report_id: {protocol}")
|
||||
retValue.status = hid_device.GetSetReturn.ERR_UNSUPPORTED_REQUEST
|
||||
return retValue
|
||||
|
||||
def on_virtual_cable_unplug_cb():
|
||||
print(f'Received Virtual Cable Unplug')
|
||||
print('Received Virtual Cable Unplug')
|
||||
asyncio.create_task(handle_virtual_cable_unplug())
|
||||
|
||||
print('<<< connecting to HCI...')
|
||||
|
||||
@@ -28,8 +28,8 @@ private val Log = Logger.getLogger("btbench.l2cap-client")
|
||||
|
||||
class L2capClient(
|
||||
private val viewModel: AppViewModel,
|
||||
val bluetoothAdapter: BluetoothAdapter,
|
||||
val context: Context
|
||||
private val bluetoothAdapter: BluetoothAdapter,
|
||||
private val context: Context
|
||||
) {
|
||||
@SuppressLint("MissingPermission")
|
||||
fun run() {
|
||||
@@ -80,6 +80,10 @@ class L2capClient(
|
||||
BluetoothDevice.PHY_OPTION_NO_PREFERRED
|
||||
)
|
||||
gatt.readPhy()
|
||||
|
||||
// Request an MTU update, even though we don't use GATT, because Android
|
||||
// won't request a larger link layer maximum data length otherwise.
|
||||
gatt.requestMtu(517)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -23,7 +23,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import java.util.UUID
|
||||
|
||||
val DEFAULT_RFCOMM_UUID = UUID.fromString("E6D55659-C8B4-4B85-96BB-B1143AF6D3AE")
|
||||
val DEFAULT_RFCOMM_UUID: UUID = UUID.fromString("E6D55659-C8B4-4B85-96BB-B1143AF6D3AE")
|
||||
const val DEFAULT_PEER_BLUETOOTH_ADDRESS = "AA:BB:CC:DD:EE:FF"
|
||||
const val DEFAULT_SENDER_PACKET_COUNT = 100
|
||||
const val DEFAULT_SENDER_PACKET_SIZE = 1024
|
||||
@@ -31,11 +31,11 @@ const val DEFAULT_SENDER_PACKET_SIZE = 1024
|
||||
class AppViewModel : ViewModel() {
|
||||
private var preferences: SharedPreferences? = null
|
||||
var peerBluetoothAddress by mutableStateOf(DEFAULT_PEER_BLUETOOTH_ADDRESS)
|
||||
var l2capPsm by mutableStateOf(0)
|
||||
var l2capPsm by mutableIntStateOf(0)
|
||||
var use2mPhy by mutableStateOf(true)
|
||||
var mtu by mutableStateOf(0)
|
||||
var rxPhy by mutableStateOf(0)
|
||||
var txPhy by mutableStateOf(0)
|
||||
var mtu by mutableIntStateOf(0)
|
||||
var rxPhy by mutableIntStateOf(0)
|
||||
var txPhy by mutableIntStateOf(0)
|
||||
var senderPacketCountSlider by mutableFloatStateOf(0.0F)
|
||||
var senderPacketSizeSlider by mutableFloatStateOf(0.0F)
|
||||
var senderPacketCount by mutableIntStateOf(DEFAULT_SENDER_PACKET_COUNT)
|
||||
@@ -79,18 +79,18 @@ class AppViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
fun updateSenderPacketCountSlider() {
|
||||
if (senderPacketCount <= 10) {
|
||||
senderPacketCountSlider = 0.0F
|
||||
senderPacketCountSlider = if (senderPacketCount <= 10) {
|
||||
0.0F
|
||||
} else if (senderPacketCount <= 50) {
|
||||
senderPacketCountSlider = 0.2F
|
||||
0.2F
|
||||
} else if (senderPacketCount <= 100) {
|
||||
senderPacketCountSlider = 0.4F
|
||||
0.4F
|
||||
} else if (senderPacketCount <= 500) {
|
||||
senderPacketCountSlider = 0.6F
|
||||
0.6F
|
||||
} else if (senderPacketCount <= 1000) {
|
||||
senderPacketCountSlider = 0.8F
|
||||
0.8F
|
||||
} else {
|
||||
senderPacketCountSlider = 1.0F
|
||||
1.0F
|
||||
}
|
||||
|
||||
with(preferences!!.edit()) {
|
||||
@@ -100,18 +100,18 @@ class AppViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
fun updateSenderPacketCount() {
|
||||
if (senderPacketCountSlider < 0.1F) {
|
||||
senderPacketCount = 10
|
||||
senderPacketCount = if (senderPacketCountSlider < 0.1F) {
|
||||
10
|
||||
} else if (senderPacketCountSlider < 0.3F) {
|
||||
senderPacketCount = 50
|
||||
50
|
||||
} else if (senderPacketCountSlider < 0.5F) {
|
||||
senderPacketCount = 100
|
||||
100
|
||||
} else if (senderPacketCountSlider < 0.7F) {
|
||||
senderPacketCount = 500
|
||||
500
|
||||
} else if (senderPacketCountSlider < 0.9F) {
|
||||
senderPacketCount = 1000
|
||||
1000
|
||||
} else {
|
||||
senderPacketCount = 10000
|
||||
10000
|
||||
}
|
||||
|
||||
with(preferences!!.edit()) {
|
||||
@@ -121,18 +121,18 @@ class AppViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
fun updateSenderPacketSizeSlider() {
|
||||
if (senderPacketSize <= 16) {
|
||||
senderPacketSizeSlider = 0.0F
|
||||
senderPacketSizeSlider = if (senderPacketSize <= 16) {
|
||||
0.0F
|
||||
} else if (senderPacketSize <= 256) {
|
||||
senderPacketSizeSlider = 0.02F
|
||||
0.02F
|
||||
} else if (senderPacketSize <= 512) {
|
||||
senderPacketSizeSlider = 0.4F
|
||||
0.4F
|
||||
} else if (senderPacketSize <= 1024) {
|
||||
senderPacketSizeSlider = 0.6F
|
||||
0.6F
|
||||
} else if (senderPacketSize <= 2048) {
|
||||
senderPacketSizeSlider = 0.8F
|
||||
0.8F
|
||||
} else {
|
||||
senderPacketSizeSlider = 1.0F
|
||||
1.0F
|
||||
}
|
||||
|
||||
with(preferences!!.edit()) {
|
||||
@@ -142,18 +142,18 @@ class AppViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
fun updateSenderPacketSize() {
|
||||
if (senderPacketSizeSlider < 0.1F) {
|
||||
senderPacketSize = 16
|
||||
senderPacketSize = if (senderPacketSizeSlider < 0.1F) {
|
||||
16
|
||||
} else if (senderPacketSizeSlider < 0.3F) {
|
||||
senderPacketSize = 256
|
||||
256
|
||||
} else if (senderPacketSizeSlider < 0.5F) {
|
||||
senderPacketSize = 512
|
||||
512
|
||||
} else if (senderPacketSizeSlider < 0.7F) {
|
||||
senderPacketSize = 1024
|
||||
1024
|
||||
} else if (senderPacketSizeSlider < 0.9F) {
|
||||
senderPacketSize = 2048
|
||||
2048
|
||||
} else {
|
||||
senderPacketSize = 4096
|
||||
4096
|
||||
}
|
||||
|
||||
with(preferences!!.edit()) {
|
||||
|
||||
@@ -42,6 +42,7 @@ public class HciServer {
|
||||
try (ServerSocket serverSocket = new ServerSocket(mPort)) {
|
||||
mListener.onMessage("Waiting for connection on port " + serverSocket.getLocalPort());
|
||||
try (Socket clientSocket = serverSocket.accept()) {
|
||||
clientSocket.setTcpNoDelay(true);
|
||||
mListener.onHostConnectionState(true);
|
||||
mListener.onMessage("Connected");
|
||||
HciParser parser = new HciParser(mListener);
|
||||
|
||||
@@ -29,7 +29,7 @@ from bumble.core import (
|
||||
ConnectionParameters,
|
||||
)
|
||||
from bumble.device import Connection, Device
|
||||
from bumble.host import Host
|
||||
from bumble.host import AclPacketQueue, Host
|
||||
from bumble.hci import (
|
||||
HCI_ACCEPT_CONNECTION_REQUEST_COMMAND,
|
||||
HCI_COMMAND_STATUS_PENDING,
|
||||
@@ -73,6 +73,13 @@ async def test_device_connect_parallel():
|
||||
d1 = Device(host=Host(None, None))
|
||||
d2 = Device(host=Host(None, None))
|
||||
|
||||
def _send(packet):
|
||||
pass
|
||||
|
||||
d0.host.acl_packet_queue = AclPacketQueue(0, 0, _send)
|
||||
d1.host.acl_packet_queue = AclPacketQueue(0, 0, _send)
|
||||
d2.host.acl_packet_queue = AclPacketQueue(0, 0, _send)
|
||||
|
||||
# enable classic
|
||||
d0.classic_enabled = True
|
||||
d1.classic_enabled = True
|
||||
|
||||
Reference in New Issue
Block a user