SimpleBLE

Read, Write, Notify

Perform GATT operations after connecting to a BLE peripheral.

The C examples include <stdio.h> and <simplecble/simplecble.h> and borrow the peripheral handle passed to them. See the C tutorial for acquisition and cleanup.

GATT operations happen after a peripheral is connected and services have been discovered. Most applications use the same shape:

  1. Select a service UUID and characteristic UUID.
  2. Check the characteristic capability.
  3. Read, write, subscribe, or unsubscribe.
  4. Keep the process, activity, or coroutine alive long enough for notifications or indications to arrive.

The snippets below assume you already have a connected peripheral, a service_uuid, and a characteristic_uuid.

Read

auto value = peripheral.read(service_uuid, characteristic_uuid);
std::cout << "Read: " << value << std::endl;
bool print_value(simpleble_peripheral_t peripheral,
                 simpleble_uuid_t service, simpleble_uuid_t characteristic) {
    simpleble_error_t* error = NULL;
    size_t length = 0;
    uint8_t* data = simpleble_peripheral_read(peripheral, service, characteristic, &length, &error);
    bool success = error == NULL;
    if (error) {
        fprintf(stderr, "%s\n", simpleble_error_message(error));
    } else {
        for (size_t i = 0; i < length; i++) printf("%02X ", data[i]);
        printf("\n");
    }
    simpleble_free(data);
    simpleble_error_release(&error);
    return success;
}
value = peripheral.read(service_uuid, characteristic_uuid)
print(f"Read: {value}")
BluetoothUUID service = new BluetoothUUID(serviceUuid);
BluetoothUUID characteristic = new BluetoothUUID(characteristicUuid);

byte[] value = peripheral.read(service, characteristic);
System.out.println("Read: " + bytesToHex(value));
let value = peripheral.read(&service_uuid, &characteristic_uuid).unwrap();
println!("Read: {:?}", value);
const value = await peripheral.read(serviceUuid, characteristicUuid);
console.log('Read:', value);
const value = await peripheral.read(serviceUuid, characteristicUuid);
console.log('Read:', value);
val value = peripheral.read(serviceUuid, characteristicUuid)
println("Read: ${value.toHexString()}")
val value = peripheral.read(serviceUuid, characteristicUuid)
Log.d("SimpleBLE", "Read: ${value.joinToString(" ") { "%02x".format(it) }}")

Write

SimpleBLE::ByteArray payload = SimpleBLE::ByteArray::fromHex("010203");

// Acknowledged write.
peripheral.write_request(service_uuid, characteristic_uuid, payload);

// Unacknowledged write.
peripheral.write_command(service_uuid, characteristic_uuid, payload);
bool write_value(simpleble_peripheral_t peripheral,
                 simpleble_uuid_t service, simpleble_uuid_t characteristic) {
    simpleble_error_t* error = NULL;
    const uint8_t payload[] = {0x01, 0x02, 0x03};
    simpleble_peripheral_write_request(peripheral, service, characteristic,
                                      payload, sizeof(payload), &error);
    bool success = error == NULL;
    if (error) fprintf(stderr, "%s\n", simpleble_error_message(error));
    simpleble_error_release(&error);
    return success;
}
payload = bytes([0x01, 0x02, 0x03])

# Acknowledged write.
peripheral.write_request(service_uuid, characteristic_uuid, payload)

# Unacknowledged write.
peripheral.write_command(service_uuid, characteristic_uuid, payload)
byte[] payload = new byte[] {0x01, 0x02, 0x03};

peripheral.writeRequest(service, characteristic, payload);
peripheral.writeCommand(service, characteristic, payload);
let payload = vec![0x01, 0x02, 0x03];

peripheral.write_request(&service_uuid, &characteristic_uuid, &payload).unwrap();
peripheral.write_command(&service_uuid, &characteristic_uuid, &payload).unwrap();
const payload = Buffer.from([0x01, 0x02, 0x03]);

// Acknowledged write.
await peripheral.writeRequest(serviceUuid, characteristicUuid, payload);

// Unacknowledged write.
await peripheral.writeCommand(serviceUuid, characteristicUuid, payload);
const payload = Uint8Array.of(0x01, 0x02, 0x03);

// Acknowledged write.
await peripheral.writeRequest(serviceUuid, characteristicUuid, payload);

// Unacknowledged write.
await peripheral.writeCommand(serviceUuid, characteristicUuid, payload);
val payload = byteArrayOf(0x01, 0x02, 0x03)

// Acknowledged write.
peripheral.writeRequest(serviceUuid, characteristicUuid, payload)

// Unacknowledged write.
peripheral.writeCommand(serviceUuid, characteristicUuid, payload)
val payload = byteArrayOf(0x01, 0x02, 0x03)

peripheral.writeRequest(serviceUuid, characteristicUuid, payload)
peripheral.writeCommand(serviceUuid, characteristicUuid, payload)

Notify

peripheral.notify(service_uuid, characteristic_uuid, [](SimpleBLE::ByteArray payload) {
    std::cout << "Notification: " << payload << std::endl;
});

