forked from auracaster/bumble_mirror
Merge pull request #608 from google/gbg/bench-android-enhancements
add startupDelay and connectionPriority params to BtBench snippets
This commit is contained in:
370
apps/bench.py
370
apps/bench.py
@@ -16,6 +16,7 @@
|
|||||||
# Imports
|
# Imports
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import dataclasses
|
||||||
import enum
|
import enum
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
@@ -97,34 +98,6 @@ DEFAULT_RFCOMM_MTU = 2048
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Utils
|
# Utils
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
def parse_packet(packet):
|
|
||||||
if len(packet) < 1:
|
|
||||||
logging.info(
|
|
||||||
color(f'!!! Packet too short (got {len(packet)} bytes, need >= 1)', 'red')
|
|
||||||
)
|
|
||||||
raise ValueError('packet too short')
|
|
||||||
|
|
||||||
try:
|
|
||||||
packet_type = PacketType(packet[0])
|
|
||||||
except ValueError:
|
|
||||||
logging.info(color(f'!!! Invalid packet type 0x{packet[0]:02X}', 'red'))
|
|
||||||
raise
|
|
||||||
|
|
||||||
return (packet_type, packet[1:])
|
|
||||||
|
|
||||||
|
|
||||||
def parse_packet_sequence(packet_data):
|
|
||||||
if len(packet_data) < 5:
|
|
||||||
logging.info(
|
|
||||||
color(
|
|
||||||
f'!!!Packet too short (got {len(packet_data)} bytes, need >= 5)',
|
|
||||||
'red',
|
|
||||||
)
|
|
||||||
)
|
|
||||||
raise ValueError('packet too short')
|
|
||||||
return struct.unpack_from('>bI', packet_data, 0)
|
|
||||||
|
|
||||||
|
|
||||||
def le_phy_name(phy_id):
|
def le_phy_name(phy_id):
|
||||||
return {HCI_LE_1M_PHY: '1M', HCI_LE_2M_PHY: '2M', HCI_LE_CODED_PHY: 'CODED'}.get(
|
return {HCI_LE_1M_PHY: '1M', HCI_LE_2M_PHY: '2M', HCI_LE_CODED_PHY: 'CODED'}.get(
|
||||||
phy_id, HCI_Constant.le_phy_name(phy_id)
|
phy_id, HCI_Constant.le_phy_name(phy_id)
|
||||||
@@ -225,13 +198,135 @@ async def switch_roles(connection, role):
|
|||||||
logging.info(f'{color("### Role switch failed:", "red")} {error}')
|
logging.info(f'{color("### Role switch failed:", "red")} {error}')
|
||||||
|
|
||||||
|
|
||||||
class PacketType(enum.IntEnum):
|
# -----------------------------------------------------------------------------
|
||||||
RESET = 0
|
# Packet
|
||||||
SEQUENCE = 1
|
# -----------------------------------------------------------------------------
|
||||||
ACK = 2
|
@dataclasses.dataclass
|
||||||
|
class Packet:
|
||||||
|
class PacketType(enum.IntEnum):
|
||||||
|
RESET = 0
|
||||||
|
SEQUENCE = 1
|
||||||
|
ACK = 2
|
||||||
|
|
||||||
|
class PacketFlags(enum.IntFlag):
|
||||||
|
LAST = 1
|
||||||
|
|
||||||
|
packet_type: PacketType
|
||||||
|
flags: PacketFlags = PacketFlags(0)
|
||||||
|
sequence: int = 0
|
||||||
|
timestamp: int = 0
|
||||||
|
payload: bytes = b""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_bytes(cls, data: bytes):
|
||||||
|
if len(data) < 1:
|
||||||
|
logging.warning(
|
||||||
|
color(f'!!! Packet too short (got {len(data)} bytes, need >= 1)', 'red')
|
||||||
|
)
|
||||||
|
raise ValueError('packet too short')
|
||||||
|
|
||||||
|
try:
|
||||||
|
packet_type = cls.PacketType(data[0])
|
||||||
|
except ValueError:
|
||||||
|
logging.warning(color(f'!!! Invalid packet type 0x{data[0]:02X}', 'red'))
|
||||||
|
raise
|
||||||
|
|
||||||
|
if packet_type == cls.PacketType.RESET:
|
||||||
|
return cls(packet_type)
|
||||||
|
|
||||||
|
flags = cls.PacketFlags(data[1])
|
||||||
|
(sequence,) = struct.unpack_from("<I", data, 2)
|
||||||
|
|
||||||
|
if packet_type == cls.PacketType.ACK:
|
||||||
|
if len(data) < 6:
|
||||||
|
logging.warning(
|
||||||
|
color(
|
||||||
|
f'!!! Packet too short (got {len(data)} bytes, need >= 6)',
|
||||||
|
'red',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return cls(packet_type, flags, sequence)
|
||||||
|
|
||||||
|
if len(data) < 10:
|
||||||
|
logging.warning(
|
||||||
|
color(
|
||||||
|
f'!!! Packet too short (got {len(data)} bytes, need >= 10)', 'red'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
raise ValueError('packet too short')
|
||||||
|
|
||||||
|
(timestamp,) = struct.unpack_from("<I", data, 6)
|
||||||
|
return cls(packet_type, flags, sequence, timestamp, data[10:])
|
||||||
|
|
||||||
|
def __bytes__(self):
|
||||||
|
if self.packet_type == self.PacketType.RESET:
|
||||||
|
return bytes([self.packet_type])
|
||||||
|
|
||||||
|
if self.packet_type == self.PacketType.ACK:
|
||||||
|
return struct.pack("<BBI", self.packet_type, self.flags, self.sequence)
|
||||||
|
|
||||||
|
return (
|
||||||
|
struct.pack(
|
||||||
|
"<BBII", self.packet_type, self.flags, self.sequence, self.timestamp
|
||||||
|
)
|
||||||
|
+ self.payload
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
PACKET_FLAG_LAST = 1
|
# -----------------------------------------------------------------------------
|
||||||
|
# Jitter Stats
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class JitterStats:
|
||||||
|
def __init__(self):
|
||||||
|
self.reset()
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self.packets = []
|
||||||
|
self.receive_times = []
|
||||||
|
self.jitter = []
|
||||||
|
|
||||||
|
def on_packet_received(self, packet):
|
||||||
|
now = time.time()
|
||||||
|
self.packets.append(packet)
|
||||||
|
self.receive_times.append(now)
|
||||||
|
|
||||||
|
if packet.timestamp and len(self.packets) > 1:
|
||||||
|
expected_time = (
|
||||||
|
self.receive_times[0]
|
||||||
|
+ (packet.timestamp - self.packets[0].timestamp) / 1000000
|
||||||
|
)
|
||||||
|
jitter = now - expected_time
|
||||||
|
else:
|
||||||
|
jitter = 0.0
|
||||||
|
|
||||||
|
self.jitter.append(jitter)
|
||||||
|
return jitter
|
||||||
|
|
||||||
|
def show_stats(self):
|
||||||
|
if len(self.jitter) < 3:
|
||||||
|
return
|
||||||
|
average = sum(self.jitter) / len(self.jitter)
|
||||||
|
adjusted = [jitter - average for jitter in self.jitter]
|
||||||
|
|
||||||
|
log_stats('Jitter (signed)', adjusted, 3)
|
||||||
|
log_stats('Jitter (absolute)', [abs(jitter) for jitter in adjusted], 3)
|
||||||
|
|
||||||
|
# Show a histogram
|
||||||
|
bin_count = 20
|
||||||
|
bins = [0] * bin_count
|
||||||
|
interval_min = min(adjusted)
|
||||||
|
interval_max = max(adjusted)
|
||||||
|
interval_range = interval_max - interval_min
|
||||||
|
bin_thresholds = [
|
||||||
|
interval_min + i * (interval_range / bin_count) for i in range(bin_count)
|
||||||
|
]
|
||||||
|
for jitter in adjusted:
|
||||||
|
for i in reversed(range(bin_count)):
|
||||||
|
if jitter >= bin_thresholds[i]:
|
||||||
|
bins[i] += 1
|
||||||
|
break
|
||||||
|
for i in range(bin_count):
|
||||||
|
logging.info(f'@@@ >= {bin_thresholds[i]:.4f}: {bins[i]}')
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -281,19 +376,37 @@ class Sender:
|
|||||||
await asyncio.sleep(self.tx_start_delay)
|
await asyncio.sleep(self.tx_start_delay)
|
||||||
|
|
||||||
logging.info(color('=== Sending RESET', 'magenta'))
|
logging.info(color('=== Sending RESET', 'magenta'))
|
||||||
await self.packet_io.send_packet(bytes([PacketType.RESET]))
|
await self.packet_io.send_packet(
|
||||||
|
bytes(Packet(packet_type=Packet.PacketType.RESET))
|
||||||
|
)
|
||||||
|
|
||||||
self.start_time = time.time()
|
self.start_time = time.time()
|
||||||
self.bytes_sent = 0
|
self.bytes_sent = 0
|
||||||
for tx_i in range(self.tx_packet_count):
|
for tx_i in range(self.tx_packet_count):
|
||||||
packet_flags = (
|
if self.pace > 0:
|
||||||
PACKET_FLAG_LAST if tx_i == self.tx_packet_count - 1 else 0
|
# Wait until it is time to send the next packet
|
||||||
|
target_time = self.start_time + (tx_i * self.pace / 1000)
|
||||||
|
now = time.time()
|
||||||
|
if now < target_time:
|
||||||
|
await asyncio.sleep(target_time - now)
|
||||||
|
else:
|
||||||
|
await self.packet_io.drain()
|
||||||
|
|
||||||
|
packet = bytes(
|
||||||
|
Packet(
|
||||||
|
packet_type=Packet.PacketType.SEQUENCE,
|
||||||
|
flags=(
|
||||||
|
Packet.PacketFlags.LAST
|
||||||
|
if tx_i == self.tx_packet_count - 1
|
||||||
|
else 0
|
||||||
|
),
|
||||||
|
sequence=tx_i,
|
||||||
|
timestamp=int((time.time() - self.start_time) * 1000000),
|
||||||
|
payload=bytes(
|
||||||
|
self.tx_packet_size - 10 - self.packet_io.overhead_size
|
||||||
|
),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
packet = struct.pack(
|
|
||||||
'>bbI',
|
|
||||||
PacketType.SEQUENCE,
|
|
||||||
packet_flags,
|
|
||||||
tx_i,
|
|
||||||
) + bytes(self.tx_packet_size - 6 - self.packet_io.overhead_size)
|
|
||||||
logging.info(
|
logging.info(
|
||||||
color(
|
color(
|
||||||
f'Sending packet {tx_i}: {self.tx_packet_size} bytes', 'yellow'
|
f'Sending packet {tx_i}: {self.tx_packet_size} bytes', 'yellow'
|
||||||
@@ -302,14 +415,6 @@ class Sender:
|
|||||||
self.bytes_sent += len(packet)
|
self.bytes_sent += len(packet)
|
||||||
await self.packet_io.send_packet(packet)
|
await self.packet_io.send_packet(packet)
|
||||||
|
|
||||||
if self.pace is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if self.pace > 0:
|
|
||||||
await asyncio.sleep(self.pace / 1000)
|
|
||||||
else:
|
|
||||||
await self.packet_io.drain()
|
|
||||||
|
|
||||||
await self.done.wait()
|
await self.done.wait()
|
||||||
|
|
||||||
run_counter = f'[{run + 1} of {self.repeat + 1}]' if self.repeat else ''
|
run_counter = f'[{run + 1} of {self.repeat + 1}]' if self.repeat else ''
|
||||||
@@ -321,13 +426,13 @@ class Sender:
|
|||||||
if self.repeat:
|
if self.repeat:
|
||||||
logging.info(color('--- End of runs', 'blue'))
|
logging.info(color('--- End of runs', 'blue'))
|
||||||
|
|
||||||
def on_packet_received(self, packet):
|
def on_packet_received(self, data):
|
||||||
try:
|
try:
|
||||||
packet_type, _ = parse_packet(packet)
|
packet = Packet.from_bytes(data)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return
|
return
|
||||||
|
|
||||||
if packet_type == PacketType.ACK:
|
if packet.packet_type == Packet.PacketType.ACK:
|
||||||
elapsed = time.time() - self.start_time
|
elapsed = time.time() - self.start_time
|
||||||
average_tx_speed = self.bytes_sent / elapsed
|
average_tx_speed = self.bytes_sent / elapsed
|
||||||
self.stats.append(average_tx_speed)
|
self.stats.append(average_tx_speed)
|
||||||
@@ -350,52 +455,53 @@ class Receiver:
|
|||||||
last_timestamp: float
|
last_timestamp: float
|
||||||
|
|
||||||
def __init__(self, packet_io, linger):
|
def __init__(self, packet_io, linger):
|
||||||
self.reset()
|
self.jitter_stats = JitterStats()
|
||||||
self.packet_io = packet_io
|
self.packet_io = packet_io
|
||||||
self.packet_io.packet_listener = self
|
self.packet_io.packet_listener = self
|
||||||
self.linger = linger
|
self.linger = linger
|
||||||
self.done = asyncio.Event()
|
self.done = asyncio.Event()
|
||||||
|
self.reset()
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
self.expected_packet_index = 0
|
self.expected_packet_index = 0
|
||||||
self.measurements = [(time.time(), 0)]
|
self.measurements = [(time.time(), 0)]
|
||||||
self.total_bytes_received = 0
|
self.total_bytes_received = 0
|
||||||
|
self.jitter_stats.reset()
|
||||||
|
|
||||||
def on_packet_received(self, packet):
|
def on_packet_received(self, data):
|
||||||
try:
|
try:
|
||||||
packet_type, packet_data = parse_packet(packet)
|
packet = Packet.from_bytes(data)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
logging.exception("invalid packet")
|
||||||
return
|
return
|
||||||
|
|
||||||
if packet_type == PacketType.RESET:
|
if packet.packet_type == Packet.PacketType.RESET:
|
||||||
logging.info(color('=== Received RESET', 'magenta'))
|
logging.info(color('=== Received RESET', 'magenta'))
|
||||||
self.reset()
|
self.reset()
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
jitter = self.jitter_stats.on_packet_received(packet)
|
||||||
packet_flags, packet_index = parse_packet_sequence(packet_data)
|
|
||||||
except ValueError:
|
|
||||||
return
|
|
||||||
logging.info(
|
logging.info(
|
||||||
f'<<< Received packet {packet_index}: '
|
f'<<< Received packet {packet.sequence}: '
|
||||||
f'flags=0x{packet_flags:02X}, '
|
f'flags={packet.flags}, '
|
||||||
f'{len(packet) + self.packet_io.overhead_size} bytes'
|
f'jitter={jitter:.4f}, '
|
||||||
|
f'{len(data) + self.packet_io.overhead_size} bytes',
|
||||||
)
|
)
|
||||||
|
|
||||||
if packet_index != self.expected_packet_index:
|
if packet.sequence != self.expected_packet_index:
|
||||||
logging.info(
|
logging.info(
|
||||||
color(
|
color(
|
||||||
f'!!! Unexpected packet, expected {self.expected_packet_index} '
|
f'!!! Unexpected packet, expected {self.expected_packet_index} '
|
||||||
f'but received {packet_index}'
|
f'but received {packet.sequence}'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
now = time.time()
|
now = time.time()
|
||||||
elapsed_since_start = now - self.measurements[0][0]
|
elapsed_since_start = now - self.measurements[0][0]
|
||||||
elapsed_since_last = now - self.measurements[-1][0]
|
elapsed_since_last = now - self.measurements[-1][0]
|
||||||
self.measurements.append((now, len(packet)))
|
self.measurements.append((now, len(data)))
|
||||||
self.total_bytes_received += len(packet)
|
self.total_bytes_received += len(data)
|
||||||
instant_rx_speed = len(packet) / elapsed_since_last
|
instant_rx_speed = len(data) / elapsed_since_last
|
||||||
average_rx_speed = self.total_bytes_received / elapsed_since_start
|
average_rx_speed = self.total_bytes_received / elapsed_since_start
|
||||||
window = self.measurements[-64:]
|
window = self.measurements[-64:]
|
||||||
windowed_rx_speed = sum(measurement[1] for measurement in window[1:]) / (
|
windowed_rx_speed = sum(measurement[1] for measurement in window[1:]) / (
|
||||||
@@ -411,15 +517,17 @@ class Receiver:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
self.expected_packet_index = packet_index + 1
|
self.expected_packet_index = packet.sequence + 1
|
||||||
|
|
||||||
if packet_flags & PACKET_FLAG_LAST:
|
if packet.flags & Packet.PacketFlags.LAST:
|
||||||
AsyncRunner.spawn(
|
AsyncRunner.spawn(
|
||||||
self.packet_io.send_packet(
|
self.packet_io.send_packet(
|
||||||
struct.pack('>bbI', PacketType.ACK, packet_flags, packet_index)
|
bytes(Packet(Packet.PacketType.ACK, packet.flags, packet.sequence))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
logging.info(color('@@@ Received last packet', 'green'))
|
logging.info(color('@@@ Received last packet', 'green'))
|
||||||
|
self.jitter_stats.show_stats()
|
||||||
|
|
||||||
if not self.linger:
|
if not self.linger:
|
||||||
self.done.set()
|
self.done.set()
|
||||||
|
|
||||||
@@ -479,25 +587,32 @@ class Ping:
|
|||||||
await asyncio.sleep(self.tx_start_delay)
|
await asyncio.sleep(self.tx_start_delay)
|
||||||
|
|
||||||
logging.info(color('=== Sending RESET', 'magenta'))
|
logging.info(color('=== Sending RESET', 'magenta'))
|
||||||
await self.packet_io.send_packet(bytes([PacketType.RESET]))
|
await self.packet_io.send_packet(bytes(Packet(Packet.PacketType.RESET)))
|
||||||
|
|
||||||
packet_interval = self.pace / 1000
|
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
self.next_expected_packet_index = 0
|
self.next_expected_packet_index = 0
|
||||||
for i in range(self.tx_packet_count):
|
for i in range(self.tx_packet_count):
|
||||||
target_time = start_time + (i * packet_interval)
|
target_time = start_time + (i * self.pace / 1000)
|
||||||
now = time.time()
|
now = time.time()
|
||||||
if now < target_time:
|
if now < target_time:
|
||||||
await asyncio.sleep(target_time - now)
|
await asyncio.sleep(target_time - now)
|
||||||
|
now = time.time()
|
||||||
|
|
||||||
packet = struct.pack(
|
packet = bytes(
|
||||||
'>bbI',
|
Packet(
|
||||||
PacketType.SEQUENCE,
|
packet_type=Packet.PacketType.SEQUENCE,
|
||||||
(PACKET_FLAG_LAST if i == self.tx_packet_count - 1 else 0),
|
flags=(
|
||||||
i,
|
Packet.PacketFlags.LAST
|
||||||
) + bytes(self.tx_packet_size - 6)
|
if i == self.tx_packet_count - 1
|
||||||
|
else 0
|
||||||
|
),
|
||||||
|
sequence=i,
|
||||||
|
timestamp=int((now - start_time) * 1000000),
|
||||||
|
payload=bytes(self.tx_packet_size - 10),
|
||||||
|
)
|
||||||
|
)
|
||||||
logging.info(color(f'Sending packet {i}', 'yellow'))
|
logging.info(color(f'Sending packet {i}', 'yellow'))
|
||||||
self.ping_times.append(time.time())
|
self.ping_times.append(now)
|
||||||
await self.packet_io.send_packet(packet)
|
await self.packet_io.send_packet(packet)
|
||||||
|
|
||||||
await self.done.wait()
|
await self.done.wait()
|
||||||
@@ -531,40 +646,35 @@ class Ping:
|
|||||||
if self.repeat:
|
if self.repeat:
|
||||||
logging.info(color('--- End of runs', 'blue'))
|
logging.info(color('--- End of runs', 'blue'))
|
||||||
|
|
||||||
def on_packet_received(self, packet):
|
def on_packet_received(self, data):
|
||||||
try:
|
try:
|
||||||
packet_type, packet_data = parse_packet(packet)
|
packet = Packet.from_bytes(data)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
if packet.packet_type == Packet.PacketType.ACK:
|
||||||
packet_flags, packet_index = parse_packet_sequence(packet_data)
|
elapsed = time.time() - self.ping_times[packet.sequence]
|
||||||
except ValueError:
|
|
||||||
return
|
|
||||||
|
|
||||||
if packet_type == PacketType.ACK:
|
|
||||||
elapsed = time.time() - self.ping_times[packet_index]
|
|
||||||
rtt = elapsed * 1000
|
rtt = elapsed * 1000
|
||||||
self.rtts.append(rtt)
|
self.rtts.append(rtt)
|
||||||
logging.info(
|
logging.info(
|
||||||
color(
|
color(
|
||||||
f'<<< Received ACK [{packet_index}], RTT={rtt:.2f}ms',
|
f'<<< Received ACK [{packet.sequence}], RTT={rtt:.2f}ms',
|
||||||
'green',
|
'green',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if packet_index == self.next_expected_packet_index:
|
if packet.sequence == self.next_expected_packet_index:
|
||||||
self.next_expected_packet_index += 1
|
self.next_expected_packet_index += 1
|
||||||
else:
|
else:
|
||||||
logging.info(
|
logging.info(
|
||||||
color(
|
color(
|
||||||
f'!!! Unexpected packet, '
|
f'!!! Unexpected packet, '
|
||||||
f'expected {self.next_expected_packet_index} '
|
f'expected {self.next_expected_packet_index} '
|
||||||
f'but received {packet_index}'
|
f'but received {packet.sequence}'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if packet_flags & PACKET_FLAG_LAST:
|
if packet.flags & Packet.PacketFlags.LAST:
|
||||||
self.done.set()
|
self.done.set()
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -576,89 +686,56 @@ class Pong:
|
|||||||
expected_packet_index: int
|
expected_packet_index: int
|
||||||
|
|
||||||
def __init__(self, packet_io, linger):
|
def __init__(self, packet_io, linger):
|
||||||
self.reset()
|
self.jitter_stats = JitterStats()
|
||||||
self.packet_io = packet_io
|
self.packet_io = packet_io
|
||||||
self.packet_io.packet_listener = self
|
self.packet_io.packet_listener = self
|
||||||
self.linger = linger
|
self.linger = linger
|
||||||
self.done = asyncio.Event()
|
self.done = asyncio.Event()
|
||||||
|
self.reset()
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
self.expected_packet_index = 0
|
self.expected_packet_index = 0
|
||||||
self.receive_times = []
|
self.jitter_stats.reset()
|
||||||
|
|
||||||
def on_packet_received(self, packet):
|
|
||||||
self.receive_times.append(time.time())
|
|
||||||
|
|
||||||
|
def on_packet_received(self, data):
|
||||||
try:
|
try:
|
||||||
packet_type, packet_data = parse_packet(packet)
|
packet = Packet.from_bytes(data)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return
|
return
|
||||||
|
|
||||||
if packet_type == PacketType.RESET:
|
if packet.packet_type == Packet.PacketType.RESET:
|
||||||
logging.info(color('=== Received RESET', 'magenta'))
|
logging.info(color('=== Received RESET', 'magenta'))
|
||||||
self.reset()
|
self.reset()
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
jitter = self.jitter_stats.on_packet_received(packet)
|
||||||
packet_flags, packet_index = parse_packet_sequence(packet_data)
|
|
||||||
except ValueError:
|
|
||||||
return
|
|
||||||
interval = (
|
|
||||||
self.receive_times[-1] - self.receive_times[-2]
|
|
||||||
if len(self.receive_times) >= 2
|
|
||||||
else 0
|
|
||||||
)
|
|
||||||
logging.info(
|
logging.info(
|
||||||
color(
|
color(
|
||||||
f'<<< Received packet {packet_index}: '
|
f'<<< Received packet {packet.sequence}: '
|
||||||
f'flags=0x{packet_flags:02X}, {len(packet)} bytes, '
|
f'flags={packet.flags}, {len(data)} bytes, '
|
||||||
f'interval={interval:.4f}',
|
f'jitter={jitter:.4f}',
|
||||||
'green',
|
'green',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if packet_index != self.expected_packet_index:
|
if packet.sequence != self.expected_packet_index:
|
||||||
logging.info(
|
logging.info(
|
||||||
color(
|
color(
|
||||||
f'!!! Unexpected packet, expected {self.expected_packet_index} '
|
f'!!! Unexpected packet, expected {self.expected_packet_index} '
|
||||||
f'but received {packet_index}'
|
f'but received {packet.sequence}'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
self.expected_packet_index = packet_index + 1
|
self.expected_packet_index = packet.sequence + 1
|
||||||
|
|
||||||
AsyncRunner.spawn(
|
AsyncRunner.spawn(
|
||||||
self.packet_io.send_packet(
|
self.packet_io.send_packet(
|
||||||
struct.pack('>bbI', PacketType.ACK, packet_flags, packet_index)
|
bytes(Packet(Packet.PacketType.ACK, packet.flags, packet.sequence))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if packet_flags & PACKET_FLAG_LAST:
|
if packet.flags & Packet.PacketFlags.LAST:
|
||||||
if len(self.receive_times) >= 3:
|
self.jitter_stats.show_stats()
|
||||||
# Show basic stats
|
|
||||||
intervals = [
|
|
||||||
self.receive_times[i + 1] - self.receive_times[i]
|
|
||||||
for i in range(len(self.receive_times) - 1)
|
|
||||||
]
|
|
||||||
log_stats('Packet intervals', intervals, 3)
|
|
||||||
|
|
||||||
# Show a histogram
|
|
||||||
bin_count = 20
|
|
||||||
bins = [0] * bin_count
|
|
||||||
interval_min = min(intervals)
|
|
||||||
interval_max = max(intervals)
|
|
||||||
interval_range = interval_max - interval_min
|
|
||||||
bin_thresholds = [
|
|
||||||
interval_min + i * (interval_range / bin_count)
|
|
||||||
for i in range(bin_count)
|
|
||||||
]
|
|
||||||
for interval in intervals:
|
|
||||||
for i in reversed(range(bin_count)):
|
|
||||||
if interval >= bin_thresholds[i]:
|
|
||||||
bins[i] += 1
|
|
||||||
break
|
|
||||||
for i in range(bin_count):
|
|
||||||
logging.info(f'@@@ >= {bin_thresholds[i]:.4f}: {bins[i]}')
|
|
||||||
|
|
||||||
if not self.linger:
|
if not self.linger:
|
||||||
self.done.set()
|
self.done.set()
|
||||||
@@ -1471,7 +1548,7 @@ def create_mode_factory(ctx, default_mode):
|
|||||||
def create_scenario_factory(ctx, default_scenario):
|
def create_scenario_factory(ctx, default_scenario):
|
||||||
scenario = ctx.obj['scenario']
|
scenario = ctx.obj['scenario']
|
||||||
if scenario is None:
|
if scenario is None:
|
||||||
scenarion = default_scenario
|
scenario = default_scenario
|
||||||
|
|
||||||
def create_scenario(packet_io):
|
def create_scenario(packet_io):
|
||||||
if scenario == 'send':
|
if scenario == 'send':
|
||||||
@@ -1530,6 +1607,7 @@ def create_scenario_factory(ctx, default_scenario):
|
|||||||
'--att-mtu',
|
'--att-mtu',
|
||||||
metavar='MTU',
|
metavar='MTU',
|
||||||
type=click.IntRange(23, 517),
|
type=click.IntRange(23, 517),
|
||||||
|
default=517,
|
||||||
help='GATT MTU (gatt-client mode)',
|
help='GATT MTU (gatt-client mode)',
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
@@ -1605,7 +1683,7 @@ def create_scenario_factory(ctx, default_scenario):
|
|||||||
'--packet-size',
|
'--packet-size',
|
||||||
'-s',
|
'-s',
|
||||||
metavar='SIZE',
|
metavar='SIZE',
|
||||||
type=click.IntRange(8, 8192),
|
type=click.IntRange(10, 8192),
|
||||||
default=500,
|
default=500,
|
||||||
help='Packet size (send or ping scenario)',
|
help='Packet size (send or ping scenario)',
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2475,7 +2475,7 @@ class Device(CompositeEventEmitter):
|
|||||||
if self.random_address != hci.Address.ANY_RANDOM:
|
if self.random_address != hci.Address.ANY_RANDOM:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
color(
|
color(
|
||||||
f'LE Random hci.Address: {self.random_address}',
|
f'LE Random Address: {self.random_address}',
|
||||||
'yellow',
|
'yellow',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class OneDeviceBenchTest(base_test.BaseTestClass):
|
|||||||
|
|
||||||
def test_l2cap_client_ping(self):
|
def test_l2cap_client_ping(self):
|
||||||
runner = self.dut.bench.runL2capClient(
|
runner = self.dut.bench.runL2capClient(
|
||||||
"ping", "4B:2A:67:76:2B:E3", 128, True, 100, 970, 100
|
"ping", "4B:2A:67:76:2B:E3", 128, True, 100, 970, 100, "HIGH"
|
||||||
)
|
)
|
||||||
print("### Initial status:", runner)
|
print("### Initial status:", runner)
|
||||||
final_status = self.dut.bench.waitForRunnerCompletion(runner["id"])
|
final_status = self.dut.bench.waitForRunnerCompletion(runner["id"])
|
||||||
@@ -36,12 +36,34 @@ class OneDeviceBenchTest(base_test.BaseTestClass):
|
|||||||
|
|
||||||
def test_l2cap_client_send(self):
|
def test_l2cap_client_send(self):
|
||||||
runner = self.dut.bench.runL2capClient(
|
runner = self.dut.bench.runL2capClient(
|
||||||
"send", "7E:90:D0:F2:7A:11", 131, True, 100, 970, 0
|
"send",
|
||||||
|
"F1:F1:F1:F1:F1:F1",
|
||||||
|
128,
|
||||||
|
True,
|
||||||
|
100,
|
||||||
|
970,
|
||||||
|
0,
|
||||||
|
"HIGH",
|
||||||
|
10000,
|
||||||
)
|
)
|
||||||
print("### Initial status:", runner)
|
print("### Initial status:", runner)
|
||||||
final_status = self.dut.bench.waitForRunnerCompletion(runner["id"])
|
final_status = self.dut.bench.waitForRunnerCompletion(runner["id"])
|
||||||
print("### Final status:", final_status)
|
print("### Final status:", final_status)
|
||||||
|
|
||||||
|
def test_gatt_client_send(self):
|
||||||
|
runner = self.dut.bench.runGattClient(
|
||||||
|
"send", "F1:F1:F1:F1:F1:F1", 128, True, 100, 970, 100, "HIGH"
|
||||||
|
)
|
||||||
|
print("### Initial status:", runner)
|
||||||
|
final_status = self.dut.bench.waitForRunnerCompletion(runner["id"])
|
||||||
|
print("### Final status:", final_status)
|
||||||
|
|
||||||
|
def test_gatt_server_receive(self):
|
||||||
|
runner = self.dut.bench.runGattServer("receive")
|
||||||
|
print("### Initial status:", runner)
|
||||||
|
final_status = self.dut.bench.waitForRunnerCompletion(runner["id"])
|
||||||
|
print("### Final status:", final_status)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
test_runner.main()
|
test_runner.main()
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ TestBeds:
|
|||||||
- Name: BenchTestBed
|
- Name: BenchTestBed
|
||||||
Controllers:
|
Controllers:
|
||||||
AndroidDevice:
|
AndroidDevice:
|
||||||
- serial: 37211FDJG000DJ
|
- serial: emulator-5554
|
||||||
local_bt_address: 94:45:60:5E:03:B0
|
local_bt_address: 94:45:60:5E:03:B0
|
||||||
|
|
||||||
- serial: 23071FDEE001F7
|
#- serial: 23071FDEE001F7
|
||||||
local_bt_address: DC:E5:5B:E5:51:2C
|
# local_bt_address: DC:E5:5B:E5:51:2C
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.github.google.bumble.btbench"
|
applicationId = "com.github.google.bumble.btbench"
|
||||||
minSdk = 30
|
minSdk = 33
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 1
|
versionCode = 1
|
||||||
versionName = "1.0"
|
versionName = "1.0"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="com.github.google.bumble.btbench">
|
package="com.github.google.bumble.btbench">
|
||||||
<uses-sdk android:minSdkVersion="30" android:targetSdkVersion="34" />
|
<uses-sdk android:minSdkVersion="33" android:targetSdkVersion="34" />
|
||||||
<!-- Request legacy Bluetooth permissions on older devices. -->
|
<!-- Request legacy Bluetooth permissions on older devices. -->
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
|
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
|
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.github.google.bumble.btbench
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.bluetooth.BluetoothAdapter
|
||||||
|
import android.bluetooth.le.AdvertiseCallback
|
||||||
|
import android.bluetooth.le.AdvertiseData
|
||||||
|
import android.bluetooth.le.AdvertiseSettings
|
||||||
|
import android.bluetooth.le.AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY
|
||||||
|
import android.os.Build
|
||||||
|
import java.util.logging.Logger
|
||||||
|
|
||||||
|
private val Log = Logger.getLogger("btbench.advertiser")
|
||||||
|
|
||||||
|
class Advertiser(private val bluetoothAdapter: BluetoothAdapter) : AdvertiseCallback() {
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
fun start() {
|
||||||
|
val advertiseSettingsBuilder = AdvertiseSettings.Builder()
|
||||||
|
.setAdvertiseMode(ADVERTISE_MODE_LOW_LATENCY)
|
||||||
|
.setConnectable(true)
|
||||||
|
advertiseSettingsBuilder.setDiscoverable(true)
|
||||||
|
val advertiseSettings = advertiseSettingsBuilder.build()
|
||||||
|
val advertiseData = AdvertiseData.Builder().build()
|
||||||
|
val scanData = AdvertiseData.Builder().setIncludeDeviceName(true).build()
|
||||||
|
bluetoothAdapter.bluetoothLeAdvertiser.startAdvertising(advertiseSettings, advertiseData, scanData, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
fun stop() {
|
||||||
|
bluetoothAdapter.bluetoothLeAdvertiser.stopAdvertising(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartFailure(errorCode: Int) {
|
||||||
|
Log.warning("failed to start advertising: $errorCode")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartSuccess(settingsInEffect: AdvertiseSettings) {
|
||||||
|
Log.info("advertising started: $settingsInEffect")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,11 +22,13 @@ import androidx.test.core.app.ApplicationProvider;
|
|||||||
|
|
||||||
import com.google.android.mobly.snippet.Snippet;
|
import com.google.android.mobly.snippet.Snippet;
|
||||||
import com.google.android.mobly.snippet.rpc.Rpc;
|
import com.google.android.mobly.snippet.rpc.Rpc;
|
||||||
|
import com.google.android.mobly.snippet.rpc.RpcOptional;
|
||||||
|
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.security.InvalidParameterException;
|
import java.security.InvalidParameterException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -71,12 +73,15 @@ public class AutomationSnippet implements Snippet {
|
|||||||
private final Context mContext;
|
private final Context mContext;
|
||||||
private final ArrayList<Runner> mRunners = new ArrayList<>();
|
private final ArrayList<Runner> mRunners = new ArrayList<>();
|
||||||
|
|
||||||
public AutomationSnippet() {
|
public AutomationSnippet() throws IOException {
|
||||||
mContext = ApplicationProvider.getApplicationContext();
|
mContext = ApplicationProvider.getApplicationContext();
|
||||||
BluetoothManager bluetoothManager = mContext.getSystemService(BluetoothManager.class);
|
BluetoothManager bluetoothManager = mContext.getSystemService(BluetoothManager.class);
|
||||||
mBluetoothAdapter = bluetoothManager.getAdapter();
|
mBluetoothAdapter = bluetoothManager.getAdapter();
|
||||||
if (mBluetoothAdapter == null) {
|
if (mBluetoothAdapter == null) {
|
||||||
throw new RuntimeException("bluetooth not supported");
|
throw new IOException("bluetooth not supported");
|
||||||
|
}
|
||||||
|
if (!mBluetoothAdapter.isEnabled()) {
|
||||||
|
throw new IOException("bluetooth not enabled");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,32 +90,46 @@ public class AutomationSnippet implements Snippet {
|
|||||||
switch (mode) {
|
switch (mode) {
|
||||||
case "rfcomm-client":
|
case "rfcomm-client":
|
||||||
runnable = new RfcommClient(model, mBluetoothAdapter,
|
runnable = new RfcommClient(model, mBluetoothAdapter,
|
||||||
(PacketIO packetIO) -> createIoClient(model, scenario,
|
(PacketIO packetIO) -> createIoClient(model, scenario,
|
||||||
packetIO));
|
packetIO));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "rfcomm-server":
|
case "rfcomm-server":
|
||||||
runnable = new RfcommServer(model, mBluetoothAdapter,
|
runnable = new RfcommServer(model, mBluetoothAdapter,
|
||||||
(PacketIO packetIO) -> createIoClient(model, scenario,
|
(PacketIO packetIO) -> createIoClient(model, scenario,
|
||||||
packetIO));
|
packetIO));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "l2cap-client":
|
case "l2cap-client":
|
||||||
runnable = new L2capClient(model, mBluetoothAdapter, mContext,
|
runnable = new L2capClient(model, mBluetoothAdapter, mContext,
|
||||||
(PacketIO packetIO) -> createIoClient(model, scenario,
|
(PacketIO packetIO) -> createIoClient(model, scenario,
|
||||||
packetIO));
|
packetIO));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "l2cap-server":
|
case "l2cap-server":
|
||||||
runnable = new L2capServer(model, mBluetoothAdapter,
|
runnable = new L2capServer(model, mBluetoothAdapter,
|
||||||
(PacketIO packetIO) -> createIoClient(model, scenario,
|
(PacketIO packetIO) -> createIoClient(model, scenario,
|
||||||
packetIO));
|
packetIO));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "gatt-client":
|
||||||
|
runnable = new GattClient(model, mBluetoothAdapter, mContext,
|
||||||
|
(PacketIO packetIO) -> createIoClient(model, scenario,
|
||||||
|
packetIO));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "gatt-server":
|
||||||
|
runnable = new GattServer(model, mBluetoothAdapter, mContext,
|
||||||
|
(PacketIO packetIO) -> createIoClient(model, scenario,
|
||||||
|
packetIO));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model.setMode(mode);
|
||||||
|
model.setScenario(scenario);
|
||||||
runnable.run();
|
runnable.run();
|
||||||
Runner runner = new Runner(runnable, mode, scenario, model);
|
Runner runner = new Runner(runnable, mode, scenario, model);
|
||||||
mRunners.add(runner);
|
mRunners.add(runner);
|
||||||
@@ -140,7 +159,21 @@ public class AutomationSnippet implements Snippet {
|
|||||||
JSONObject result = new JSONObject();
|
JSONObject result = new JSONObject();
|
||||||
result.put("status", model.getStatus());
|
result.put("status", model.getStatus());
|
||||||
result.put("running", model.getRunning());
|
result.put("running", model.getRunning());
|
||||||
|
result.put("peer_bluetooth_address", model.getPeerBluetoothAddress());
|
||||||
|
result.put("mode", model.getMode());
|
||||||
|
result.put("scenario", model.getScenario());
|
||||||
|
result.put("sender_packet_size", model.getSenderPacketSize());
|
||||||
|
result.put("sender_packet_count", model.getSenderPacketCount());
|
||||||
|
result.put("sender_packet_interval", model.getSenderPacketInterval());
|
||||||
|
result.put("packets_sent", model.getPacketsSent());
|
||||||
|
result.put("packets_received", model.getPacketsReceived());
|
||||||
result.put("l2cap_psm", model.getL2capPsm());
|
result.put("l2cap_psm", model.getL2capPsm());
|
||||||
|
result.put("use_2m_phy", model.getUse2mPhy());
|
||||||
|
result.put("connection_priority", model.getConnectionPriority());
|
||||||
|
result.put("mtu", model.getMtu());
|
||||||
|
result.put("rx_phy", model.getRxPhy());
|
||||||
|
result.put("tx_phy", model.getTxPhy());
|
||||||
|
result.put("startup_delay", model.getStartupDelay());
|
||||||
if (model.getStatus().equals("OK")) {
|
if (model.getStatus().equals("OK")) {
|
||||||
JSONObject stats = new JSONObject();
|
JSONObject stats = new JSONObject();
|
||||||
result.put("stats", stats);
|
result.put("stats", stats);
|
||||||
@@ -167,12 +200,12 @@ public class AutomationSnippet implements Snippet {
|
|||||||
|
|
||||||
@Rpc(description = "Run a scenario in RFComm Client mode")
|
@Rpc(description = "Run a scenario in RFComm Client mode")
|
||||||
public JSONObject runRfcommClient(String scenario, String peerBluetoothAddress, int packetCount,
|
public JSONObject runRfcommClient(String scenario, String peerBluetoothAddress, int packetCount,
|
||||||
int packetSize, int packetInterval) throws JSONException {
|
int packetSize, int packetInterval,
|
||||||
assert (mBluetoothAdapter != null);
|
@RpcOptional Integer startupDelay) throws JSONException {
|
||||||
|
|
||||||
// We only support "send" and "ping" for this mode for now
|
// We only support "send" and "ping" for this mode for now
|
||||||
if (!(scenario.equals("send") || scenario.equals("ping"))) {
|
if (!(scenario.equals("send") || scenario.equals("ping"))) {
|
||||||
throw new InvalidParameterException("only 'send' and 'ping' are supported for this mode");
|
throw new InvalidParameterException(
|
||||||
|
"only 'send' and 'ping' are supported for this mode");
|
||||||
}
|
}
|
||||||
|
|
||||||
AppViewModel model = new AppViewModel();
|
AppViewModel model = new AppViewModel();
|
||||||
@@ -180,6 +213,9 @@ public class AutomationSnippet implements Snippet {
|
|||||||
model.setSenderPacketCount(packetCount);
|
model.setSenderPacketCount(packetCount);
|
||||||
model.setSenderPacketSize(packetSize);
|
model.setSenderPacketSize(packetSize);
|
||||||
model.setSenderPacketInterval(packetInterval);
|
model.setSenderPacketInterval(packetInterval);
|
||||||
|
if (startupDelay != null) {
|
||||||
|
model.setStartupDelay(startupDelay);
|
||||||
|
}
|
||||||
|
|
||||||
Runner runner = runScenario(model, "rfcomm-client", scenario);
|
Runner runner = runScenario(model, "rfcomm-client", scenario);
|
||||||
assert runner != null;
|
assert runner != null;
|
||||||
@@ -187,15 +223,18 @@ public class AutomationSnippet implements Snippet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Rpc(description = "Run a scenario in RFComm Server mode")
|
@Rpc(description = "Run a scenario in RFComm Server mode")
|
||||||
public JSONObject runRfcommServer(String scenario) throws JSONException {
|
public JSONObject runRfcommServer(String scenario,
|
||||||
assert (mBluetoothAdapter != null);
|
@RpcOptional Integer startupDelay) throws JSONException {
|
||||||
|
|
||||||
// We only support "receive" and "pong" for this mode for now
|
// We only support "receive" and "pong" for this mode for now
|
||||||
if (!(scenario.equals("receive") || scenario.equals("pong"))) {
|
if (!(scenario.equals("receive") || scenario.equals("pong"))) {
|
||||||
throw new InvalidParameterException("only 'receive' and 'pong' are supported for this mode");
|
throw new InvalidParameterException(
|
||||||
|
"only 'receive' and 'pong' are supported for this mode");
|
||||||
}
|
}
|
||||||
|
|
||||||
AppViewModel model = new AppViewModel();
|
AppViewModel model = new AppViewModel();
|
||||||
|
if (startupDelay != null) {
|
||||||
|
model.setStartupDelay(startupDelay);
|
||||||
|
}
|
||||||
|
|
||||||
Runner runner = runScenario(model, "rfcomm-server", scenario);
|
Runner runner = runScenario(model, "rfcomm-server", scenario);
|
||||||
assert runner != null;
|
assert runner != null;
|
||||||
@@ -205,12 +244,12 @@ public class AutomationSnippet implements Snippet {
|
|||||||
@Rpc(description = "Run a scenario in L2CAP Client mode")
|
@Rpc(description = "Run a scenario in L2CAP Client mode")
|
||||||
public JSONObject runL2capClient(String scenario, String peerBluetoothAddress, int psm,
|
public JSONObject runL2capClient(String scenario, String peerBluetoothAddress, int psm,
|
||||||
boolean use_2m_phy, int packetCount, int packetSize,
|
boolean use_2m_phy, int packetCount, int packetSize,
|
||||||
int packetInterval) throws JSONException {
|
int packetInterval, @RpcOptional String connectionPriority,
|
||||||
assert (mBluetoothAdapter != null);
|
@RpcOptional Integer startupDelay) throws JSONException {
|
||||||
|
|
||||||
// We only support "send" and "ping" for this mode for now
|
// We only support "send" and "ping" for this mode for now
|
||||||
if (!(scenario.equals("send") || scenario.equals("ping"))) {
|
if (!(scenario.equals("send") || scenario.equals("ping"))) {
|
||||||
throw new InvalidParameterException("only 'send' and 'ping' are supported for this mode");
|
throw new InvalidParameterException(
|
||||||
|
"only 'send' and 'ping' are supported for this mode");
|
||||||
}
|
}
|
||||||
|
|
||||||
AppViewModel model = new AppViewModel();
|
AppViewModel model = new AppViewModel();
|
||||||
@@ -220,28 +259,83 @@ public class AutomationSnippet implements Snippet {
|
|||||||
model.setSenderPacketCount(packetCount);
|
model.setSenderPacketCount(packetCount);
|
||||||
model.setSenderPacketSize(packetSize);
|
model.setSenderPacketSize(packetSize);
|
||||||
model.setSenderPacketInterval(packetInterval);
|
model.setSenderPacketInterval(packetInterval);
|
||||||
|
if (connectionPriority != null) {
|
||||||
|
model.setConnectionPriority(connectionPriority);
|
||||||
|
}
|
||||||
|
if (startupDelay != null) {
|
||||||
|
model.setStartupDelay(startupDelay);
|
||||||
|
}
|
||||||
Runner runner = runScenario(model, "l2cap-client", scenario);
|
Runner runner = runScenario(model, "l2cap-client", scenario);
|
||||||
assert runner != null;
|
assert runner != null;
|
||||||
return runner.toJson();
|
return runner.toJson();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Rpc(description = "Run a scenario in L2CAP Server mode")
|
@Rpc(description = "Run a scenario in L2CAP Server mode")
|
||||||
public JSONObject runL2capServer(String scenario) throws JSONException {
|
public JSONObject runL2capServer(String scenario,
|
||||||
assert (mBluetoothAdapter != null);
|
@RpcOptional Integer startupDelay) throws JSONException {
|
||||||
|
|
||||||
// We only support "receive" and "pong" for this mode for now
|
// We only support "receive" and "pong" for this mode for now
|
||||||
if (!(scenario.equals("receive") || scenario.equals("pong"))) {
|
if (!(scenario.equals("receive") || scenario.equals("pong"))) {
|
||||||
throw new InvalidParameterException("only 'receive' and 'pong' are supported for this mode");
|
throw new InvalidParameterException(
|
||||||
|
"only 'receive' and 'pong' are supported for this mode");
|
||||||
}
|
}
|
||||||
|
|
||||||
AppViewModel model = new AppViewModel();
|
AppViewModel model = new AppViewModel();
|
||||||
|
if (startupDelay != null) {
|
||||||
|
model.setStartupDelay(startupDelay);
|
||||||
|
}
|
||||||
|
|
||||||
Runner runner = runScenario(model, "l2cap-server", scenario);
|
Runner runner = runScenario(model, "l2cap-server", scenario);
|
||||||
assert runner != null;
|
assert runner != null;
|
||||||
return runner.toJson();
|
return runner.toJson();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Rpc(description = "Run a scenario in GATT Client mode")
|
||||||
|
public JSONObject runGattClient(String scenario, String peerBluetoothAddress,
|
||||||
|
boolean use_2m_phy, int packetCount, int packetSize,
|
||||||
|
int packetInterval, @RpcOptional String connectionPriority,
|
||||||
|
@RpcOptional Integer startupDelay) throws JSONException {
|
||||||
|
// We only support "send" and "ping" for this mode for now
|
||||||
|
if (!(scenario.equals("send") || scenario.equals("ping"))) {
|
||||||
|
throw new InvalidParameterException(
|
||||||
|
"only 'send' and 'ping' are supported for this mode");
|
||||||
|
}
|
||||||
|
|
||||||
|
AppViewModel model = new AppViewModel();
|
||||||
|
model.setPeerBluetoothAddress(peerBluetoothAddress);
|
||||||
|
model.setUse2mPhy(use_2m_phy);
|
||||||
|
model.setSenderPacketCount(packetCount);
|
||||||
|
model.setSenderPacketSize(packetSize);
|
||||||
|
model.setSenderPacketInterval(packetInterval);
|
||||||
|
if (connectionPriority != null) {
|
||||||
|
model.setConnectionPriority(connectionPriority);
|
||||||
|
}
|
||||||
|
if (startupDelay != null) {
|
||||||
|
model.setStartupDelay(startupDelay);
|
||||||
|
}
|
||||||
|
Runner runner = runScenario(model, "gatt-client", scenario);
|
||||||
|
assert runner != null;
|
||||||
|
return runner.toJson();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Rpc(description = "Run a scenario in GATT Server mode")
|
||||||
|
public JSONObject runGattServer(String scenario,
|
||||||
|
@RpcOptional Integer startupDelay) throws JSONException {
|
||||||
|
// We only support "receive" and "pong" for this mode for now
|
||||||
|
if (!(scenario.equals("receive") || scenario.equals("pong"))) {
|
||||||
|
throw new InvalidParameterException(
|
||||||
|
"only 'receive' and 'pong' are supported for this mode");
|
||||||
|
}
|
||||||
|
|
||||||
|
AppViewModel model = new AppViewModel();
|
||||||
|
if (startupDelay != null) {
|
||||||
|
model.setStartupDelay(startupDelay);
|
||||||
|
}
|
||||||
|
|
||||||
|
Runner runner = runScenario(model, "gatt-server", scenario);
|
||||||
|
assert runner != null;
|
||||||
|
return runner.toJson();
|
||||||
|
}
|
||||||
|
|
||||||
@Rpc(description = "Stop a Runner")
|
@Rpc(description = "Stop a Runner")
|
||||||
public JSONObject stopRunner(String runnerId) throws JSONException {
|
public JSONObject stopRunner(String runnerId) throws JSONException {
|
||||||
Runner runner = findRunner(runnerId);
|
Runner runner = findRunner(runnerId);
|
||||||
@@ -276,7 +370,7 @@ public class AutomationSnippet implements Snippet {
|
|||||||
JSONObject result = new JSONObject();
|
JSONObject result = new JSONObject();
|
||||||
JSONArray runners = new JSONArray();
|
JSONArray runners = new JSONArray();
|
||||||
result.put("runners", runners);
|
result.put("runners", runners);
|
||||||
for (Runner runner: mRunners) {
|
for (Runner runner : mRunners) {
|
||||||
runners.put(runner.toJson());
|
runners.put(runner.toJson());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package com.github.google.bumble.btbench
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.bluetooth.BluetoothAdapter
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.bluetooth.BluetoothGatt
|
||||||
|
import android.bluetooth.BluetoothGattCallback
|
||||||
|
import android.bluetooth.BluetoothManager
|
||||||
|
import android.bluetooth.BluetoothProfile
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import java.util.logging.Logger
|
||||||
|
|
||||||
|
private val Log = Logger.getLogger("btbench.connection")
|
||||||
|
|
||||||
|
open class Connection(
|
||||||
|
private val viewModel: AppViewModel,
|
||||||
|
private val bluetoothAdapter: BluetoothAdapter,
|
||||||
|
private val context: Context
|
||||||
|
) : BluetoothGattCallback() {
|
||||||
|
var remoteDevice: BluetoothDevice? = null
|
||||||
|
var gatt: BluetoothGatt? = null
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
open fun connect() {
|
||||||
|
val addressIsPublic = viewModel.peerBluetoothAddress.endsWith("/P")
|
||||||
|
val address = viewModel.peerBluetoothAddress.take(17)
|
||||||
|
remoteDevice = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
bluetoothAdapter.getRemoteLeDevice(
|
||||||
|
address,
|
||||||
|
if (addressIsPublic) {
|
||||||
|
BluetoothDevice.ADDRESS_TYPE_PUBLIC
|
||||||
|
} else {
|
||||||
|
BluetoothDevice.ADDRESS_TYPE_RANDOM
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
bluetoothAdapter.getRemoteDevice(address)
|
||||||
|
}
|
||||||
|
|
||||||
|
gatt = remoteDevice?.connectGatt(
|
||||||
|
context,
|
||||||
|
false,
|
||||||
|
this,
|
||||||
|
BluetoothDevice.TRANSPORT_LE,
|
||||||
|
if (viewModel.use2mPhy) BluetoothDevice.PHY_LE_2M_MASK else BluetoothDevice.PHY_LE_1M_MASK
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
open fun disconnect() {
|
||||||
|
gatt?.disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
|
||||||
|
Log.info("MTU update: mtu=$mtu status=$status")
|
||||||
|
viewModel.mtu = mtu
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPhyUpdate(gatt: BluetoothGatt, txPhy: Int, rxPhy: Int, status: Int) {
|
||||||
|
Log.info("PHY update: tx=$txPhy, rx=$rxPhy, status=$status")
|
||||||
|
viewModel.txPhy = txPhy
|
||||||
|
viewModel.rxPhy = rxPhy
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPhyRead(gatt: BluetoothGatt, txPhy: Int, rxPhy: Int, status: Int) {
|
||||||
|
Log.info("PHY: tx=$txPhy, rx=$rxPhy, status=$status")
|
||||||
|
viewModel.txPhy = txPhy
|
||||||
|
viewModel.rxPhy = rxPhy
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
override fun onConnectionStateChange(
|
||||||
|
gatt: BluetoothGatt?, status: Int, newState: Int
|
||||||
|
) {
|
||||||
|
if (status != BluetoothGatt.GATT_SUCCESS) {
|
||||||
|
Log.warning("onConnectionStateChange status=$status")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gatt != null && newState == BluetoothProfile.STATE_CONNECTED) {
|
||||||
|
if (viewModel.use2mPhy) {
|
||||||
|
Log.info("requesting 2M PHY")
|
||||||
|
gatt.setPreferredPhy(
|
||||||
|
BluetoothDevice.PHY_LE_2M_MASK,
|
||||||
|
BluetoothDevice.PHY_LE_2M_MASK,
|
||||||
|
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)
|
||||||
|
|
||||||
|
// Request a specific connection priority
|
||||||
|
val connectionPriority = when (viewModel.connectionPriority) {
|
||||||
|
"BALANCED" -> BluetoothGatt.CONNECTION_PRIORITY_BALANCED
|
||||||
|
"LOW_POWER" -> BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER
|
||||||
|
"HIGH" -> BluetoothGatt.CONNECTION_PRIORITY_HIGH
|
||||||
|
"DCK" -> BluetoothGatt.CONNECTION_PRIORITY_DCK
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
if (!gatt.requestConnectionPriority(connectionPriority)) {
|
||||||
|
Log.warning("requestConnectionPriority failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// Copyright 2024 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.
|
||||||
|
|
||||||
|
package com.github.google.bumble.btbench
|
||||||
|
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
var CCCD_UUID = UUID.fromString("00002902-0000-1000-8000-00805F9B34FB")
|
||||||
|
|
||||||
|
val BENCH_SERVICE_UUID = UUID.fromString("50DB505C-8AC4-4738-8448-3B1D9CC09CC5")
|
||||||
|
val BENCH_TX_UUID = UUID.fromString("E789C754-41A1-45F4-A948-A0A1A90DBA53")
|
||||||
|
val BENCH_RX_UUID = UUID.fromString("016A2CC7-E14B-4819-935F-1F56EAE4098D")
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
// Copyright 2024 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.
|
||||||
|
|
||||||
|
package com.github.google.bumble.btbench
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.bluetooth.BluetoothAdapter
|
||||||
|
import android.bluetooth.BluetoothGatt
|
||||||
|
import android.bluetooth.BluetoothGattCharacteristic
|
||||||
|
import android.bluetooth.BluetoothGattDescriptor
|
||||||
|
import android.bluetooth.BluetoothProfile
|
||||||
|
import android.content.Context
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.UUID
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import java.util.concurrent.Semaphore
|
||||||
|
import java.util.logging.Logger
|
||||||
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
|
private val Log = Logger.getLogger("btbench.gatt-client")
|
||||||
|
|
||||||
|
|
||||||
|
class GattClientConnection(
|
||||||
|
viewModel: AppViewModel,
|
||||||
|
bluetoothAdapter: BluetoothAdapter,
|
||||||
|
context: Context
|
||||||
|
) : Connection(viewModel, bluetoothAdapter, context), PacketIO {
|
||||||
|
override var packetSink: PacketSink? = null
|
||||||
|
private val discoveryDone: CountDownLatch = CountDownLatch(1)
|
||||||
|
private val writeSemaphore: Semaphore = Semaphore(1)
|
||||||
|
var rxCharacteristic: BluetoothGattCharacteristic? = null
|
||||||
|
var txCharacteristic: BluetoothGattCharacteristic? = null
|
||||||
|
|
||||||
|
override fun connect() {
|
||||||
|
super.connect()
|
||||||
|
|
||||||
|
// Check if we're already connected and have discovered the services
|
||||||
|
if (gatt?.getService(BENCH_SERVICE_UUID) != null) {
|
||||||
|
Log.fine("already connected")
|
||||||
|
onServicesDiscovered(gatt, BluetoothGatt.GATT_SUCCESS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
override fun onConnectionStateChange(
|
||||||
|
gatt: BluetoothGatt?, status: Int, newState: Int
|
||||||
|
) {
|
||||||
|
super.onConnectionStateChange(gatt, status, newState)
|
||||||
|
if (status != BluetoothGatt.GATT_SUCCESS) {
|
||||||
|
Log.warning("onConnectionStateChange status=$status")
|
||||||
|
discoveryDone.countDown()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (gatt != null && newState == BluetoothProfile.STATE_CONNECTED) {
|
||||||
|
if (!gatt.discoverServices()) {
|
||||||
|
Log.warning("discoverServices could not start")
|
||||||
|
discoveryDone.countDown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
|
||||||
|
Log.fine("onServicesDiscovered")
|
||||||
|
|
||||||
|
if (status != BluetoothGatt.GATT_SUCCESS) {
|
||||||
|
Log.warning("failed to discover services: ${status}")
|
||||||
|
discoveryDone.countDown()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the service
|
||||||
|
val service = gatt!!.getService(BENCH_SERVICE_UUID)
|
||||||
|
if (service == null) {
|
||||||
|
Log.warning("GATT Service not found")
|
||||||
|
discoveryDone.countDown()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the RX and TX characteristics
|
||||||
|
rxCharacteristic = service.getCharacteristic(BENCH_RX_UUID)
|
||||||
|
if (rxCharacteristic == null) {
|
||||||
|
Log.warning("GATT RX Characteristics not found")
|
||||||
|
discoveryDone.countDown()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
txCharacteristic = service.getCharacteristic(BENCH_TX_UUID)
|
||||||
|
if (txCharacteristic == null) {
|
||||||
|
Log.warning("GATT TX Characteristics not found")
|
||||||
|
discoveryDone.countDown()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to the RX characteristic
|
||||||
|
Log.fine("subscribing to RX")
|
||||||
|
gatt.setCharacteristicNotification(rxCharacteristic, true)
|
||||||
|
val cccdDescriptor = rxCharacteristic!!.getDescriptor(CCCD_UUID)
|
||||||
|
gatt.writeDescriptor(cccdDescriptor, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
|
||||||
|
|
||||||
|
Log.info("GATT discovery complete")
|
||||||
|
discoveryDone.countDown()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCharacteristicWrite(
|
||||||
|
gatt: BluetoothGatt?,
|
||||||
|
characteristic: BluetoothGattCharacteristic?,
|
||||||
|
status: Int
|
||||||
|
) {
|
||||||
|
// Now we can write again
|
||||||
|
writeSemaphore.release()
|
||||||
|
|
||||||
|
if (status != BluetoothGatt.GATT_SUCCESS) {
|
||||||
|
Log.warning("onCharacteristicWrite failed: $status")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCharacteristicChanged(
|
||||||
|
gatt: BluetoothGatt,
|
||||||
|
characteristic: BluetoothGattCharacteristic,
|
||||||
|
value: ByteArray
|
||||||
|
) {
|
||||||
|
if (characteristic.uuid == BENCH_RX_UUID && packetSink != null) {
|
||||||
|
val packet = Packet.from(value)
|
||||||
|
packetSink!!.onPacket(packet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
override fun sendPacket(packet: Packet) {
|
||||||
|
if (txCharacteristic == null) {
|
||||||
|
Log.warning("No TX characteristic, dropping")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait until we can write
|
||||||
|
writeSemaphore.acquire()
|
||||||
|
|
||||||
|
// Write the data
|
||||||
|
val data = packet.toBytes()
|
||||||
|
val clampedData = if (data.size > 512) {
|
||||||
|
// Clamp the data to the maximum allowed characteristic data size
|
||||||
|
data.copyOf(512)
|
||||||
|
} else {
|
||||||
|
data
|
||||||
|
}
|
||||||
|
gatt?.writeCharacteristic(
|
||||||
|
txCharacteristic!!,
|
||||||
|
clampedData,
|
||||||
|
BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override
|
||||||
|
fun disconnect() {
|
||||||
|
super.disconnect()
|
||||||
|
discoveryDone.countDown()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun waitForDiscoveryCompletion() {
|
||||||
|
discoveryDone.await()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GattClient(
|
||||||
|
private val viewModel: AppViewModel,
|
||||||
|
bluetoothAdapter: BluetoothAdapter,
|
||||||
|
context: Context,
|
||||||
|
private val createIoClient: (packetIo: PacketIO) -> IoClient
|
||||||
|
) : Mode {
|
||||||
|
private var connection: GattClientConnection =
|
||||||
|
GattClientConnection(viewModel, bluetoothAdapter, context)
|
||||||
|
private var clientThread: Thread? = null
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
override fun run() {
|
||||||
|
viewModel.running = true
|
||||||
|
|
||||||
|
clientThread = thread(name = "GattClient") {
|
||||||
|
connection.connect()
|
||||||
|
|
||||||
|
viewModel.aborter = {
|
||||||
|
connection.disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discover the rx and tx characteristics
|
||||||
|
connection.waitForDiscoveryCompletion()
|
||||||
|
if (connection.rxCharacteristic == null || connection.txCharacteristic == null) {
|
||||||
|
connection.disconnect()
|
||||||
|
viewModel.running = false
|
||||||
|
return@thread
|
||||||
|
}
|
||||||
|
|
||||||
|
val ioClient = createIoClient(connection)
|
||||||
|
|
||||||
|
try {
|
||||||
|
ioClient.run()
|
||||||
|
viewModel.status = "OK"
|
||||||
|
} catch (error: IOException) {
|
||||||
|
Log.info("run ended abruptly")
|
||||||
|
viewModel.status = "ABORTED"
|
||||||
|
viewModel.lastError = "IO_ERROR"
|
||||||
|
} finally {
|
||||||
|
connection.disconnect()
|
||||||
|
viewModel.running = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun waitForCompletion() {
|
||||||
|
clientThread?.join()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
// Copyright 2024 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.
|
||||||
|
|
||||||
|
package com.github.google.bumble.btbench
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.bluetooth.BluetoothAdapter
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.bluetooth.BluetoothGatt
|
||||||
|
import android.bluetooth.BluetoothGattCharacteristic
|
||||||
|
import android.bluetooth.BluetoothGattDescriptor
|
||||||
|
import android.bluetooth.BluetoothGattServer
|
||||||
|
import android.bluetooth.BluetoothGattServerCallback
|
||||||
|
import android.bluetooth.BluetoothGattService
|
||||||
|
import android.bluetooth.BluetoothManager
|
||||||
|
import android.bluetooth.BluetoothStatusCodes
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue
|
||||||
|
import java.util.concurrent.Semaphore
|
||||||
|
import java.util.logging.Logger
|
||||||
|
import kotlin.concurrent.thread
|
||||||
|
import kotlin.experimental.and
|
||||||
|
|
||||||
|
private val Log = Logger.getLogger("btbench.gatt-server")
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
class GattServer(
|
||||||
|
private val viewModel: AppViewModel,
|
||||||
|
private val bluetoothAdapter: BluetoothAdapter,
|
||||||
|
context: Context,
|
||||||
|
private val createIoClient: (packetIo: PacketIO) -> IoClient
|
||||||
|
) : Mode, PacketIO, BluetoothGattServerCallback() {
|
||||||
|
override var packetSink: PacketSink? = null
|
||||||
|
private val gattServer: BluetoothGattServer
|
||||||
|
private val rxCharacteristic: BluetoothGattCharacteristic?
|
||||||
|
private val txCharacteristic: BluetoothGattCharacteristic?
|
||||||
|
private val notifySemaphore: Semaphore = Semaphore(1)
|
||||||
|
private val ready: CountDownLatch = CountDownLatch(1)
|
||||||
|
private var peerDevice: BluetoothDevice? = null
|
||||||
|
private var clientThread: Thread? = null
|
||||||
|
private var sinkQueue: LinkedBlockingQueue<Packet>? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
val bluetoothManager = ContextCompat.getSystemService(context, BluetoothManager::class.java)
|
||||||
|
gattServer = bluetoothManager!!.openGattServer(context, this)
|
||||||
|
val benchService = gattServer.getService(BENCH_SERVICE_UUID)
|
||||||
|
if (benchService == null) {
|
||||||
|
rxCharacteristic = BluetoothGattCharacteristic(
|
||||||
|
BENCH_RX_UUID,
|
||||||
|
BluetoothGattCharacteristic.PROPERTY_NOTIFY,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
txCharacteristic = BluetoothGattCharacteristic(
|
||||||
|
BENCH_TX_UUID,
|
||||||
|
BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE,
|
||||||
|
BluetoothGattCharacteristic.PERMISSION_WRITE
|
||||||
|
)
|
||||||
|
val rxCCCD = BluetoothGattDescriptor(
|
||||||
|
CCCD_UUID,
|
||||||
|
BluetoothGattDescriptor.PERMISSION_READ or BluetoothGattDescriptor.PERMISSION_WRITE
|
||||||
|
)
|
||||||
|
rxCharacteristic.addDescriptor(rxCCCD)
|
||||||
|
|
||||||
|
val service =
|
||||||
|
BluetoothGattService(BENCH_SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY)
|
||||||
|
service.addCharacteristic(rxCharacteristic)
|
||||||
|
service.addCharacteristic(txCharacteristic)
|
||||||
|
|
||||||
|
gattServer.addService(service)
|
||||||
|
} else {
|
||||||
|
rxCharacteristic = benchService.getCharacteristic(BENCH_RX_UUID)
|
||||||
|
txCharacteristic = benchService.getCharacteristic(BENCH_TX_UUID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCharacteristicWriteRequest(
|
||||||
|
device: BluetoothDevice?,
|
||||||
|
requestId: Int,
|
||||||
|
characteristic: BluetoothGattCharacteristic?,
|
||||||
|
preparedWrite: Boolean,
|
||||||
|
responseNeeded: Boolean,
|
||||||
|
offset: Int,
|
||||||
|
value: ByteArray?
|
||||||
|
) {
|
||||||
|
Log.info("onCharacteristicWriteRequest")
|
||||||
|
if (characteristic != null && characteristic.uuid == BENCH_TX_UUID) {
|
||||||
|
if (packetSink == null) {
|
||||||
|
Log.warning("no sink, dropping")
|
||||||
|
} else if (offset != 0) {
|
||||||
|
Log.warning("offset != 0")
|
||||||
|
} else if (value == null) {
|
||||||
|
Log.warning("no value")
|
||||||
|
} else {
|
||||||
|
// Deliver the packet in a separate thread so that we don't block this
|
||||||
|
// callback.
|
||||||
|
sinkQueue?.put(Packet.from(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responseNeeded) {
|
||||||
|
gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNotificationSent(device: BluetoothDevice?, status: Int) {
|
||||||
|
if (status == BluetoothGatt.GATT_SUCCESS) {
|
||||||
|
notifySemaphore.release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDescriptorWriteRequest(
|
||||||
|
device: BluetoothDevice?,
|
||||||
|
requestId: Int,
|
||||||
|
descriptor: BluetoothGattDescriptor?,
|
||||||
|
preparedWrite: Boolean,
|
||||||
|
responseNeeded: Boolean,
|
||||||
|
offset: Int,
|
||||||
|
value: ByteArray?
|
||||||
|
) {
|
||||||
|
if (descriptor?.uuid == CCCD_UUID && descriptor?.characteristic?.uuid == BENCH_RX_UUID) {
|
||||||
|
if (offset == 0 && value?.size == 2) {
|
||||||
|
if (value[0].and(1).toInt() != 0) {
|
||||||
|
// Subscription
|
||||||
|
Log.fine("peer subscribed to RX")
|
||||||
|
peerDevice = device
|
||||||
|
ready.countDown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responseNeeded) {
|
||||||
|
gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
override fun sendPacket(packet: Packet) {
|
||||||
|
if (peerDevice == null) {
|
||||||
|
Log.warning("no peer device, cannot send")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (rxCharacteristic == null) {
|
||||||
|
Log.warning("no RX characteristic, cannot send")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait until we can notify
|
||||||
|
notifySemaphore.acquire()
|
||||||
|
|
||||||
|
// Send the packet via a notification
|
||||||
|
val result = gattServer.notifyCharacteristicChanged(
|
||||||
|
peerDevice!!,
|
||||||
|
rxCharacteristic,
|
||||||
|
false,
|
||||||
|
packet.toBytes()
|
||||||
|
)
|
||||||
|
if (result != BluetoothStatusCodes.SUCCESS) {
|
||||||
|
Log.warning("notifyCharacteristicChanged failed: $result")
|
||||||
|
notifySemaphore.release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun run() {
|
||||||
|
viewModel.running = true
|
||||||
|
|
||||||
|
// Start advertising
|
||||||
|
Log.fine("starting advertiser")
|
||||||
|
val advertiser = Advertiser(bluetoothAdapter)
|
||||||
|
advertiser.start()
|
||||||
|
|
||||||
|
clientThread = thread(name = "GattServer") {
|
||||||
|
// Wait for a subscriber
|
||||||
|
Log.info("waiting for RX subscriber")
|
||||||
|
viewModel.aborter = {
|
||||||
|
ready.countDown()
|
||||||
|
}
|
||||||
|
ready.await()
|
||||||
|
if (peerDevice == null) {
|
||||||
|
Log.warning("server interrupted")
|
||||||
|
viewModel.running = false
|
||||||
|
gattServer.close()
|
||||||
|
return@thread
|
||||||
|
}
|
||||||
|
Log.info("RX subscriber accepted")
|
||||||
|
|
||||||
|
// Stop advertising
|
||||||
|
Log.info("stopping advertiser")
|
||||||
|
advertiser.stop()
|
||||||
|
|
||||||
|
sinkQueue = LinkedBlockingQueue()
|
||||||
|
val sinkWriterThread = thread(name = "SinkWriter") {
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
val packet = sinkQueue!!.take()
|
||||||
|
if (packetSink == null) {
|
||||||
|
Log.warning("no sink, dropping packet")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
packetSink!!.onPacket(packet)
|
||||||
|
} catch (error: InterruptedException) {
|
||||||
|
Log.warning("sink writer interrupted")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val ioClient = createIoClient(this)
|
||||||
|
|
||||||
|
try {
|
||||||
|
ioClient.run()
|
||||||
|
viewModel.status = "OK"
|
||||||
|
} catch (error: IOException) {
|
||||||
|
Log.info("run ended abruptly")
|
||||||
|
viewModel.status = "ABORTED"
|
||||||
|
viewModel.lastError = "IO_ERROR"
|
||||||
|
} finally {
|
||||||
|
sinkWriterThread.interrupt()
|
||||||
|
sinkWriterThread.join()
|
||||||
|
gattServer.close()
|
||||||
|
viewModel.running = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun waitForCompletion() {
|
||||||
|
clientThread?.join()
|
||||||
|
Log.info("server thread completed")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,89 +16,25 @@ package com.github.google.bumble.btbench
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.bluetooth.BluetoothAdapter
|
import android.bluetooth.BluetoothAdapter
|
||||||
import android.bluetooth.BluetoothDevice
|
|
||||||
import android.bluetooth.BluetoothGatt
|
|
||||||
import android.bluetooth.BluetoothGattCallback
|
|
||||||
import android.bluetooth.BluetoothProfile
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
|
||||||
import java.util.logging.Logger
|
import java.util.logging.Logger
|
||||||
|
|
||||||
private val Log = Logger.getLogger("btbench.l2cap-client")
|
private val Log = Logger.getLogger("btbench.l2cap-client")
|
||||||
|
|
||||||
class L2capClient(
|
class L2capClient(
|
||||||
private val viewModel: AppViewModel,
|
private val viewModel: AppViewModel,
|
||||||
private val bluetoothAdapter: BluetoothAdapter,
|
bluetoothAdapter: BluetoothAdapter,
|
||||||
private val context: Context,
|
context: Context,
|
||||||
private val createIoClient: (packetIo: PacketIO) -> IoClient
|
private val createIoClient: (packetIo: PacketIO) -> IoClient
|
||||||
) : Mode {
|
) : Mode {
|
||||||
|
private var connection: Connection = Connection(viewModel, bluetoothAdapter, context)
|
||||||
private var socketClient: SocketClient? = null
|
private var socketClient: SocketClient? = null
|
||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
override fun run() {
|
override fun run() {
|
||||||
viewModel.running = true
|
viewModel.running = true
|
||||||
val addressIsPublic = viewModel.peerBluetoothAddress.endsWith("/P")
|
connection.connect()
|
||||||
val address = viewModel.peerBluetoothAddress.take(17)
|
val socket = connection.remoteDevice!!.createInsecureL2capChannel(viewModel.l2capPsm)
|
||||||
val remoteDevice = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
bluetoothAdapter.getRemoteLeDevice(
|
|
||||||
address,
|
|
||||||
if (addressIsPublic) {
|
|
||||||
BluetoothDevice.ADDRESS_TYPE_PUBLIC
|
|
||||||
} else {
|
|
||||||
BluetoothDevice.ADDRESS_TYPE_RANDOM
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
bluetoothAdapter.getRemoteDevice(address)
|
|
||||||
}
|
|
||||||
|
|
||||||
val gatt = remoteDevice.connectGatt(
|
|
||||||
context,
|
|
||||||
false,
|
|
||||||
object : BluetoothGattCallback() {
|
|
||||||
override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
|
|
||||||
Log.info("MTU update: mtu=$mtu status=$status")
|
|
||||||
viewModel.mtu = mtu
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPhyUpdate(gatt: BluetoothGatt, txPhy: Int, rxPhy: Int, status: Int) {
|
|
||||||
Log.info("PHY update: tx=$txPhy, rx=$rxPhy, status=$status")
|
|
||||||
viewModel.txPhy = txPhy
|
|
||||||
viewModel.rxPhy = rxPhy
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPhyRead(gatt: BluetoothGatt, txPhy: Int, rxPhy: Int, status: Int) {
|
|
||||||
Log.info("PHY: tx=$txPhy, rx=$rxPhy, status=$status")
|
|
||||||
viewModel.txPhy = txPhy
|
|
||||||
viewModel.rxPhy = rxPhy
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onConnectionStateChange(
|
|
||||||
gatt: BluetoothGatt?, status: Int, newState: Int
|
|
||||||
) {
|
|
||||||
if (gatt != null && newState == BluetoothProfile.STATE_CONNECTED) {
|
|
||||||
if (viewModel.use2mPhy) {
|
|
||||||
Log.info("requesting 2M PHY")
|
|
||||||
gatt.setPreferredPhy(
|
|
||||||
BluetoothDevice.PHY_LE_2M_MASK,
|
|
||||||
BluetoothDevice.PHY_LE_2M_MASK,
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
BluetoothDevice.TRANSPORT_LE,
|
|
||||||
if (viewModel.use2mPhy) BluetoothDevice.PHY_LE_2M_MASK else BluetoothDevice.PHY_LE_1M_MASK
|
|
||||||
)
|
|
||||||
|
|
||||||
val socket = remoteDevice.createInsecureL2capChannel(viewModel.l2capPsm)
|
|
||||||
|
|
||||||
socketClient = SocketClient(viewModel, socket, createIoClient)
|
socketClient = SocketClient(viewModel, socket, createIoClient)
|
||||||
socketClient!!.run()
|
socketClient!!.run()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,34 +37,15 @@ class L2capServer(
|
|||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
override fun run() {
|
override fun run() {
|
||||||
// Advertise so that the peer can find us and connect.
|
// Advertise so that the peer can find us and connect.
|
||||||
val callback = object : AdvertiseCallback() {
|
val advertiser = Advertiser(bluetoothAdapter)
|
||||||
override fun onStartFailure(errorCode: Int) {
|
|
||||||
Log.warning("failed to start advertising: $errorCode")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStartSuccess(settingsInEffect: AdvertiseSettings) {
|
|
||||||
Log.info("advertising started: $settingsInEffect")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val advertiseSettingsBuilder = AdvertiseSettings.Builder()
|
|
||||||
.setAdvertiseMode(ADVERTISE_MODE_LOW_LATENCY)
|
|
||||||
.setConnectable(true)
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
||||||
advertiseSettingsBuilder.setDiscoverable(true)
|
|
||||||
}
|
|
||||||
val advertiseSettings = advertiseSettingsBuilder.build()
|
|
||||||
val advertiseData = AdvertiseData.Builder().build()
|
|
||||||
val scanData = AdvertiseData.Builder().setIncludeDeviceName(true).build()
|
|
||||||
val advertiser = bluetoothAdapter.bluetoothLeAdvertiser
|
|
||||||
|
|
||||||
val serverSocket = bluetoothAdapter.listenUsingInsecureL2capChannel()
|
val serverSocket = bluetoothAdapter.listenUsingInsecureL2capChannel()
|
||||||
viewModel.l2capPsm = serverSocket.psm
|
viewModel.l2capPsm = serverSocket.psm
|
||||||
Log.info("psm = $serverSocket.psm")
|
Log.info("psm = $serverSocket.psm")
|
||||||
|
|
||||||
socketServer = SocketServer(viewModel, serverSocket, createIoClient)
|
socketServer = SocketServer(viewModel, serverSocket, createIoClient)
|
||||||
socketServer!!.run(
|
socketServer!!.run(
|
||||||
{ advertiser.stopAdvertising(callback) },
|
{ advertiser.stop() },
|
||||||
{ advertiser.startAdvertising(advertiseSettings, advertiseData, scanData, callback) }
|
{ advertiser.start() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,9 +17,12 @@ package com.github.google.bumble.btbench
|
|||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.bluetooth.BluetoothAdapter
|
import android.bluetooth.BluetoothAdapter
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
import android.bluetooth.BluetoothManager
|
import android.bluetooth.BluetoothManager
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@@ -66,6 +69,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.github.google.bumble.btbench.ui.theme.BTBenchTheme
|
import com.github.google.bumble.btbench.ui.theme.BTBenchTheme
|
||||||
|
import java.io.IOException
|
||||||
import java.util.logging.Logger
|
import java.util.logging.Logger
|
||||||
|
|
||||||
private val Log = Logger.getLogger("bumble.main-activity")
|
private val Log = Logger.getLogger("bumble.main-activity")
|
||||||
@@ -76,6 +80,7 @@ const val SENDER_PACKET_SIZE_PREF_KEY = "sender_packet_size"
|
|||||||
const val SENDER_PACKET_INTERVAL_PREF_KEY = "sender_packet_interval"
|
const val SENDER_PACKET_INTERVAL_PREF_KEY = "sender_packet_interval"
|
||||||
const val SCENARIO_PREF_KEY = "scenario"
|
const val SCENARIO_PREF_KEY = "scenario"
|
||||||
const val MODE_PREF_KEY = "mode"
|
const val MODE_PREF_KEY = "mode"
|
||||||
|
const val CONNECTION_PRIORITY_PREF_KEY = "connection_priority"
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
private val appViewModel = AppViewModel()
|
private val appViewModel = AppViewModel()
|
||||||
@@ -84,6 +89,47 @@ class MainActivity : ComponentActivity() {
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
appViewModel.loadPreferences(getPreferences(Context.MODE_PRIVATE))
|
appViewModel.loadPreferences(getPreferences(Context.MODE_PRIVATE))
|
||||||
checkPermissions()
|
checkPermissions()
|
||||||
|
registerReceivers()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun registerReceivers() {
|
||||||
|
val pairingRequestIntentFilter = IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST)
|
||||||
|
registerReceiver(object: BroadcastReceiver() {
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
Log.info("ACTION_PAIRING_REQUEST")
|
||||||
|
val extras = intent.extras
|
||||||
|
if (extras != null) {
|
||||||
|
for (key in extras.keySet()) {
|
||||||
|
Log.info("$key: ${extras.get(key)}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val device: BluetoothDevice? = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
|
||||||
|
if (device != null) {
|
||||||
|
if (checkSelfPermission(Manifest.permission.BLUETOOTH_PRIVILEGED) == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
Log.info("confirming pairing")
|
||||||
|
device.setPairingConfirmation(true)
|
||||||
|
} else {
|
||||||
|
Log.info("we don't have BLUETOOTH_PRIVILEGED, not confirming")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}, pairingRequestIntentFilter)
|
||||||
|
|
||||||
|
val bondStateChangedIntentFilter = IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
|
||||||
|
registerReceiver(object: BroadcastReceiver() {
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
Log.info("ACTION_BOND_STATE_CHANGED")
|
||||||
|
val extras = intent.extras
|
||||||
|
if (extras != null) {
|
||||||
|
for (key in extras.keySet()) {
|
||||||
|
Log.info("$key: ${extras.get(key)}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, bondStateChangedIntentFilter)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkPermissions() {
|
private fun checkPermissions() {
|
||||||
@@ -144,9 +190,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
initBluetooth()
|
initBluetooth()
|
||||||
setContent {
|
setContent {
|
||||||
MainView(
|
MainView(
|
||||||
appViewModel,
|
appViewModel, ::becomeDiscoverable, ::runScenario
|
||||||
::becomeDiscoverable,
|
|
||||||
::runScenario
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,6 +226,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
"rfcomm-server" -> appViewModel.mode = RFCOMM_SERVER_MODE
|
"rfcomm-server" -> appViewModel.mode = RFCOMM_SERVER_MODE
|
||||||
"l2cap-client" -> appViewModel.mode = L2CAP_CLIENT_MODE
|
"l2cap-client" -> appViewModel.mode = L2CAP_CLIENT_MODE
|
||||||
"l2cap-server" -> appViewModel.mode = L2CAP_SERVER_MODE
|
"l2cap-server" -> appViewModel.mode = L2CAP_SERVER_MODE
|
||||||
|
"gatt-client" -> appViewModel.mode = GATT_CLIENT_MODE
|
||||||
|
"gatt-server" -> appViewModel.mode = GATT_SERVER_MODE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
intent.getStringExtra("autostart")?.let {
|
intent.getStringExtra("autostart")?.let {
|
||||||
@@ -195,19 +241,24 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
private fun runScenario() {
|
private fun runScenario() {
|
||||||
if (bluetoothAdapter == null) {
|
if (bluetoothAdapter == null) {
|
||||||
return
|
throw IOException("bluetooth not enabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
val runner = when (appViewModel.mode) {
|
val runner = when (appViewModel.mode) {
|
||||||
RFCOMM_CLIENT_MODE -> RfcommClient(appViewModel, bluetoothAdapter!!, ::createIoClient)
|
RFCOMM_CLIENT_MODE -> RfcommClient(appViewModel, bluetoothAdapter!!, ::createIoClient)
|
||||||
RFCOMM_SERVER_MODE -> RfcommServer(appViewModel, bluetoothAdapter!!, ::createIoClient)
|
RFCOMM_SERVER_MODE -> RfcommServer(appViewModel, bluetoothAdapter!!, ::createIoClient)
|
||||||
L2CAP_CLIENT_MODE -> L2capClient(
|
L2CAP_CLIENT_MODE -> L2capClient(
|
||||||
appViewModel,
|
appViewModel, bluetoothAdapter!!, baseContext, ::createIoClient
|
||||||
bluetoothAdapter!!,
|
|
||||||
baseContext,
|
|
||||||
::createIoClient
|
|
||||||
)
|
)
|
||||||
|
|
||||||
L2CAP_SERVER_MODE -> L2capServer(appViewModel, bluetoothAdapter!!, ::createIoClient)
|
L2CAP_SERVER_MODE -> L2capServer(appViewModel, bluetoothAdapter!!, ::createIoClient)
|
||||||
|
GATT_CLIENT_MODE -> GattClient(
|
||||||
|
appViewModel, bluetoothAdapter!!, baseContext, ::createIoClient
|
||||||
|
)
|
||||||
|
GATT_SERVER_MODE -> GattServer(
|
||||||
|
appViewModel, bluetoothAdapter!!, baseContext, ::createIoClient
|
||||||
|
)
|
||||||
|
|
||||||
else -> throw IllegalStateException()
|
else -> throw IllegalStateException()
|
||||||
}
|
}
|
||||||
runner.run()
|
runner.run()
|
||||||
@@ -281,7 +332,7 @@ fun MainView(
|
|||||||
keyboardController?.hide()
|
keyboardController?.hide()
|
||||||
focusManager.clearFocus()
|
focusManager.clearFocus()
|
||||||
}),
|
}),
|
||||||
enabled = (appViewModel.mode == RFCOMM_CLIENT_MODE) or (appViewModel.mode == L2CAP_CLIENT_MODE)
|
enabled = (appViewModel.mode == RFCOMM_CLIENT_MODE || appViewModel.mode == L2CAP_CLIENT_MODE || appViewModel.mode == GATT_CLIENT_MODE)
|
||||||
)
|
)
|
||||||
Divider()
|
Divider()
|
||||||
TextField(
|
TextField(
|
||||||
@@ -349,24 +400,45 @@ fun MainView(
|
|||||||
keyboardController?.hide()
|
keyboardController?.hide()
|
||||||
focusManager.clearFocus()
|
focusManager.clearFocus()
|
||||||
}),
|
}),
|
||||||
enabled = (appViewModel.scenario == PING_SCENARIO)
|
enabled = (appViewModel.scenario == PING_SCENARIO || appViewModel.scenario == SEND_SCENARIO)
|
||||||
)
|
)
|
||||||
Divider()
|
Divider()
|
||||||
ActionButton(
|
|
||||||
text = "Become Discoverable", onClick = becomeDiscoverable, true
|
|
||||||
)
|
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Text(text = "2M PHY")
|
Text(text = "2M PHY")
|
||||||
Spacer(modifier = Modifier.padding(start = 8.dp))
|
Spacer(modifier = Modifier.padding(start = 8.dp))
|
||||||
Switch(
|
Switch(enabled = (appViewModel.mode == L2CAP_CLIENT_MODE || appViewModel.mode == L2CAP_SERVER_MODE || appViewModel.mode == GATT_CLIENT_MODE || appViewModel.mode == GATT_SERVER_MODE),
|
||||||
enabled = (appViewModel.mode == L2CAP_CLIENT_MODE || appViewModel.mode == L2CAP_SERVER_MODE),
|
|
||||||
checked = appViewModel.use2mPhy,
|
checked = appViewModel.use2mPhy,
|
||||||
onCheckedChange = { appViewModel.use2mPhy = it }
|
onCheckedChange = { appViewModel.use2mPhy = it })
|
||||||
)
|
Column(Modifier.selectableGroup()) {
|
||||||
|
listOf(
|
||||||
|
"BALANCED", "LOW", "HIGH", "DCK"
|
||||||
|
).forEach { text ->
|
||||||
|
Row(
|
||||||
|
Modifier
|
||||||
|
.selectable(
|
||||||
|
selected = (text == appViewModel.connectionPriority),
|
||||||
|
onClick = { appViewModel.updateConnectionPriority(text) },
|
||||||
|
role = Role.RadioButton,
|
||||||
|
)
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
RadioButton(
|
||||||
|
selected = (text == appViewModel.connectionPriority),
|
||||||
|
onClick = null,
|
||||||
|
enabled = (appViewModel.mode == L2CAP_CLIENT_MODE || appViewModel.mode == L2CAP_SERVER_MODE || appViewModel.mode == GATT_CLIENT_MODE || appViewModel.mode == GATT_SERVER_MODE)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
modifier = Modifier.padding(start = 16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Row {
|
Row {
|
||||||
Column(Modifier.selectableGroup()) {
|
Column(Modifier.selectableGroup()) {
|
||||||
@@ -374,7 +446,9 @@ fun MainView(
|
|||||||
RFCOMM_CLIENT_MODE,
|
RFCOMM_CLIENT_MODE,
|
||||||
RFCOMM_SERVER_MODE,
|
RFCOMM_SERVER_MODE,
|
||||||
L2CAP_CLIENT_MODE,
|
L2CAP_CLIENT_MODE,
|
||||||
L2CAP_SERVER_MODE
|
L2CAP_SERVER_MODE,
|
||||||
|
GATT_CLIENT_MODE,
|
||||||
|
GATT_SERVER_MODE
|
||||||
).forEach { text ->
|
).forEach { text ->
|
||||||
Row(
|
Row(
|
||||||
Modifier
|
Modifier
|
||||||
@@ -387,8 +461,7 @@ fun MainView(
|
|||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
RadioButton(
|
RadioButton(
|
||||||
selected = (text == appViewModel.mode),
|
selected = (text == appViewModel.mode), onClick = null
|
||||||
onClick = null
|
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = text,
|
text = text,
|
||||||
@@ -400,10 +473,7 @@ fun MainView(
|
|||||||
}
|
}
|
||||||
Column(Modifier.selectableGroup()) {
|
Column(Modifier.selectableGroup()) {
|
||||||
listOf(
|
listOf(
|
||||||
SEND_SCENARIO,
|
SEND_SCENARIO, RECEIVE_SCENARIO, PING_SCENARIO, PONG_SCENARIO
|
||||||
RECEIVE_SCENARIO,
|
|
||||||
PING_SCENARIO,
|
|
||||||
PONG_SCENARIO
|
|
||||||
).forEach { text ->
|
).forEach { text ->
|
||||||
Row(
|
Row(
|
||||||
Modifier
|
Modifier
|
||||||
@@ -416,8 +486,7 @@ fun MainView(
|
|||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
RadioButton(
|
RadioButton(
|
||||||
selected = (text == appViewModel.scenario),
|
selected = (text == appViewModel.scenario), onClick = null
|
||||||
onClick = null
|
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = text,
|
text = text,
|
||||||
@@ -435,20 +504,29 @@ fun MainView(
|
|||||||
ActionButton(
|
ActionButton(
|
||||||
text = "Stop", onClick = appViewModel::abort, enabled = appViewModel.running
|
text = "Stop", onClick = appViewModel::abort, enabled = appViewModel.running
|
||||||
)
|
)
|
||||||
|
ActionButton(
|
||||||
|
text = "Become Discoverable", onClick = becomeDiscoverable, true
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Divider()
|
Divider()
|
||||||
Text(
|
if (appViewModel.mtu != 0) {
|
||||||
text = if (appViewModel.mtu != 0) "MTU: ${appViewModel.mtu}" else ""
|
Text(
|
||||||
)
|
text = "MTU: ${appViewModel.mtu}"
|
||||||
Text(
|
)
|
||||||
text = if (appViewModel.rxPhy != 0 || appViewModel.txPhy != 0) "PHY: tx=${appViewModel.txPhy}, rx=${appViewModel.rxPhy}" else ""
|
}
|
||||||
)
|
if (appViewModel.rxPhy != 0) {
|
||||||
|
Text(
|
||||||
|
text = "PHY: tx=${appViewModel.txPhy}, rx=${appViewModel.rxPhy}"
|
||||||
|
)
|
||||||
|
}
|
||||||
Text(
|
Text(
|
||||||
text = "Status: ${appViewModel.status}"
|
text = "Status: ${appViewModel.status}"
|
||||||
)
|
)
|
||||||
Text(
|
if (appViewModel.lastError.isNotEmpty()) {
|
||||||
text = "Last Error: ${appViewModel.lastError}"
|
Text(
|
||||||
)
|
text = "Last Error: ${appViewModel.lastError}"
|
||||||
|
)
|
||||||
|
}
|
||||||
Text(
|
Text(
|
||||||
text = "Packets Sent: ${appViewModel.packetsSent}"
|
text = "Packets Sent: ${appViewModel.packetsSent}"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import java.util.UUID
|
|||||||
|
|
||||||
val DEFAULT_RFCOMM_UUID: 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_PEER_BLUETOOTH_ADDRESS = "AA:BB:CC:DD:EE:FF"
|
||||||
|
const val DEFAULT_STARTUP_DELAY = 3000
|
||||||
const val DEFAULT_SENDER_PACKET_COUNT = 100
|
const val DEFAULT_SENDER_PACKET_COUNT = 100
|
||||||
const val DEFAULT_SENDER_PACKET_SIZE = 1024
|
const val DEFAULT_SENDER_PACKET_SIZE = 1024
|
||||||
const val DEFAULT_SENDER_PACKET_INTERVAL = 100
|
const val DEFAULT_SENDER_PACKET_INTERVAL = 100
|
||||||
@@ -34,6 +35,8 @@ const val L2CAP_CLIENT_MODE = "L2CAP Client"
|
|||||||
const val L2CAP_SERVER_MODE = "L2CAP Server"
|
const val L2CAP_SERVER_MODE = "L2CAP Server"
|
||||||
const val RFCOMM_CLIENT_MODE = "RFCOMM Client"
|
const val RFCOMM_CLIENT_MODE = "RFCOMM Client"
|
||||||
const val RFCOMM_SERVER_MODE = "RFCOMM Server"
|
const val RFCOMM_SERVER_MODE = "RFCOMM Server"
|
||||||
|
const val GATT_CLIENT_MODE = "GATT Client"
|
||||||
|
const val GATT_SERVER_MODE = "GATT Server"
|
||||||
|
|
||||||
const val SEND_SCENARIO = "Send"
|
const val SEND_SCENARIO = "Send"
|
||||||
const val RECEIVE_SCENARIO = "Receive"
|
const val RECEIVE_SCENARIO = "Receive"
|
||||||
@@ -47,8 +50,10 @@ class AppViewModel : ViewModel() {
|
|||||||
var mode by mutableStateOf(RFCOMM_SERVER_MODE)
|
var mode by mutableStateOf(RFCOMM_SERVER_MODE)
|
||||||
var scenario by mutableStateOf(RECEIVE_SCENARIO)
|
var scenario by mutableStateOf(RECEIVE_SCENARIO)
|
||||||
var peerBluetoothAddress by mutableStateOf(DEFAULT_PEER_BLUETOOTH_ADDRESS)
|
var peerBluetoothAddress by mutableStateOf(DEFAULT_PEER_BLUETOOTH_ADDRESS)
|
||||||
|
var startupDelay by mutableIntStateOf(DEFAULT_STARTUP_DELAY)
|
||||||
var l2capPsm by mutableIntStateOf(DEFAULT_PSM)
|
var l2capPsm by mutableIntStateOf(DEFAULT_PSM)
|
||||||
var use2mPhy by mutableStateOf(true)
|
var use2mPhy by mutableStateOf(true)
|
||||||
|
var connectionPriority by mutableStateOf("BALANCED")
|
||||||
var mtu by mutableIntStateOf(0)
|
var mtu by mutableIntStateOf(0)
|
||||||
var rxPhy by mutableIntStateOf(0)
|
var rxPhy by mutableIntStateOf(0)
|
||||||
var txPhy by mutableIntStateOf(0)
|
var txPhy by mutableIntStateOf(0)
|
||||||
@@ -98,6 +103,11 @@ class AppViewModel : ViewModel() {
|
|||||||
if (savedScenario != null) {
|
if (savedScenario != null) {
|
||||||
scenario = savedScenario
|
scenario = savedScenario
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val savedConnectionPriority = preferences.getString(CONNECTION_PRIORITY_PREF_KEY, null)
|
||||||
|
if (savedConnectionPriority != null) {
|
||||||
|
connectionPriority = savedConnectionPriority
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updatePeerBluetoothAddress(peerBluetoothAddress: String) {
|
fun updatePeerBluetoothAddress(peerBluetoothAddress: String) {
|
||||||
@@ -220,6 +230,14 @@ class AppViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateConnectionPriority(connectionPriority: String) {
|
||||||
|
this.connectionPriority = connectionPriority
|
||||||
|
with(preferences!!.edit()) {
|
||||||
|
putString(CONNECTION_PRIORITY_PREF_KEY, connectionPriority)
|
||||||
|
apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
status = ""
|
status = ""
|
||||||
lastError = ""
|
lastError = ""
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ package com.github.google.bumble.btbench
|
|||||||
import android.bluetooth.BluetoothSocket
|
import android.bluetooth.BluetoothSocket
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.ByteOrder
|
||||||
import java.util.logging.Logger
|
import java.util.logging.Logger
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
@@ -37,11 +38,16 @@ abstract class Packet(val type: Int, val payload: ByteArray = ByteArray(0)) {
|
|||||||
RESET -> ResetPacket()
|
RESET -> ResetPacket()
|
||||||
SEQUENCE -> SequencePacket(
|
SEQUENCE -> SequencePacket(
|
||||||
data[1].toInt(),
|
data[1].toInt(),
|
||||||
ByteBuffer.wrap(data, 2, 4).getInt(),
|
ByteBuffer.wrap(data, 2, 4).order(ByteOrder.LITTLE_ENDIAN).getInt(),
|
||||||
data.sliceArray(6..<data.size)
|
ByteBuffer.wrap(data, 6, 4).order(ByteOrder.LITTLE_ENDIAN).getInt(),
|
||||||
|
data.sliceArray(10..<data.size)
|
||||||
|
)
|
||||||
|
|
||||||
|
ACK -> AckPacket(
|
||||||
|
data[1].toInt(),
|
||||||
|
ByteBuffer.wrap(data, 2, 4).order(ByteOrder.LITTLE_ENDIAN).getInt()
|
||||||
)
|
)
|
||||||
|
|
||||||
ACK -> AckPacket(data[1].toInt(), ByteBuffer.wrap(data, 2, 4).getInt())
|
|
||||||
else -> GenericPacket(data[0].toInt(), data.sliceArray(1..<data.size))
|
else -> GenericPacket(data[0].toInt(), data.sliceArray(1..<data.size))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,16 +63,24 @@ class ResetPacket : Packet(RESET)
|
|||||||
|
|
||||||
class AckPacket(val flags: Int, val sequenceNumber: Int) : Packet(ACK) {
|
class AckPacket(val flags: Int, val sequenceNumber: Int) : Packet(ACK) {
|
||||||
override fun toBytes(): ByteArray {
|
override fun toBytes(): ByteArray {
|
||||||
return ByteBuffer.allocate(1 + 1 + 4).put(type.toByte()).put(flags.toByte())
|
return ByteBuffer.allocate(6).order(
|
||||||
|
ByteOrder.LITTLE_ENDIAN
|
||||||
|
).put(type.toByte()).put(flags.toByte())
|
||||||
.putInt(sequenceNumber).array()
|
.putInt(sequenceNumber).array()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SequencePacket(val flags: Int, val sequenceNumber: Int, payload: ByteArray) :
|
class SequencePacket(
|
||||||
|
val flags: Int,
|
||||||
|
val sequenceNumber: Int,
|
||||||
|
val timestamp: Int,
|
||||||
|
payload: ByteArray
|
||||||
|
) :
|
||||||
Packet(SEQUENCE, payload) {
|
Packet(SEQUENCE, payload) {
|
||||||
override fun toBytes(): ByteArray {
|
override fun toBytes(): ByteArray {
|
||||||
return ByteBuffer.allocate(1 + 1 + 4 + payload.size).put(type.toByte()).put(flags.toByte())
|
return ByteBuffer.allocate(10 + payload.size).order(ByteOrder.LITTLE_ENDIAN)
|
||||||
.putInt(sequenceNumber).put(payload).array()
|
.put(type.toByte()).put(flags.toByte())
|
||||||
|
.putInt(sequenceNumber).putInt(timestamp).put(payload).array()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ import java.util.logging.Logger
|
|||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
import kotlin.time.TimeSource
|
import kotlin.time.TimeSource
|
||||||
|
|
||||||
private const val DEFAULT_STARTUP_DELAY = 3000
|
|
||||||
|
|
||||||
private val Log = Logger.getLogger("btbench.pinger")
|
private val Log = Logger.getLogger("btbench.pinger")
|
||||||
|
|
||||||
class Pinger(private val viewModel: AppViewModel, private val packetIO: PacketIO) : IoClient,
|
class Pinger(private val viewModel: AppViewModel, private val packetIO: PacketIO) : IoClient,
|
||||||
@@ -36,8 +34,8 @@ class Pinger(private val viewModel: AppViewModel, private val packetIO: PacketIO
|
|||||||
override fun run() {
|
override fun run() {
|
||||||
viewModel.clear()
|
viewModel.clear()
|
||||||
|
|
||||||
Log.info("startup delay: $DEFAULT_STARTUP_DELAY")
|
Log.info("startup delay: ${viewModel.startupDelay}")
|
||||||
Thread.sleep(DEFAULT_STARTUP_DELAY.toLong());
|
Thread.sleep(viewModel.startupDelay.toLong());
|
||||||
Log.info("running")
|
Log.info("running")
|
||||||
|
|
||||||
Log.info("sending reset")
|
Log.info("sending reset")
|
||||||
@@ -48,19 +46,23 @@ class Pinger(private val viewModel: AppViewModel, private val packetIO: PacketIO
|
|||||||
|
|
||||||
val startTime = TimeSource.Monotonic.markNow()
|
val startTime = TimeSource.Monotonic.markNow()
|
||||||
for (i in 0..<packetCount) {
|
for (i in 0..<packetCount) {
|
||||||
val now = TimeSource.Monotonic.markNow()
|
var now = TimeSource.Monotonic.markNow()
|
||||||
val targetTime = startTime + (i * viewModel.senderPacketInterval).milliseconds
|
if (viewModel.senderPacketInterval > 0) {
|
||||||
val delay = targetTime - now
|
val targetTime = startTime + (i * viewModel.senderPacketInterval).milliseconds
|
||||||
if (delay.isPositive()) {
|
val delay = targetTime - now
|
||||||
Log.info("sleeping ${delay.inWholeMilliseconds} ms")
|
if (delay.isPositive()) {
|
||||||
Thread.sleep(delay.inWholeMilliseconds)
|
Log.info("sleeping ${delay.inWholeMilliseconds} ms")
|
||||||
|
Thread.sleep(delay.inWholeMilliseconds)
|
||||||
|
now = TimeSource.Monotonic.markNow()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pingTimes.add(TimeSource.Monotonic.markNow())
|
pingTimes.add(TimeSource.Monotonic.markNow())
|
||||||
packetIO.sendPacket(
|
packetIO.sendPacket(
|
||||||
SequencePacket(
|
SequencePacket(
|
||||||
if (i < packetCount - 1) 0 else Packet.LAST_FLAG,
|
if (i < packetCount - 1) 0 else Packet.LAST_FLAG,
|
||||||
i,
|
i,
|
||||||
ByteArray(packetSize - 6)
|
(now - startTime).inWholeMicroseconds.toInt(),
|
||||||
|
ByteArray(packetSize - 10)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
viewModel.packetsSent = i + 1
|
viewModel.packetsSent = i + 1
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
package com.github.google.bumble.btbench
|
package com.github.google.bumble.btbench
|
||||||
|
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
import java.util.logging.Logger
|
import java.util.logging.Logger
|
||||||
import kotlin.time.TimeSource
|
import kotlin.time.TimeSource
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ class Ponger(private val viewModel: AppViewModel, private val packetIO: PacketIO
|
|||||||
private var startTime: TimeSource.Monotonic.ValueTimeMark = TimeSource.Monotonic.markNow()
|
private var startTime: TimeSource.Monotonic.ValueTimeMark = TimeSource.Monotonic.markNow()
|
||||||
private var lastPacketTime: TimeSource.Monotonic.ValueTimeMark = TimeSource.Monotonic.markNow()
|
private var lastPacketTime: TimeSource.Monotonic.ValueTimeMark = TimeSource.Monotonic.markNow()
|
||||||
private var expectedSequenceNumber: Int = 0
|
private var expectedSequenceNumber: Int = 0
|
||||||
|
private val done = CountDownLatch(1)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
packetIO.packetSink = this
|
packetIO.packetSink = this
|
||||||
@@ -30,6 +32,7 @@ class Ponger(private val viewModel: AppViewModel, private val packetIO: PacketIO
|
|||||||
|
|
||||||
override fun run() {
|
override fun run() {
|
||||||
viewModel.clear()
|
viewModel.clear()
|
||||||
|
done.await()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun abort() {}
|
override fun abort() {}
|
||||||
@@ -58,5 +61,10 @@ class Ponger(private val viewModel: AppViewModel, private val packetIO: PacketIO
|
|||||||
|
|
||||||
packetIO.sendPacket(AckPacket(packet.flags, packet.sequenceNumber))
|
packetIO.sendPacket(AckPacket(packet.flags, packet.sequenceNumber))
|
||||||
viewModel.packetsSent += 1
|
viewModel.packetsSent += 1
|
||||||
|
|
||||||
|
if (packet.flags and Packet.LAST_FLAG != 0) {
|
||||||
|
Log.info("received last packet")
|
||||||
|
done.countDown()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
package com.github.google.bumble.btbench
|
package com.github.google.bumble.btbench
|
||||||
|
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
import java.util.logging.Logger
|
import java.util.logging.Logger
|
||||||
import kotlin.time.DurationUnit
|
import kotlin.time.DurationUnit
|
||||||
import kotlin.time.TimeSource
|
import kotlin.time.TimeSource
|
||||||
@@ -24,6 +25,7 @@ class Receiver(private val viewModel: AppViewModel, private val packetIO: Packet
|
|||||||
private var startTime: TimeSource.Monotonic.ValueTimeMark = TimeSource.Monotonic.markNow()
|
private var startTime: TimeSource.Monotonic.ValueTimeMark = TimeSource.Monotonic.markNow()
|
||||||
private var lastPacketTime: TimeSource.Monotonic.ValueTimeMark = TimeSource.Monotonic.markNow()
|
private var lastPacketTime: TimeSource.Monotonic.ValueTimeMark = TimeSource.Monotonic.markNow()
|
||||||
private var bytesReceived = 0
|
private var bytesReceived = 0
|
||||||
|
private val done = CountDownLatch(1)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
packetIO.packetSink = this
|
packetIO.packetSink = this
|
||||||
@@ -31,6 +33,7 @@ class Receiver(private val viewModel: AppViewModel, private val packetIO: Packet
|
|||||||
|
|
||||||
override fun run() {
|
override fun run() {
|
||||||
viewModel.clear()
|
viewModel.clear()
|
||||||
|
done.await()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun abort() {}
|
override fun abort() {}
|
||||||
@@ -62,6 +65,7 @@ class Receiver(private val viewModel: AppViewModel, private val packetIO: Packet
|
|||||||
Log.info("throughput: $throughput")
|
Log.info("throughput: $throughput")
|
||||||
viewModel.throughput = throughput
|
viewModel.throughput = throughput
|
||||||
packetIO.sendPacket(AckPacket(packet.flags, packet.sequenceNumber))
|
packetIO.sendPacket(AckPacket(packet.flags, packet.sequenceNumber))
|
||||||
|
done.countDown()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,11 +16,10 @@ package com.github.google.bumble.btbench
|
|||||||
|
|
||||||
import java.util.concurrent.Semaphore
|
import java.util.concurrent.Semaphore
|
||||||
import java.util.logging.Logger
|
import java.util.logging.Logger
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
import kotlin.time.DurationUnit
|
import kotlin.time.DurationUnit
|
||||||
import kotlin.time.TimeSource
|
import kotlin.time.TimeSource
|
||||||
|
|
||||||
private const val DEFAULT_STARTUP_DELAY = 3000
|
|
||||||
|
|
||||||
private val Log = Logger.getLogger("btbench.sender")
|
private val Log = Logger.getLogger("btbench.sender")
|
||||||
|
|
||||||
class Sender(private val viewModel: AppViewModel, private val packetIO: PacketIO) : IoClient,
|
class Sender(private val viewModel: AppViewModel, private val packetIO: PacketIO) : IoClient,
|
||||||
@@ -36,8 +35,8 @@ class Sender(private val viewModel: AppViewModel, private val packetIO: PacketIO
|
|||||||
override fun run() {
|
override fun run() {
|
||||||
viewModel.clear()
|
viewModel.clear()
|
||||||
|
|
||||||
Log.info("startup delay: $DEFAULT_STARTUP_DELAY")
|
Log.info("startup delay: ${viewModel.startupDelay}")
|
||||||
Thread.sleep(DEFAULT_STARTUP_DELAY.toLong());
|
Thread.sleep(viewModel.startupDelay.toLong());
|
||||||
Log.info("running")
|
Log.info("running")
|
||||||
|
|
||||||
Log.info("sending reset")
|
Log.info("sending reset")
|
||||||
@@ -47,20 +46,32 @@ class Sender(private val viewModel: AppViewModel, private val packetIO: PacketIO
|
|||||||
|
|
||||||
val packetCount = viewModel.senderPacketCount
|
val packetCount = viewModel.senderPacketCount
|
||||||
val packetSize = viewModel.senderPacketSize
|
val packetSize = viewModel.senderPacketSize
|
||||||
for (i in 0..<packetCount - 1) {
|
for (i in 0..<packetCount) {
|
||||||
packetIO.sendPacket(SequencePacket(0, i, ByteArray(packetSize - 6)))
|
var now = TimeSource.Monotonic.markNow()
|
||||||
|
if (viewModel.senderPacketInterval > 0) {
|
||||||
|
val targetTime = startTime + (i * viewModel.senderPacketInterval).milliseconds
|
||||||
|
val delay = targetTime - now
|
||||||
|
if (delay.isPositive()) {
|
||||||
|
Log.info("sleeping ${delay.inWholeMilliseconds} ms")
|
||||||
|
Thread.sleep(delay.inWholeMilliseconds)
|
||||||
|
}
|
||||||
|
now = TimeSource.Monotonic.markNow()
|
||||||
|
}
|
||||||
|
val flags = when (i) {
|
||||||
|
packetCount - 1 -> Packet.LAST_FLAG
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
packetIO.sendPacket(
|
||||||
|
SequencePacket(
|
||||||
|
flags,
|
||||||
|
i,
|
||||||
|
(now - startTime).inWholeMicroseconds.toInt(),
|
||||||
|
ByteArray(packetSize - 10)
|
||||||
|
)
|
||||||
|
)
|
||||||
bytesSent += packetSize
|
bytesSent += packetSize
|
||||||
viewModel.packetsSent = i + 1
|
viewModel.packetsSent = i + 1
|
||||||
}
|
}
|
||||||
packetIO.sendPacket(
|
|
||||||
SequencePacket(
|
|
||||||
Packet.LAST_FLAG,
|
|
||||||
packetCount - 1,
|
|
||||||
ByteArray(packetSize - 6)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
bytesSent += packetSize
|
|
||||||
viewModel.packetsSent = packetCount
|
|
||||||
|
|
||||||
// Wait for the ACK
|
// Wait for the ACK
|
||||||
Log.info("waiting for ACK")
|
Log.info("waiting for ACK")
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ disable = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[tool.pylint.main]
|
[tool.pylint.main]
|
||||||
ignore = "pandora" # FIXME: pylint does not support stubs yet:
|
ignore=["pandora", "mobly"] # FIXME: pylint does not support stubs yet
|
||||||
|
|
||||||
[tool.pylint.typecheck]
|
[tool.pylint.typecheck]
|
||||||
signature-mutators = "AsyncRunner.run_in_task"
|
signature-mutators = "AsyncRunner.run_in_task"
|
||||||
|
|||||||
Reference in New Issue
Block a user