Skip to content
CrossFly is 100% open sourceFollow us on GitHub
CrossFlyCrossFly
Get started

This page was machine-translated from Portuguese and may contain errors. The Portuguese version is canonical.

Bluetooth BLE / GATT

CrossFly uses an asynchronous BLE/GATT stack (dev.crossfly.device.bluetooth), which replaced the old JSR-82/SPP. The modeling is based on CompletableFuture.

⚠️ Honesty note (read before coding)

In the core, several operations are a skeleton over in-memory state — BluetoothConnectionBuilder.build() creates empty streams, GattCharacteristic.read()/write() only touch the local cache, and discoverServices() returns an empty list. The real radio transport comes from the platform launchers (Android/iOS). The modeling (UUIDs, futures, service→characteristic→descriptor hierarchy) is already final; rely on the API, but test on a real device per platform.

Discovery and connection

BluetoothManager bt = BluetoothManager.getInstance();   // throws IOException

// scan (asynchronous)
bt.scanForDevices(10, TimeUnit.SECONDS, new BluetoothManager.ScanCallback() {
   public void onSuccess(List<BluetoothDevice> devices) { /* choose one */ }
   public void onError(String error) { Debug.log(error); }
});

// connect (asynchronous) — returns a BluetoothConnection
bt.connect(device, new BluetoothManager.ConnectionCallback() {
   public void onSuccess(BluetoothConnection conn) { usar(conn); }
   public void onError(String error) { Debug.log(error); }
});

There are *Sync variants (scanForDevicesSync, connectSync) — use them only on a worker thread, never on the paint thread.

GATT — read the battery and subscribe to notifications

service → characteristic → descriptor model, I/O via CompletableFuture:

device.discoverServices(services -> {
   GattService bateria = device.getService("0000180F-0000-1000-8000-00805F9B34FB");
   GattCharacteristic nivel = bateria.getCharacteristic("00002A19-0000-1000-8000-00805F9B34FB");

   nivel.read().thenAccept(bytes -> {
      int pct = bytes[0] & 0xFF;
      Component.onUI(() -> label.setText(pct + "%"));
   });

   nivel.enableNotifications(bytes ->                     // subscribes to changes
      Component.onUI(() -> label.setText((bytes[0] & 0xFF) + "%")));
});

GattCharacteristic exposes read() → CompletableFuture<byte[]>, write(byte[]) → CompletableFuture<Void>, enableNotifications(Consumer<byte[]>), and the capability checks canRead()/canWrite()/canNotify().

BluetoothConnection is AutoCloseable

try (BluetoothConnection conn = bt.connectSync(device)) {   // only on a worker thread
   conn.write("PING".getBytes());
   byte[] resp = conn.read();
}   // closes on its own

Best practices

  • Async on the UI (connect(...)), *Sync only on a worker.
  • Callbacks/futures return to the UI with Component.onUI(...).
  • try-with-resources on the BluetoothConnection.
  • Test on a real device per platform (the core is modeling; the radio lives in the launcher).

Common mistakes

  • Calling *Sync variants on the paint thread (blocks the UI).
  • Expecting the core to do real radio I/O (it’s a skeleton; the transport belongs to the platform).
  • Touching the UI inside thenAccept/the callback without onUI(...).

See also

Was this page helpful?