BlueZ AcquireWrite and AcquireNotify: Own the File Descriptor Across Disconnects

Treat an acquired GATT descriptor as a connection-scoped resource with one owner, a negotiated size context and an explicit close path. On HUP, retire that acquisition; after reconnect, obtain a new descriptor and MTU rather than reviving the old integer.

Applicability: BlueZ 5.79 remote/client GattCharacteristic1 AcquireWrite and AcquireNotify, with the matching gatt-client.c implementation. Local/server implementations are outside this procedure. All descriptor-reuse and disconnect fixtures are hypothetical; no D-Bus method or Bluetooth connection was exercised.

Acquired-interface lifecycle with asynchronous acquisition, HUP retirement, API locks and a hypothetical reused fd 9 across two generations.
Original resource-lifetime diagram for BlueZ remote/client APIs. Descriptor numbers and tests are illustrative; no acquired socket or radio throughput was measured.

1. Choose one interface for each direction

The BlueZ API makes AcquireWrite and AcquireNotify optional. Check the characteristic’s supported properties and flags instead of assuming every writable or notifiable characteristic exposes the acquired path.

AcquireWrite is tied to write-without-response capability and locks ordinary WriteValue. AcquireNotify enables the notification path itself and locks StartNotify. Close the acquired descriptor to release its lock. Calling both styles concurrently is not a harmless way to improve reliability.

Owned acquisition Use Avoid
AcquireWrite Write through the returned socket under its size contract Concurrent WriteValue on that characteristic
AcquireNotify Read notifications through the returned socket Calling StartNotify or waiting for Value updates for that acquired stream
No acquisition Use the chosen ordinary D-Bus API Using a descriptor retained from an earlier connection

During acquired notifications, the Value property is not updated by that notification path. Keep the consumer attached to the socket; a property observer that goes quiet is not evidence that the peripheral stopped transmitting.

2. Model acquisition as asynchronous ownership transfer

Use states UNACQUIRED, ACQUIRING, ACQUIRED and RETIRING. Store connection generation, characteristic identity, direction, returned descriptor, returned MTU and event-loop watch identity together.

The method may wait for GATT readiness or MTU exchange. The tagged implementation keeps a pending method message when the client is not ready. Do not mark the path usable when the request is merely submitted.

On a successful reply, verify that the request still belongs to the current generation. If the user cancelled or the connection changed while acquisition was pending, close the newly received descriptor through the binding’s ownership rules instead of attaching it to a stale session.

Define who owns a descriptor received through D-Bus. Some language bindings expose a wrapper or require an explicit take/duplicate operation. Follow that binding’s documented lifetime and ensure exactly one application component closes the owned reference.

If acquisition fails, preserve the precise error and remain unacquired. A fallback to ordinary methods must be deliberate, after confirming no acquired lock remains. Do not launch simultaneous acquisition retries from several consumers.

3. Preserve socket records and negotiated limits

BlueZ 5.79 create_sock uses a local nonblocking SOCK_SEQPACKET socket pair. This is a record-oriented path, not an arbitrary byte stream where splitting or concatenating writes is automatically harmless. Define each submitted record’s characteristic-value meaning.

Retain the returned MTU and derive the allowed value length from the exact ATT operation and implementation. The MTU is not a universal application-payload allowance. Recheck the rule after reconnect and reject an oversized record before submission rather than hoping the socket fragments it into valid GATT writes.

Receive into storage sized for the qualified record limit, detect truncation using the chosen socket API, and reject truncated values. A partial-looking application structure is not repaired by joining it to the next notification.

The inspected write path submits a write-without-response operation. A successful local socket write is therefore not proof that the peer application committed a command. Use a separate application result contract when that distinction matters.

The notification forwarding callback logs a failed socket send; it does not implement an unlimited reliable backlog. Keep the consumer responsive, bound its own queue and use sequence/age diagnostics to expose losses. The acquired interface does not eliminate backpressure.

4. Retire the descriptor on HUP or owner loss

The documented disconnect behavior produces HUP, and descriptors are closed across reconnection because MTU must be renegotiated. Stop new I/O, remove the event-loop watch, invalidate the acquisition record and close the application-owned descriptor once.

If readable data and HUP arrive together, define whether buffered records are drained as old-generation diagnostics or discarded. Never label them as fresh data from a replacement connection. Keep product state changes gated by the generation and freshness policy.

Descriptor numbers can be reused. In a hypothetical fixture, generation 7 owns fd 9, closes it, and generation 8 later receives fd 9. A delayed generation-7 callback must not close or write generation 8’s descriptor merely because the integer matches.

Use a generation-aware event-loop context and mark ownership retired before cleanup can trigger another callback. Also handle daemon disappearance, object removal and application shutdown through the same idempotent retirement path.

5. Reacquire only after the new connection is ready

Resolve the current characteristic object, confirm supported properties and required security, then issue one new acquisition. Store the newly returned MTU even if the numeric value matches the previous one.

For notifications, do not call StartNotify as a preliminary step. The acquired operation owns that setup. In the exact 5.79 client implementation, an existing acquired notify socket produces NotPermitted, while an existing ordinary notify-client list produces InProgress during AcquireNotify.

Those source-specific outcomes are useful diagnostics, not reasons to stop another process’s subscription without coordination. Decide which component owns the characteristic’s consumer interface and report a conflict when that ownership cannot be established.

For writes, resume only records permitted by the application’s retry semantics. A disconnect after local submission leaves some operation outcomes uncertain; blindly replaying every queued control command can duplicate an action.

6. Test ownership, exclusion and reconnection

  • AcquireWrite, then call WriteValue: verify the expected API conflict without leaking the descriptor.
  • AcquireNotify and confirm notifications arrive through the socket while the acquired path leaves Value unchanged.
  • Start ordinary notifications first, then attempt AcquireNotify: preserve the tagged implementation’s conflict result.
  • Disconnect during acquisition and after acquisition: verify late-reply cleanup and HUP retirement.
  • Force descriptor-number reuse in a software fixture: verify old callbacks cannot affect the replacement.
  • Reconnect with a different negotiated MTU: verify value limits are rebuilt.
  • Slow the socket consumer: verify bounded memory and visible record loss rather than a delivery guarantee.

Acceptance requires one live owner per acquired direction, no use after retirement, correct interface exclusion and a fresh acquisition for every connection generation. Keep socket delivery, ATT behavior and application completion as separate observations.

Similar Posts