Skip to content

Lifetimes

Everything you open comes from an async with block, and is closed when the block ends:

async with aio_rfcomm.open_bluetooth() as bluetooth:
    async with bluetooth.open_service(DEVICE, SERIAL_PORT) as channel:
        ...

Using it after its block has ended is a bug, and raises ScopeClosedError:

async with bluetooth.open_service(DEVICE, SERIAL_PORT) as channel:
    ...

await channel.send(b"hello\n")  # raises ScopeClosedError

Noticing that a connection has ended

A task waiting in receive finds out straight away when the connection ends, because receive raises. A task waiting for anything else, such as keyboard input, a queue or a timer, does not find out. It can wait forever for a device that has gone.

So keep a task receiving in the same task group, even if your program does not expect anything from the device. When the connection ends, that task raises, and the task group cancels the other tasks:

async with asyncio.TaskGroup() as group:
    group.create_task(print_received(channel))
    group.create_task(send_typed_lines(channel))

A task group raises an ExceptionGroup, even when only one task failed. Catch the group in the function that made the task group, and raise the channel's error by itself:

try:
    async with asyncio.TaskGroup() as group:
        ...
except ExceptionGroup as failed:
    endings, others = failed.split(ChannelClosedError)
    if endings is not None and others is None:
        raise endings.exceptions[0] from None
    raise

Anything else in the group is a bug, so it goes on up unchanged.

When Bluetooth stops being usable, receive and send raise BluetoothUnavailableError, so the same task group notices that too.

Cancellation

Canceling an operation stops your program waiting for it. It does not always stop the operation itself, because some Bluetooth operations underneath cannot be called off. The library does what each platform allows, such as closing the connection.

Keep these in mind before putting a timeout around anything.

The device may still connect. After you give up on connecting, the connection may still complete on the device's side. Retrying straight away can then fail, because a device allows only one connection per channel. Wait before retrying. On Windows, the library lets the connection finish and then hangs up, and your next open of that channel waits until it has.

Do not use asyncio.shield. A shielded call keeps running after you stop waiting for it. A shielded receive, for example, still holds the channel, so your next receive raises BusyResourceError.

Canceling send loses nothing. send queues all its bytes before it waits. If it is canceled, the bytes are still sent, in order, unless the connection ends first. Sending them again after a timeout sends them twice.

To be sure everything has stopped, leave the block

A timeout only stops you waiting. Leaving the async with block closes everything opened inside it.

Next: Errors, for what each error means.