SimpleBLE

Read, Write, Notify

Perform GATT operations after connecting to a BLE peripheral.

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;
uint8_t* data = NULL;
size_t data_length = 0;

simpleble_peripheral_read(peripheral, service_uuid, characteristic_uuid, &data, &data_length);

for (size_t i = 0; i < data_length; i++) {
    printf("%02X ", data[i]);
}
printf("\n");

simpleble_free(data);
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);
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);
uint8_t payload[] = {0x01, 0x02, 0x03};

simpleble_peripheral_write_request(
    peripheral,
    service_uuid,
    characteristic_uuid,
    payload,
    sizeof(payload));

simpleble_peripheral_write_command(
    peripheral,
    service_uuid,
    characteristic_uuid,
    payload,
    sizeof(payload));
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();
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 data_length,
                      void* userdata) {
    for (size_t i = 0; i < data_length; i++) {
        printf("%02X ", data[i]);
    }
    printf("\n");
}

simpleble_peripheral_notify(peripheral, service_uuid, characteristic_uuid, on_notify, NULL);

/* Keep the process alive while notifications arrive. */

simpleble_peripheral_unsubscribe(peripheral, service_uuid, characteristic_uuid);
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();
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.

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