BlueZ GATT Service Registration: Make D-Bus Object Lifetimes Match the Service Tree

Keep a local BlueZ GATT application’s exported object tree alive for the entire registration lifetime, and treat every new D-Bus owner as a new registration. Reusing the same object-path strings after process restart does not restore the previous application automatically.

Applicability: BlueZ 5.79 local GATT server APIs and the matching gatt-database.c implementation on the system bus. This is a proposed lifecycle design and test plan; no daemon, bus session or radio service was changed. Remote GATT cache migration and UUID evolution are separate concerns.

A hypothetical ObjectManager tree owned by bus name :1.50, with service, characteristic and descriptor nodes, and a new owner :1.73 requiring fresh registration.
Original object-ownership diagram. Example bus names and paths are fictional; exact service-node removal behavior is distinguished from the broader documented lifetime rule.

1. Define an application-owned tree

The GattManager contract groups local service, characteristic and descriptor objects beneath an ObjectManager root. Export the complete intended hierarchy and its Properties interfaces before asking BlueZ to register it.

A hypothetical root /com/example/app can contain service0, service0/char0 and service0/char0/desc0. Use explicit parent references: a characteristic’s Service property identifies its service, and a descriptor’s Characteristic property identifies its characteristic. Path naming alone is not the relationship contract.

Build a manifest containing each object path, interface, UUID, flags and parent reference. Check that every referenced parent exists in the same intended application tree. Keep unrelated management objects outside this ObjectManager scope so enumeration has a clear purpose.

The local GattService interface uses the application’s unique bus name and freely defined paths. Its optional Handle can request allocation, with zero requesting automatic allocation. Do not confuse that attribute handle with the D-Bus object path or use one as a substitute for the other.

2. Keep the event loop alive during registration

RegisterApplication is not a one-way declaration that BlueZ has already copied every object. The 5.79 implementation creates a client for the sender and root path, collects proxies, processes services before characteristics and descriptors, then returns success after adding the application to its database.

Therefore the application must continue servicing ObjectManager and Properties requests while registration is pending. Blocking the only D-Bus dispatch thread waiting for registration can prevent the very enumeration needed to finish it. Use the binding’s asynchronous completion path or another supported dispatch arrangement.

Maintain states such as EXPORTED, REGISTERING, REGISTERED and STOPPING. Set REGISTERED only after the successful method reply. A request that was sent but has no reply is still pending or uncertain, not a ready service.

On failure, retain the returned error and the exported manifest. The implementation can reject an empty or invalid tree. Do not respond by repeatedly registering an unchanged broken hierarchy without diagnosing the missing object or invalid property.

3. Preserve tree lifetime after success

The API documentation says later InterfacesAdded signals do not extend the registered service and warns against removing objects after registration. Treat the registered schema as stable. Ordinary value changes should use the characteristic’s supported value operations, not destroy and recreate its object.

There is an important source-level precision boundary. The documentation broadly describes removal-triggered unregistration; the inspected proxy_removed_cb specifically matches a service path and frees that service. It does not justify claiming that every characteristic removal immediately destroys the entire application.

Do not exploit that narrower implementation behavior as a dynamic-edit interface. Removing a child can leave BlueZ’s existing attribute definition referring to a missing application object. The supported engineering rule remains to keep all required nodes alive and perform deliberate unregister/re-register when replacing the hierarchy.

For a deterministic removal fixture, remove a service node and observe that service’s withdrawal. Test child removal separately as a prohibited-lifecycle diagnostic, without asserting the same cleanup outcome for all node types.

4. Bind registration to bus ownership

BlueZ identifies the application by both its system-bus connection identity and application path. Its disconnect callback removes the departed owner’s application. A restarted process with the same root path but a new unique bus name is a different owner.

For example, hypothetical owner :1.50 exporting /com/example/app and replacement owner :1.73 exporting that same path are two registration lifetimes. Keep that distinction in logs and callback generations. The numbers are illustrative bus names, not captured identifiers.

Within one owner, a duplicate registration for the same root can return AlreadyExists. Do not interpret that response as proof that a newly intended schema replaced the old one. Reconcile the current registration state before issuing more mutations.

A daemon restart is a separate loss of the server-side registration. Monitor the availability/owner of org.bluez, reacquire the desired adapter’s GattManager interface and register the still-valid exported tree through one recovery owner. Do not let several independent retry loops race to register it.

5. Make shutdown and replacement explicit

For planned shutdown, stop accepting new application work, resolve or cancel outstanding method handling under the application’s contract, and call UnregisterApplication with the same root through the owning bus connection. Keep objects available until the result or confirmed owner loss establishes the boundary.

After successful unregistration, unexport the tree and release its storage. If the bus or daemon disappears during shutdown, distinguish that from a successful explicit unregister reply. The final product state may be stopped either way, but the diagnostic history should remain accurate.

For a schema replacement, validate the complete replacement manifest before withdrawing the old application. Decide how the product exposes the resulting service gap. Advertising is separately managed; a successful GATT registration does not by itself publish an advertising instance.

Re-establish application readiness after the new registration and any separately required advertising step. Avoid a window in which discovery promises a service whose methods are not yet available.

6. Test lifecycle faults with observable outcomes

Proposed test Expected evidence
Complete tree before registration Successful reply and remote service operation
Missing parent or invalid property Registration failure with preserved manifest
Application bus disconnect Old owner’s services withdrawn
Same path under new owner Fresh registration, no inherited ready state
Service-node InterfacesRemoved That service removed under the tagged implementation
BlueZ daemon restart One controlled re-registration after manager availability

Also test registration while the application’s dispatch loop is busy, cancellation during pending registration and a late completion callback from an older generation. Bound retries and retain stage timing instead of reducing every failure to “service missing.”

Acceptance requires a matching application manifest, bus owner, successful registration generation and usable service methods. That ownership record explains why a process restart or object teardown can remove a service even though its UUID and path strings never changed.

Similar Posts