SimpleBLE

Examples

Code examples for SimpleBluez.

To learn how to use SimpleBluez, please refer to the examples provided in the repository.

For controllers that support extended advertising, the advertisement secondary channel can be selected before registering the advertisement:

auto advertisement = bluez.root_custom()->advertisement_add("reader");
advertisement->secondary_channel("2M");
adapter->register_advertisement(advertisement);

Use adapter->supported_secondary_channels() to check whether BlueZ reports support for "2M" on the selected adapter.

Local GATT read/write options

When exposing local GATT server objects, BlueZ passes per-client context in the ReadValue and WriteValue options dictionary. The callbacks take Characteristic::ValueOptions, which exposes fields such as device, mtu, offset, link, type, and prepare-authorize as prepare_authorize in C++:

characteristic->set_on_read_value([characteristic](SimpleBluez::Characteristic::ValueOptions options) {
    if (options.mtu.has_value()) {
        std::cout << "Read MTU: " << *options.mtu << std::endl;
    }

    characteristic->value(SimpleBluez::ByteArray("hello"));
});

characteristic->set_on_write_value(
    [](SimpleBluez::ByteArray value, SimpleBluez::Characteristic::ValueOptions options) {
        if (options.mtu.has_value()) {
            std::cout << "Write MTU: " << *options.mtu << std::endl;
        }
    });

For user-generated/local GATT server characteristics, characteristic->mtu() reflects the BlueZ GattCharacteristic1.MTU property and should not be treated as a per-device server-side MTU. BlueZ provides the per-request server MTU in ValueOptions::mtu for ReadValue and WriteValue callbacks when that field is present.

Local GATT notify acquisition capability

By default, local GATT characteristics do not expose BlueZ's optional NotifyAcquired property, so BlueZ uses the StartNotify and StopNotify flow:

characteristic->set_on_notify([](bool notifying) {
    std::cout << "Notifying: " << notifying << std::endl;
});

The NotifyAcquired property can be exported or removed at runtime:

struct NotifyClient {
    SimpleDBus::UnixSocket socket;
    SimpleBluez::Characteristic::ValueOptions options;
};

std::vector<NotifyClient> notify_clients;

characteristic->set_on_acquire_notify(
    [&notify_clients](SimpleDBus::UnixSocket socket, SimpleBluez::Characteristic::ValueOptions options) {
        std::cout << "Notify MTU: " << options.mtu.value_or(0) << std::endl;

        notify_clients.emplace_back(NotifyClient{std::move(socket), options});
    });

characteristic->enable_acquire_notify();

// Future BlueZ notification subscriptions can now discover NotifyAcquired.

characteristic->disable_acquire_notify();

enable_acquire_notify() exports NotifyAcquired with a value of false. disable_acquire_notify() invalidates the property, allowing BlueZ to fall back to StartNotify and StopNotify for future subscriptions. Existing acquired sockets are not closed automatically.

Acquired notify socket ownership

Each acquired socket is per BlueZ AcquireNotify call. SimpleBluez creates a connected Unix socket pair, returns one end to BlueZ, and passes the application-owned end to the callback. The application must move and store that socket if it wants the acquired notify stream to remain alive after the callback returns. If the socket is destroyed at the end of the callback, its file descriptor is closed and BlueZ treats the acquired stream as released.

The socket passed to the callback is move-only. socket.fd() returns a borrowed descriptor for polling or integration with an event loop; do not close that raw descriptor directly while UnixSocket still owns it. Use socket.close() to release the acquired stream, or socket.release() only when transferring the raw descriptor to another owner that will close it.

Writing to the socket sends notification or indication payloads through BlueZ. SimpleBluez creates acquired notify sockets in non-blocking mode, so send() can write fewer bytes than requested or fail with EAGAIN / EWOULDBLOCK. User code should poll socket.fd() for writability and retry as needed. Use options.mtu as the negotiated MTU context for that acquired stream. SimpleBluez does not chunk payloads, enforce MTU limits, remove closed sockets from user containers, or track the lifetime of user-owned sockets after the callback returns.

For indications, poll socket.fd() for readability and call socket.receive() to read confirmation bytes from BlueZ. A hangup or unrecoverable read/write error means the acquired notify stream is gone; remove that socket from your container and close it if it is still valid. Disabling acquire notify only removes the optional NotifyAcquired property for future subscriptions; it does not close sockets already handed to the application.

The MTU in options.mtu belongs to the acquired socket delivered in the same callback. It is stable for that socket's lifetime. Store the MTU alongside the socket, use it while writing to that socket, and discard both when the socket is closed or reports an unrecoverable I/O error. Do not expect SimpleBluez to update the MTU for a socket that has already been handed to application code.

When a central stops notifications or indications, BlueZ closes its end of the acquired notify socket. SimpleBluez does not receive a separate StopNotify callback for that fd-backed path. The application should detect the end of the stream by polling the socket fd or by handling send() / receive() failures:

// Requires <cerrno>.
void notify_all(std::vector<NotifyClient>& clients, const SimpleBluez::ByteArray& payload) {
    for (auto it = clients.begin(); it != clients.end();) {
        auto& client = *it;
        uint16_t mtu = client.options.mtu.value_or(23);
        size_t max_payload = mtu > 3 ? mtu - 3 : 0;

        if (payload.size() > max_payload) {
            // Split or drop oversized payloads; SimpleBluez does not chunk them.
            ++it;
            continue;
        }

        ssize_t bytes_sent = client.socket.send(payload.data(), payload.size());
        if (bytes_sent == static_cast<ssize_t>(payload.size())) {
            ++it;
            continue;
        }

        if (bytes_sent < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
            // The non-blocking socket is not ready yet. Poll for POLLOUT and retry later.
            ++it;
            continue;
        }

        // HUP, disconnect, central unsubscribe, or another hard socket error.
        it = clients.erase(it);
    }
}

For event-loop based code, monitor each socket.fd() for POLLHUP, POLLERR, or POLLNVAL and remove that NotifyClient when any of those flags appear. Monitor for POLLOUT before retrying a payload that previously failed with EAGAIN / EWOULDBLOCK. For indications, also monitor for POLLIN and call socket.receive() to read BlueZ's confirmation byte.

On this page