std::this_thread::sleep_for(std::chrono::seconds(10));

peripheral.unsubscribe(service_uuid, characteristic_uuid);
static void on_notify(simpleble_peripheral_t peripheral,
                      simpleble_uuid_t service, simpleble_uuid_t characteristic,
                      const uint8_t* data, size_t length, void* userdata) {
    for (size_t i = 0; i < length; i++) printf("%02X ", data[i]);
    printf("\n");
}

bool receive_notifications(simpleble_peripheral_t peripheral,
                           simpleble_uuid_t service, simpleble_uuid_t characteristic) {
    simpleble_error_t* error = NULL;
    simpleble_peripheral_notify(peripheral, service, characteristic, on_notify, NULL, &error);
    if (error) {
        fprintf(stderr, "%s\n", simpleble_error_message(error));
        simpleble_error_release(&error);
        return false;
    }

    printf("Press Enter to unsubscribe.\n");
    getchar();
    simpleble_peripheral_unsubscribe(peripheral, service, characteristic, &error);
    bool success = error == NULL;
    if (error) fprintf(stderr, "%s\n", simpleble_error_message(error));
    simpleble_error_release(&error);
    return success;
}
import time

peripheral.notify(
    service_uuid,
    characteristic_uuid,
    lambda data: print(f"Notification: {data}"),
)

time.sleep(10)

peripheral.unsubscribe(service_uuid, characteristic_uuid)
peripheral.notify(service, characteristic, data -> {
    System.out.println("Notification: " + bytesToHex(data));
});

Thread.sleep(10_000);

peripheral.unsubscribe(service, characteristic);
let mut stream = peripheral.notify(&service_uuid, &characteristic_uuid).unwrap();

tokio::spawn(async move {
    while let Some(Ok(event)) = stream.next().await {
        if let simplersble::ValueChangedEvent::ValueUpdated(data) = event {
            println!("Notification: {:?}", data);
        }
    }
});

std::thread::sleep(std::time::Duration::from_secs(10));

peripheral.unsubscribe(&service_uuid, &characteristic_uuid).unwrap();
import { setTimeout as sleep } from 'node:timers/promises';

await peripheral.notify(serviceUuid, characteristicUuid, data => console.log('Notification:', data));

await sleep(10_000);

await peripheral.unsubscribe(serviceUuid, characteristicUuid);
await peripheral.notify(serviceUuid, characteristicUuid, data => console.log('Notification:', data));

await new Promise<void>(resolve => setTimeout(() => resolve(), 10_000));

await peripheral.unsubscribe(serviceUuid, characteristicUuid);
// Collecting subscribes. When the timeout ends the collection, the flow unsubscribes.
withTimeoutOrNull(10_000) {
    peripheral.notify(serviceUuid, characteristicUuid).collect { payload ->
        println("Notification: ${payload.toHexString()}")
    }
}
withTimeoutOrNull(10_000) {
    peripheral.notify(serviceUuid, characteristicUuid).collect { payload ->
        Log.d("SimpleBLE", "Notification: ${payload.joinToString(" ") { "%02x".format(it) }}")
    }
}

On Android, collecting the flow subscribes to the characteristic. When collection is cancelled or completes, SimpleDroidBLE unsubscribes and waits for native cleanup to finish. The timeout above is only a compact way to demonstrate a ten-second subscription.

With SimpleKtBLE, collecting the flow subscribes and ending the collection unsubscribes in the same way. When the peripheral disconnects, the flow completes, and you collect it again after reconnecting.

Production Android apps

Choose the subscription lifetime based on what the app promises:

  • For data needed only while a screen is visible, collect inside repeatOnLifecycle. Leaving the screen cancels collection and unsubscribes; returning starts a new subscription.
  • For a connection that must survive screen changes, let a ViewModel or connection controller own the collection job and expose parsed values as UI state. Cancel and join that job before disconnecting, selecting another peripheral, or subscribing again.
  • A notification flow does not reconnect automatically. After reconnecting, refresh services and collect a new flow.
  • Serialize connect, read, write, subscribe, and disconnect actions so taps and lifecycle events cannot start conflicting GATT operations.
  • Surface subscription failures through UI state. Cancellation is normal lifecycle cleanup and should not be shown as an error.

A screen-scoped subscription can stay small:

lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        peripheral.notify(serviceUuid, characteristicUuid).collect { payload ->
            viewModel.onNotification(payload)
        }
    }
}

If notifications must continue while the app is not visible, a ViewModel is not sufficient. Follow Android's background BLE guidance and use a companion-device service or a connectedDevice foreground service as appropriate.

Common pitfalls

  • Use write_request / writeRequest when you need the peripheral to acknowledge the write.
  • Use write_command / writeCommand only when the characteristic supports write-without-response.
  • Subscribe only to characteristics that advertise notify or indicate capability.
  • Keep the process, activity, or coroutine alive while notifications are active.
  • Unsubscribe before disconnecting when your workflow has a clear end. On Android, cancel and join the collector to unsubscribe automatically.
  • On Android, only one notify or indicate collector may be active for a characteristic. Cancel and join it before resubscribing.

On this page