Class BlePsFtpClient

  • All Implemented Interfaces:

    
    public final class BlePsFtpClient
    extends BleGattBase
                        

    Polar simple file transfer client declaration.

    Operations (request, query, write, sendNotification) can suspend for up to 90 seconds while waiting for BLE packets from the device. Without an early-exit path, a BLE disconnection would leave those coroutines blocked on the internal channels — holding operationMutex and preventing any subsequent operation from starting.

    To solve this, every operation participates in a two-sided race via Kotlin's select:

    withTimeoutOrNull(timeoutMillis) {
        select {
            channel.onReceive { ... }           // normal path: BLE packet arrives
            disconnectSignal.onAwait { ... }    // fast path: disconnect fires
        }
    }

    disconnectSignal is a snapshot of disconnectDeferred taken at the start of each operation, before the mutex is acquired. When reset is called on disconnection:

    • disconnectDeferred is replaced with a fresh CompletableDeferred for the next connection.

    • The old deferred is completed exceptionally with BleDisconnected.

    Any coroutine suspended in onAwait on the old deferred is immediately unblocked: because the deferred completed exceptionally, onAwait re-throws BleDisconnected rather than executing its lambda. That exception propagates through withTimeoutOrNull (which only catches TimeoutCancellationException), out of the suspend helper, and up to operationMutex.withLock, whose finally block releases the lock — freeing the next waiting operation immediately.

    Capture-before-lock ordering prevents a race: even if reset runs between the snapshot (val disconnectSignal = disconnectDeferred) and the first suspension point, the old deferred is already completed exceptionally, so onAwait throws the moment the select starts.