From aa1d7933dae8f62d6d75f22cf23c812d2831e24b Mon Sep 17 00:00:00 2001 From: Gilles Boccon-Gibod Date: Thu, 25 Sep 2025 18:31:14 +0200 Subject: [PATCH] enhance serial port transport --- bumble/host.py | 2 +- bumble/transport/android_netsim.py | 6 ++- bumble/transport/serial.py | 57 +++++++++++++++++++++++++++- docs/mkdocs/src/transports/serial.md | 17 +++++++-- 4 files changed, 74 insertions(+), 8 deletions(-) diff --git a/bumble/host.py b/bumble/host.py index 66b5ed8..bc8a792 100644 --- a/bumble/host.py +++ b/bumble/host.py @@ -550,7 +550,7 @@ class Host(utils.EventEmitter): logger.debug( 'HCI LE flow control: ' f'le_acl_data_packet_length={le_acl_data_packet_length},' - f'total_num_le_acl_data_packets={total_num_le_acl_data_packets}' + f'total_num_le_acl_data_packets={total_num_le_acl_data_packets},' f'iso_data_packet_length={iso_data_packet_length},' f'total_num_iso_data_packets={total_num_iso_data_packets}' ) diff --git a/bumble/transport/android_netsim.py b/bumble/transport/android_netsim.py index 2575814..3565380 100644 --- a/bumble/transport/android_netsim.py +++ b/bumble/transport/android_netsim.py @@ -131,7 +131,11 @@ def publish_grpc_port(grpc_port: int, instance_number: int) -> bool: def cleanup(): logger.debug("removing .ini file") - ini_file.unlink() + try: + ini_file.unlink() + except OSError as error: + # Don't log at exception level, since this may happen normally. + logger.debug(f'failed to remove .ini file ({error})') atexit.register(cleanup) return True diff --git a/bumble/transport/serial.py b/bumble/transport/serial.py index 7c8c14c..dca498f 100644 --- a/bumble/transport/serial.py +++ b/bumble/transport/serial.py @@ -28,25 +28,56 @@ from bumble.transport.common import StreamPacketSink, StreamPacketSource, Transp logger = logging.getLogger(__name__) +# ----------------------------------------------------------------------------- +# Constants +# ----------------------------------------------------------------------------- +DEFAULT_POST_OPEN_DELAY = 0.5 # in seconds + +# ----------------------------------------------------------------------------- +# Classes and Functions +# ----------------------------------------------------------------------------- + + +# ----------------------------------------------------------------------------- +class SerialPacketSource(StreamPacketSource): + def __init__(self) -> None: + super().__init__() + self._ready = asyncio.Event() + + async def wait_until_ready(self) -> None: + await self._ready.wait() + + def connection_made(self, transport: asyncio.BaseTransport) -> None: + logger.debug('connection made') + self._ready.set() + + def connection_lost(self, exc: Exception | None) -> None: + logger.debug('connection lost') + self.on_transport_lost() + + # ----------------------------------------------------------------------------- async def open_serial_transport(spec: str) -> Transport: ''' Open a serial port transport. The parameter string has this syntax: - [,][,rtscts][,dsrdtr] + [,][,rtscts][,dsrdtr][,delay] When is omitted, the default value of 1000000 is used When "rtscts" is specified, RTS/CTS hardware flow control is enabled When "dsrdtr" is specified, DSR/DTR hardware flow control is enabled + When "delay" is specified, a short delay is added after opening the port Examples: /dev/tty.usbmodem0006839912172 /dev/tty.usbmodem0006839912172,1000000 /dev/tty.usbmodem0006839912172,rtscts + /dev/tty.usbmodem0006839912172,rtscts,delay ''' speed = 1000000 rtscts = False dsrdtr = False + delay = 0.0 if ',' in spec: parts = spec.split(',') device = parts[0] @@ -55,13 +86,16 @@ async def open_serial_transport(spec: str) -> Transport: rtscts = True elif part == 'dsrdtr': dsrdtr = True + elif part == 'delay': + delay = DEFAULT_POST_OPEN_DELAY elif part.isnumeric(): speed = int(part) else: device = spec + serial_transport, packet_source = await serial_asyncio.create_serial_connection( asyncio.get_running_loop(), - StreamPacketSource, + SerialPacketSource, device, baudrate=speed, rtscts=rtscts, @@ -69,4 +103,23 @@ async def open_serial_transport(spec: str) -> Transport: ) packet_sink = StreamPacketSink(serial_transport) + logger.debug('waiting for the port to be ready') + await packet_source.wait_until_ready() + logger.debug('port is ready') + + # Try to assert DTR + assert serial_transport.serial is not None + try: + serial_transport.serial.dtr = True + logger.debug( + f"DSR={serial_transport.serial.dsr}, DTR={serial_transport.serial.dtr}" + ) + except Exception as e: + logger.warning(f'could not assert DTR: {e}') + + # Wait a bit after opening the port, if requested + if delay > 0.0: + logger.debug(f'waiting {delay} seconds after opening the port') + await asyncio.sleep(delay) + return Transport(packet_source, packet_sink) diff --git a/docs/mkdocs/src/transports/serial.md b/docs/mkdocs/src/transports/serial.md index acc2c0d..4752066 100644 --- a/docs/mkdocs/src/transports/serial.md +++ b/docs/mkdocs/src/transports/serial.md @@ -4,9 +4,18 @@ SERIAL TRANSPORT The serial transport implements sending/receiving HCI packets over a UART (a.k.a serial port). ## Moniker -The moniker syntax for a serial transport is: `serial:[,]` -When `` is omitted, the default value of 1000000 is used +The moniker syntax for a serial transport is: + `[,][,rtscts][,dsrdtr][,delay]` + +When `` is omitted, the default value of 1000000 is used. +When `rtscts` is specified, RTS/CTS hardware flow control is enabled. +When `dsrdtr` is specified, DSR/DTR hardware flow control is enabled. +When `delay` is specified, a short delay is added after opening the port. !!! example - `serial:/dev/tty.usbmodem0006839912172,1000000` - Opens the serial port `/dev/tty.usbmodem0006839912172` at `1000000`bps + ``` + /dev/tty.usbmodem0006839912172 + /dev/tty.usbmodem0006839912172,1000000 + /dev/tty.usbmodem0006839912172,rtscts + /dev/tty.usbmodem0006839912172,rtscts,delay + ``` \ No newline at end of file