improve DLC parameters

This commit is contained in:
Gilles Boccon-Gibod
2024-06-03 18:11:13 -07:00
parent f2dc8bd84e
commit f5baf51132
4 changed files with 204 additions and 78 deletions

View File

@@ -899,14 +899,26 @@ class L2capServer(StreamedPacketIO):
# RfcommClient # RfcommClient
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
class RfcommClient(StreamedPacketIO): class RfcommClient(StreamedPacketIO):
def __init__(self, device, channel, uuid, l2cap_mtu, max_frame_size, window_size): def __init__(
self,
device,
channel,
uuid,
l2cap_mtu,
max_frame_size,
initial_credits,
max_credits,
credits_threshold,
):
super().__init__() super().__init__()
self.device = device self.device = device
self.channel = channel self.channel = channel
self.uuid = uuid self.uuid = uuid
self.l2cap_mtu = l2cap_mtu self.l2cap_mtu = l2cap_mtu
self.max_frame_size = max_frame_size self.max_frame_size = max_frame_size
self.window_size = window_size self.initial_credits = initial_credits
self.max_credits = max_credits
self.credits_threshold = credits_threshold
self.rfcomm_session = None self.rfcomm_session = None
self.ready = asyncio.Event() self.ready = asyncio.Event()
@@ -940,12 +952,17 @@ class RfcommClient(StreamedPacketIO):
logging.info(color(f'### Opening session for channel {channel}...', 'yellow')) logging.info(color(f'### Opening session for channel {channel}...', 'yellow'))
try: try:
dlc_options = {} dlc_options = {}
if self.max_frame_size: if self.max_frame_size is not None:
dlc_options['max_frame_size'] = self.max_frame_size dlc_options['max_frame_size'] = self.max_frame_size
if self.window_size: if self.initial_credits is not None:
dlc_options['window_size'] = self.window_size dlc_options['initial_credits'] = self.initial_credits
rfcomm_session = await rfcomm_mux.open_dlc(channel, **dlc_options) rfcomm_session = await rfcomm_mux.open_dlc(channel, **dlc_options)
logging.info(color(f'### Session open: {rfcomm_session}', 'yellow')) logging.info(color(f'### Session open: {rfcomm_session}', 'yellow'))
if self.max_credits is not None:
rfcomm_session.rx_max_credits = self.max_credits
if self.credits_threshold is not None:
rfcomm_session.rx_credits_threshold = self.credits_threshold
except bumble.core.ConnectionError as error: except bumble.core.ConnectionError as error:
logging.info(color(f'!!! Session open failed: {error}', 'red')) logging.info(color(f'!!! Session open failed: {error}', 'red'))
await rfcomm_mux.disconnect() await rfcomm_mux.disconnect()
@@ -969,8 +986,19 @@ class RfcommClient(StreamedPacketIO):
# RfcommServer # RfcommServer
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
class RfcommServer(StreamedPacketIO): class RfcommServer(StreamedPacketIO):
def __init__(self, device, channel, l2cap_mtu): def __init__(
self,
device,
channel,
l2cap_mtu,
max_frame_size,
initial_credits,
max_credits,
credits_threshold,
):
super().__init__() super().__init__()
self.max_credits = max_credits
self.credits_threshold = credits_threshold
self.dlc = None self.dlc = None
self.ready = asyncio.Event() self.ready = asyncio.Event()
@@ -981,7 +1009,12 @@ class RfcommServer(StreamedPacketIO):
rfcomm_server = bumble.rfcomm.Server(device, **server_options) rfcomm_server = bumble.rfcomm.Server(device, **server_options)
# Listen for incoming DLC connections # Listen for incoming DLC connections
channel_number = rfcomm_server.listen(self.on_dlc, channel) dlc_options = {}
if max_frame_size is not None:
dlc_options['max_frame_size'] = max_frame_size
if initial_credits is not None:
dlc_options['initial_credits'] = initial_credits
channel_number = rfcomm_server.listen(self.on_dlc, channel, **dlc_options)
# Setup the SDP to advertise this channel # Setup the SDP to advertise this channel
device.sdp_service_records = make_sdp_records(channel_number) device.sdp_service_records = make_sdp_records(channel_number)
@@ -1004,6 +1037,10 @@ class RfcommServer(StreamedPacketIO):
dlc.sink = self.on_packet dlc.sink = self.on_packet
self.io_sink = dlc.write self.io_sink = dlc.write
self.dlc = dlc self.dlc = dlc
if self.max_credits is not None:
dlc.rx_max_credits = self.max_credits
if self.credits_threshold is not None:
dlc.rx_credits_threshold = self.credits_threshold
async def drain(self): async def drain(self):
assert self.dlc assert self.dlc
@@ -1321,7 +1358,9 @@ def create_mode_factory(ctx, default_mode):
uuid=ctx.obj['rfcomm_uuid'], uuid=ctx.obj['rfcomm_uuid'],
l2cap_mtu=ctx.obj['rfcomm_l2cap_mtu'], l2cap_mtu=ctx.obj['rfcomm_l2cap_mtu'],
max_frame_size=ctx.obj['rfcomm_max_frame_size'], max_frame_size=ctx.obj['rfcomm_max_frame_size'],
window_size=ctx.obj['rfcomm_window_size'], initial_credits=ctx.obj['rfcomm_initial_credits'],
max_credits=ctx.obj['rfcomm_max_credits'],
credits_threshold=ctx.obj['rfcomm_credits_threshold'],
) )
if mode == 'rfcomm-server': if mode == 'rfcomm-server':
@@ -1329,6 +1368,10 @@ def create_mode_factory(ctx, default_mode):
device, device,
channel=ctx.obj['rfcomm_channel'], channel=ctx.obj['rfcomm_channel'],
l2cap_mtu=ctx.obj['rfcomm_l2cap_mtu'], l2cap_mtu=ctx.obj['rfcomm_l2cap_mtu'],
max_frame_size=ctx.obj['rfcomm_max_frame_size'],
initial_credits=ctx.obj['rfcomm_initial_credits'],
max_credits=ctx.obj['rfcomm_max_credits'],
credits_threshold=ctx.obj['rfcomm_credits_threshold'],
) )
raise ValueError('invalid mode') raise ValueError('invalid mode')
@@ -1427,9 +1470,19 @@ def create_role_factory(ctx, default_role):
help='RFComm maximum frame size', help='RFComm maximum frame size',
) )
@click.option( @click.option(
'--rfcomm-window-size', '--rfcomm-initial-credits',
type=int, type=int,
help='RFComm window size', help='RFComm initial credits',
)
@click.option(
'--rfcomm-max-credits',
type=int,
help='RFComm max credits',
)
@click.option(
'--rfcomm-credits-threshold',
type=int,
help='RFComm credits threshold',
) )
@click.option( @click.option(
'--l2cap-psm', '--l2cap-psm',
@@ -1530,7 +1583,9 @@ def bench(
rfcomm_uuid, rfcomm_uuid,
rfcomm_l2cap_mtu, rfcomm_l2cap_mtu,
rfcomm_max_frame_size, rfcomm_max_frame_size,
rfcomm_window_size, rfcomm_initial_credits,
rfcomm_max_credits,
rfcomm_credits_threshold,
l2cap_psm, l2cap_psm,
l2cap_mtu, l2cap_mtu,
l2cap_mps, l2cap_mps,
@@ -1545,7 +1600,9 @@ def bench(
ctx.obj['rfcomm_uuid'] = rfcomm_uuid ctx.obj['rfcomm_uuid'] = rfcomm_uuid
ctx.obj['rfcomm_l2cap_mtu'] = rfcomm_l2cap_mtu ctx.obj['rfcomm_l2cap_mtu'] = rfcomm_l2cap_mtu
ctx.obj['rfcomm_max_frame_size'] = rfcomm_max_frame_size ctx.obj['rfcomm_max_frame_size'] = rfcomm_max_frame_size
ctx.obj['rfcomm_window_size'] = rfcomm_window_size ctx.obj['rfcomm_initial_credits'] = rfcomm_initial_credits
ctx.obj['rfcomm_max_credits'] = rfcomm_max_credits
ctx.obj['rfcomm_credits_threshold'] = rfcomm_credits_threshold
ctx.obj['l2cap_psm'] = l2cap_psm ctx.obj['l2cap_psm'] = l2cap_psm
ctx.obj['l2cap_mtu'] = l2cap_mtu ctx.obj['l2cap_mtu'] = l2cap_mtu
ctx.obj['l2cap_mps'] = l2cap_mps ctx.obj['l2cap_mps'] = l2cap_mps

View File

@@ -24,7 +24,7 @@ from typing import Optional
import click import click
from bumble.colors import color from bumble.colors import color
from bumble.device import Device, Connection from bumble.device import Device, DeviceConfiguration, Connection
from bumble import core from bumble import core
from bumble import hci from bumble import hci
from bumble import rfcomm from bumble import rfcomm
@@ -37,7 +37,8 @@ from bumble import utils
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
DEFAULT_RFCOMM_UUID = "E6D55659-C8B4-4B85-96BB-B1143AF6D3AE" DEFAULT_RFCOMM_UUID = "E6D55659-C8B4-4B85-96BB-B1143AF6D3AE"
DEFAULT_MTU = 4096 DEFAULT_MTU = 4096
DEFAULT_TCP_PORT = 9544 DEFAULT_CLIENT_TCP_PORT = 9544
DEFAULT_SERVER_TCP_PORT = 9545
TRACE_MAX_SIZE = 48 TRACE_MAX_SIZE = 48
@@ -149,7 +150,7 @@ class ServerBridge:
def on_disconnection(self, reason: int): def on_disconnection(self, reason: int):
print( print(
color("@@@ Bluetooth disconnection:", "blue"), color("@@@ Bluetooth disconnection:", "red"),
hci.HCI_Constant.error_name(reason), hci.HCI_Constant.error_name(reason),
) )
@@ -271,6 +272,7 @@ class ClientBridge:
self.address, transport=core.BT_BR_EDR_TRANSPORT self.address, transport=core.BT_BR_EDR_TRANSPORT
) )
print(color(f"@@@ Bluetooth connection: {self.connection}", "blue")) print(color(f"@@@ Bluetooth connection: {self.connection}", "blue"))
self.connection.on("disconnection", self.on_disconnection)
if self.encrypt: if self.encrypt:
print(color("@@@ Encrypting Bluetooth connection", "blue")) print(color("@@@ Encrypting Bluetooth connection", "blue"))
@@ -278,19 +280,16 @@ class ClientBridge:
print(color("@@@ Bluetooth connection encrypted", "blue")) print(color("@@@ Bluetooth connection encrypted", "blue"))
self.rfcomm_client = rfcomm.Client(self.connection) self.rfcomm_client = rfcomm.Client(self.connection)
self.rfcomm_mux = await self.rfcomm_client.start() try:
self.rfcomm_mux = await self.rfcomm_client.start()
def on_disconnection(reason): except BaseException as e:
print( print(color("!!! Failed to setup RFCOMM connection", "red"), e)
color("@@@ Bluetooth disconnection:", "red"), raise
hci.HCI_Constant.error_name(reason),
)
self.connection = None
self.connection.on("disconnection", on_disconnection)
async def start(self, device: Device) -> None: async def start(self, device: Device) -> None:
self.device = device self.device = device
await device.set_connectable(False)
await device.set_discoverable(False)
# Called when a TCP connection is established # Called when a TCP connection is established
async def on_tcp_connection(reader, writer): async def on_tcp_connection(reader, writer):
@@ -303,9 +302,15 @@ class ClientBridge:
return return
self.tcp_connected = True self.tcp_connected = True
await self.pipe(reader, writer) try:
writer.close() await self.pipe(reader, writer)
await writer.wait_closed() except BaseException as error:
print(color("!!! Exception while piping data:", "red"), error)
return
finally:
writer.close()
await writer.wait_closed()
self.tcp_connected = False
await asyncio.start_server( await asyncio.start_server(
on_tcp_connection, on_tcp_connection,
@@ -339,12 +344,12 @@ class ClientBridge:
# Connect a new RFCOMM channel # Connect a new RFCOMM channel
await self.connect() await self.connect()
assert self.rfcomm_mux assert self.rfcomm_mux
print(color(f'*** Opening RFCOMM channel {channel}', 'green')) print(color(f"*** Opening RFCOMM channel {channel}", "green"))
try: try:
rfcomm_channel = await self.rfcomm_mux.open_dlc(channel) rfcomm_channel = await self.rfcomm_mux.open_dlc(channel)
print(color(f'*** RFCOMM channel open: {rfcomm_channel}', "green")) print(color(f"*** RFCOMM channel open: {rfcomm_channel}", "green"))
except Exception as error: except Exception as error:
print(color(f'!!! RFCOMM open failed: {error}', 'red')) print(color(f"!!! RFCOMM open failed: {error}", "red"))
return return
# Pipe data from RFCOMM to TCP # Pipe data from RFCOMM to TCP
@@ -383,6 +388,13 @@ class ClientBridge:
print(color("~~~ Bye bye", "magenta")) print(color("~~~ Bye bye", "magenta"))
def on_disconnection(self, reason: int) -> None:
print(
color("@@@ Bluetooth disconnection:", "red"),
hci.HCI_Constant.error_name(reason),
)
self.connection = None
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
async def run(device_config, hci_transport, bridge): async def run(device_config, hci_transport, bridge):
@@ -393,7 +405,14 @@ async def run(device_config, hci_transport, bridge):
): ):
print("<<< connected") print("<<< connected")
device = Device.from_config_file_with_hci(device_config, hci_source, hci_sink) if device_config:
device = Device.from_config_file_with_hci(
device_config, hci_source, hci_sink
)
else:
device = Device.from_config_with_hci(
DeviceConfiguration(), hci_source, hci_sink
)
device.classic_enabled = True device.classic_enabled = True
# Let's go # Let's go
@@ -412,11 +431,28 @@ async def run(device_config, hci_transport, bridge):
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@click.group() @click.group()
@click.pass_context @click.pass_context
@click.option("--device-config", help="Device configuration file", required=True) @click.option(
@click.option("--hci-transport", help="HCI transport", required=True) "--device-config",
metavar="CONFIG_FILE",
help="Device configuration file",
)
@click.option(
"--hci-transport", metavar="TRANSPORT_NAME", help="HCI transport", required=True
)
@click.option("--trace", is_flag=True, help="Trace bridged data to stdout") @click.option("--trace", is_flag=True, help="Trace bridged data to stdout")
@click.option("--channel", help="RFCOMM channel number", type=int, default=0) @click.option(
@click.option("--uuid", help="UUID for the RFCOMM channel", default=DEFAULT_RFCOMM_UUID) "--channel",
metavar="CHANNEL_NUMER",
help="RFCOMM channel number",
type=int,
default=0,
)
@click.option(
"--uuid",
metavar="UUID",
help="UUID for the RFCOMM channel",
default=DEFAULT_RFCOMM_UUID,
)
def cli( def cli(
context, context,
device_config, device_config,
@@ -437,7 +473,7 @@ def cli(
@cli.command() @cli.command()
@click.pass_context @click.pass_context
@click.option("--tcp-host", help="TCP host", default="localhost") @click.option("--tcp-host", help="TCP host", default="localhost")
@click.option("--tcp-port", help="TCP port", default=DEFAULT_TCP_PORT) @click.option("--tcp-port", help="TCP port", default=DEFAULT_SERVER_TCP_PORT)
def server(context, tcp_host, tcp_port): def server(context, tcp_host, tcp_port):
bridge = ServerBridge( bridge = ServerBridge(
context.obj["channel"], context.obj["channel"],
@@ -454,7 +490,7 @@ def server(context, tcp_host, tcp_port):
@click.pass_context @click.pass_context
@click.argument("bluetooth-address") @click.argument("bluetooth-address")
@click.option("--tcp-host", help="TCP host", default="_") @click.option("--tcp-host", help="TCP host", default="_")
@click.option("--tcp-port", help="TCP port", default=DEFAULT_TCP_PORT) @click.option("--tcp-port", help="TCP port", default=DEFAULT_CLIENT_TCP_PORT)
@click.option("--encrypt", is_flag=True, help="Encrypt the connection") @click.option("--encrypt", is_flag=True, help="Encrypt the connection")
def client(context, bluetooth_address, tcp_host, tcp_port, encrypt): def client(context, bluetooth_address, tcp_host, tcp_port, encrypt):
bridge = ClientBridge( bridge = ClientBridge(

View File

@@ -833,7 +833,9 @@ class ClassicChannel(EventEmitter):
# Wait for the connection to succeed or fail # Wait for the connection to succeed or fail
try: try:
return await self.connection_result return await self.connection.abort_on(
'disconnection', self.connection_result
)
finally: finally:
self.connection_result = None self.connection_result = None
@@ -2226,7 +2228,7 @@ class ChannelManager:
# Connect # Connect
try: try:
await channel.connect() await channel.connect()
except Exception as e: except BaseException as e:
del connection_channels[source_cid] del connection_channels[source_cid]
raise e raise e

View File

@@ -106,9 +106,11 @@ CRC_TABLE = bytes([
0XBA, 0X2B, 0X59, 0XC8, 0XBD, 0X2C, 0X5E, 0XCF 0XBA, 0X2B, 0X59, 0XC8, 0XBD, 0X2C, 0X5E, 0XCF
]) ])
RFCOMM_DEFAULT_L2CAP_MTU = 2048 RFCOMM_DEFAULT_L2CAP_MTU = 2048
RFCOMM_DEFAULT_WINDOW_SIZE = 7 RFCOMM_DEFAULT_INITIAL_CREDITS = 7
RFCOMM_DEFAULT_MAX_FRAME_SIZE = 2000 RFCOMM_DEFAULT_MAX_CREDITS = 32
RFCOMM_DEFAULT_CREDIT_THRESHOLD = RFCOMM_DEFAULT_MAX_CREDITS // 2
RFCOMM_DEFAULT_MAX_FRAME_SIZE = 2000
RFCOMM_DYNAMIC_CHANNEL_NUMBER_START = 1 RFCOMM_DYNAMIC_CHANNEL_NUMBER_START = 1
RFCOMM_DYNAMIC_CHANNEL_NUMBER_END = 30 RFCOMM_DYNAMIC_CHANNEL_NUMBER_END = 30
@@ -365,12 +367,12 @@ class RFCOMM_MCC_PN:
ack_timer: int ack_timer: int
max_frame_size: int max_frame_size: int
max_retransmissions: int max_retransmissions: int
window_size: int initial_credits: int
def __post_init__(self) -> None: def __post_init__(self) -> None:
if self.window_size < 1 or self.window_size > 7: if self.initial_credits < 1 or self.initial_credits > 7:
logger.warning( logger.warning(
f'Error Recovery Window size {self.window_size} is out of range [1, 7].' f'Initial credits {self.initial_credits} is out of range [1, 7].'
) )
@staticmethod @staticmethod
@@ -382,7 +384,7 @@ class RFCOMM_MCC_PN:
ack_timer=data[3], ack_timer=data[3],
max_frame_size=data[4] | data[5] << 8, max_frame_size=data[4] | data[5] << 8,
max_retransmissions=data[6], max_retransmissions=data[6],
window_size=data[7] & 0x07, initial_credits=data[7] & 0x07,
) )
def __bytes__(self) -> bytes: def __bytes__(self) -> bytes:
@@ -396,7 +398,7 @@ class RFCOMM_MCC_PN:
(self.max_frame_size >> 8) & 0xFF, (self.max_frame_size >> 8) & 0xFF,
self.max_retransmissions & 0xFF, self.max_retransmissions & 0xFF,
# Only 3 bits are meaningful. # Only 3 bits are meaningful.
self.window_size & 0x07, self.initial_credits & 0x07,
] ]
) )
@@ -450,17 +452,21 @@ class DLC(EventEmitter):
self, self,
multiplexer: Multiplexer, multiplexer: Multiplexer,
dlci: int, dlci: int,
max_frame_size: int, tx_max_frame_size: int,
window_size: int, tx_initial_credits: int,
rx_max_frame_size: int,
rx_initial_credits: int,
) -> None: ) -> None:
super().__init__() super().__init__()
self.multiplexer = multiplexer self.multiplexer = multiplexer
self.dlci = dlci self.dlci = dlci
self.max_frame_size = max_frame_size self.rx_max_frame_size = rx_max_frame_size
self.window_size = window_size self.rx_initial_credits = rx_initial_credits
self.rx_credits = window_size self.rx_max_credits = RFCOMM_DEFAULT_MAX_CREDITS
self.rx_threshold = window_size // 2 self.rx_credits = rx_initial_credits
self.tx_credits = window_size self.rx_credits_threshold = RFCOMM_DEFAULT_CREDIT_THRESHOLD
self.tx_max_frame_size = tx_max_frame_size
self.tx_credits = tx_initial_credits
self.tx_buffer = b'' self.tx_buffer = b''
self.state = DLC.State.INIT self.state = DLC.State.INIT
self.role = multiplexer.role self.role = multiplexer.role
@@ -478,7 +484,7 @@ class DLC(EventEmitter):
# Compute the MTU # Compute the MTU
max_overhead = 4 + 1 # header with 2-byte length + fcs max_overhead = 4 + 1 # header with 2-byte length + fcs
self.mtu = min( self.mtu = min(
max_frame_size, self.multiplexer.l2cap_channel.peer_mtu - max_overhead tx_max_frame_size, self.multiplexer.l2cap_channel.peer_mtu - max_overhead
) )
@property @property
@@ -645,9 +651,9 @@ class DLC(EventEmitter):
cl=0xE0, cl=0xE0,
priority=7, priority=7,
ack_timer=0, ack_timer=0,
max_frame_size=self.max_frame_size, max_frame_size=self.rx_max_frame_size,
max_retransmissions=0, max_retransmissions=0,
window_size=self.window_size, initial_credits=self.rx_initial_credits,
) )
mcc = RFCOMM_Frame.make_mcc(mcc_type=MccType.PN, c_r=0, data=bytes(pn)) mcc = RFCOMM_Frame.make_mcc(mcc_type=MccType.PN, c_r=0, data=bytes(pn))
logger.debug(f'>>> PN Response: {pn}') logger.debug(f'>>> PN Response: {pn}')
@@ -655,8 +661,8 @@ class DLC(EventEmitter):
self.change_state(DLC.State.CONNECTING) self.change_state(DLC.State.CONNECTING)
def rx_credits_needed(self) -> int: def rx_credits_needed(self) -> int:
if self.rx_credits <= self.rx_threshold: if self.rx_credits <= self.rx_credits_threshold:
return self.window_size - self.rx_credits return self.rx_max_credits - self.rx_credits
return 0 return 0
@@ -749,7 +755,7 @@ class Multiplexer(EventEmitter):
connection_result: Optional[asyncio.Future] connection_result: Optional[asyncio.Future]
disconnection_result: Optional[asyncio.Future] disconnection_result: Optional[asyncio.Future]
open_result: Optional[asyncio.Future] open_result: Optional[asyncio.Future]
acceptor: Optional[Callable[[int], bool]] acceptor: Optional[Callable[[int], Optional[Tuple[int, int]]]]
dlcs: Dict[int, DLC] dlcs: Dict[int, DLC]
def __init__(self, l2cap_channel: l2cap.ClassicChannel, role: Role) -> None: def __init__(self, l2cap_channel: l2cap.ClassicChannel, role: Role) -> None:
@@ -761,6 +767,8 @@ class Multiplexer(EventEmitter):
self.connection_result = None self.connection_result = None
self.disconnection_result = None self.disconnection_result = None
self.open_result = None self.open_result = None
self.open_pn: Optional[RFCOMM_MCC_PN] = None
self.open_rx_max_credits = 0
self.acceptor = None self.acceptor = None
# Become a sink for the L2CAP channel # Become a sink for the L2CAP channel
@@ -869,9 +877,16 @@ class Multiplexer(EventEmitter):
else: else:
if self.acceptor: if self.acceptor:
channel_number = pn.dlci >> 1 channel_number = pn.dlci >> 1
if self.acceptor(channel_number): if dlc_params := self.acceptor(channel_number):
# Create a new DLC # Create a new DLC
dlc = DLC(self, pn.dlci, pn.max_frame_size, pn.window_size) dlc = DLC(
self,
dlci=pn.dlci,
tx_max_frame_size=pn.max_frame_size,
tx_initial_credits=pn.initial_credits,
rx_max_frame_size=dlc_params[0],
rx_initial_credits=dlc_params[1],
)
self.dlcs[pn.dlci] = dlc self.dlcs[pn.dlci] = dlc
# Re-emit the handshake completion event # Re-emit the handshake completion event
@@ -889,8 +904,17 @@ class Multiplexer(EventEmitter):
# Response # Response
logger.debug(f'>>> PN Response: {pn}') logger.debug(f'>>> PN Response: {pn}')
if self.state == Multiplexer.State.OPENING: if self.state == Multiplexer.State.OPENING:
dlc = DLC(self, pn.dlci, pn.max_frame_size, pn.window_size) assert self.open_pn
dlc = DLC(
self,
dlci=pn.dlci,
tx_max_frame_size=pn.max_frame_size,
tx_initial_credits=pn.initial_credits,
rx_max_frame_size=self.open_pn.max_frame_size,
rx_initial_credits=self.open_pn.initial_credits,
)
self.dlcs[pn.dlci] = dlc self.dlcs[pn.dlci] = dlc
self.open_pn = None
dlc.connect() dlc.connect()
else: else:
logger.warning('ignoring PN response') logger.warning('ignoring PN response')
@@ -928,7 +952,7 @@ class Multiplexer(EventEmitter):
self, self,
channel: int, channel: int,
max_frame_size: int = RFCOMM_DEFAULT_MAX_FRAME_SIZE, max_frame_size: int = RFCOMM_DEFAULT_MAX_FRAME_SIZE,
window_size: int = RFCOMM_DEFAULT_WINDOW_SIZE, initial_credits: int = RFCOMM_DEFAULT_INITIAL_CREDITS,
) -> DLC: ) -> DLC:
if self.state != Multiplexer.State.CONNECTED: if self.state != Multiplexer.State.CONNECTED:
if self.state == Multiplexer.State.OPENING: if self.state == Multiplexer.State.OPENING:
@@ -936,17 +960,19 @@ class Multiplexer(EventEmitter):
raise InvalidStateError('not connected') raise InvalidStateError('not connected')
pn = RFCOMM_MCC_PN( self.open_pn = RFCOMM_MCC_PN(
dlci=channel << 1, dlci=channel << 1,
cl=0xF0, cl=0xF0,
priority=7, priority=7,
ack_timer=0, ack_timer=0,
max_frame_size=max_frame_size, max_frame_size=max_frame_size,
max_retransmissions=0, max_retransmissions=0,
window_size=window_size, initial_credits=initial_credits,
) )
mcc = RFCOMM_Frame.make_mcc(mcc_type=MccType.PN, c_r=1, data=bytes(pn)) mcc = RFCOMM_Frame.make_mcc(
logger.debug(f'>>> Sending MCC: {pn}') mcc_type=MccType.PN, c_r=1, data=bytes(self.open_pn)
)
logger.debug(f'>>> Sending MCC: {self.open_pn}')
self.open_result = asyncio.get_running_loop().create_future() self.open_result = asyncio.get_running_loop().create_future()
self.change_state(Multiplexer.State.OPENING) self.change_state(Multiplexer.State.OPENING)
self.send_frame( self.send_frame(
@@ -1039,15 +1065,13 @@ class Client:
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
class Server(EventEmitter): class Server(EventEmitter):
acceptors: Dict[int, Callable[[DLC], None]]
def __init__( def __init__(
self, device: Device, l2cap_mtu: int = RFCOMM_DEFAULT_L2CAP_MTU self, device: Device, l2cap_mtu: int = RFCOMM_DEFAULT_L2CAP_MTU
) -> None: ) -> None:
super().__init__() super().__init__()
self.device = device self.device = device
self.multiplexer = None self.acceptors: Dict[int, Callable[[DLC], None]] = {}
self.acceptors = {} self.dlc_configs: Dict[int, Tuple[int, int]] = {}
# Register ourselves with the L2CAP channel manager # Register ourselves with the L2CAP channel manager
self.l2cap_server = device.create_l2cap_server( self.l2cap_server = device.create_l2cap_server(
@@ -1055,7 +1079,13 @@ class Server(EventEmitter):
handler=self.on_connection, handler=self.on_connection,
) )
def listen(self, acceptor: Callable[[DLC], None], channel: int = 0) -> int: def listen(
self,
acceptor: Callable[[DLC], None],
channel: int = 0,
max_frame_size: int = RFCOMM_DEFAULT_MAX_FRAME_SIZE,
initial_credits: int = RFCOMM_DEFAULT_INITIAL_CREDITS,
) -> int:
if channel: if channel:
if channel in self.acceptors: if channel in self.acceptors:
# Busy # Busy
@@ -1075,6 +1105,8 @@ class Server(EventEmitter):
return 0 return 0
self.acceptors[channel] = acceptor self.acceptors[channel] = acceptor
self.dlc_configs[channel] = (max_frame_size, initial_credits)
return channel return channel
def on_connection(self, l2cap_channel: l2cap.ClassicChannel) -> None: def on_connection(self, l2cap_channel: l2cap.ClassicChannel) -> None:
@@ -1092,15 +1124,14 @@ class Server(EventEmitter):
# Notify # Notify
self.emit('start', multiplexer) self.emit('start', multiplexer)
def accept_dlc(self, channel_number: int) -> bool: def accept_dlc(self, channel_number: int) -> Optional[Tuple[int, int]]:
return channel_number in self.acceptors return self.dlc_configs.get(channel_number)
def on_dlc(self, dlc: DLC) -> None: def on_dlc(self, dlc: DLC) -> None:
logger.debug(f'@@@ new DLC connected: {dlc}') logger.debug(f'@@@ new DLC connected: {dlc}')
# Let the acceptor know # Let the acceptor know
acceptor = self.acceptors.get(dlc.dlci >> 1) if acceptor := self.acceptors.get(dlc.dlci >> 1):
if acceptor:
acceptor(dlc) acceptor(dlc)
def __enter__(self) -> Self: def __enter__(self) -> Self: