Ultra-wide band communication

Ultra-wide band (UWB) communication is a radio technology focused on precise ranging (measuring the location to an accuracy of 10 cm) between devices. This radio technology can use a low-energy density for short range measurements and perform high-bandwidth signaling over a large portion of the radio spectrum. UWB’s bandwidth is greater than 500 MHz (or exceeding 20% fractional bandwidth).

Controller vs controlee

UWB communication occurs between two devices, where one is a controller and the other is a controlee. The controller determines the complex channel (UwbComplexChannel) that the two devices will share. A controller can handle multiple controlees, but a controlee can only subscribe to a single controller.

Channel selection

For the controlee to start ranging to the controller, the controlee must identify the controller’s local address and complex channel. The local address provides context on the device of the controller, while the complex channel provides context on the ranging session. Both the local address and complex channel rotate after the ranging session ends.

We recommend using Bluetooth Low Energy (BLE) communication to allow the controlee to learn the controller’s local address and complex channel.

Code sample

This code sample initiates and terminates UWB ranging for a controlee:

// The coroutineScope responsible for handling uwb ranging.
// This will be initialized when startRanging is called.
var job: Job?

// A code snippet that initiates uwb ranging for a controlee.
suspend fun startRanging() {

    // Get the ranging parameter of a partnering controller using an OOB mechanism of choice.
    val partnerAddress : Pair<UwbAddress, UwbComplexChannel> = listenForPartnersAddress()

    // Create the ranging parameters.
    val partnerParameters = RangingParameters(
        uwbConfigType = UwbRangingParamters.UWB_CONFIG_ID_1,
        // SessionKeyInfo is used to encrypt the ranging session.
        sessionKeyInfo = null,
        complexChannel = partnerAddress.second,
        peerDevices = listOf(UwbDevice.createForAddress(partnerAddress.first)),
        updateRateType = UwbRangingParamters.RANGING_UPDATE_RATE_AUTOMATIC
    )

    // Initiate a session that will be valid for a single ranging session.
    val clientSession = uwbManager.clientSessionScope()

    // Share the localAddress of the current session to the partner device.
    broadcastMyParameters(clientSession.localAddress)

    val sessionFlow = clientSession.prepareSession(partnerParameters)

    // Start a coroutine scope that initiates ranging.
    CoroutineScope(Dispatchers.Main.immediate).launch {
        sessionFlow.collect {
            when(it) {
                is RangingResultPosition -> doSomethingWithPosition(it.position)
                is RangingResultPeerDisconnected -> peerDisconnected(it)
            }
        }
    }
}

// A code snippet that cancels uwb ranging.
fun cancelRanging() {

    // Canceling the CoroutineScope will stop the ranging.
    job?.let {
        it.cancel()
    }
}

UWB API

To use the UWB API, follow these steps:

  1. Ensure the device is running on Android 12 or higher.
  2. Ensure the device supports UWB using PackageManager#hasSystemFeature("android.hardware.uwb").
  3. Ensure the IoT device (or other side) is FiRa MAC 1.0 compliant.
  4. Recommended: Discover UWB capable peer devices using an out-of-band (OOB) mechanism, such as BluetoothLeScanner for BLE scanning.
  5. Recommended: Exchange the local device's address and peer device's address and complex channel to use for the session using an OOB mechanism, such as BluetoothGatt for a BLE GATT connection.
  6. If the user wants to stop the session, cancel the scope of the session.

RxJava3 support

Rxjava3 support is now available to help achieve interoperability with Java clients. This library provides a way to get ranging results as an Observable or Flowable stream, and to retrieve the UwbClientSessionScope as a Single object.

private final UwbManager uwbManager;

// Retrieve uwbManager.clientSessionScope as a Single object
Single<UwbClientSessionScope> clientSessionScopeSingle =
                UwbManagerRx.clientSessionScopeSingle(uwbManager);
UwbClientSessionScope uwbClientSessionScope = clientSessionScopeSingle.blockingGet();

// Retrieve uwbClientSessionScope.prepareSession Flow as an Observable object
Observable<RangingResult> rangingResultObservable =
                UwbClientSessionScopeRx.rangingResultsObservable(clientSessionScope,
                        rangingParameters);

// Consume ranging results from Observable
rangingResultObservable.subscribe(
   rangingResult -> doSomethingWithRangingResult(result), // onNext
   (error) -> doSomethingWithError(error), // onError
   () -> doSomethingOnResultEventsCompleted(), //onCompleted
);
// Unsubscribe
rangingResultObservable.unsubscribe();
   

// Retrieve uwbClientSessionScope.prepareSession Flow as a Flowable object
Flowable<RangingResult> rangingResultFlowable =
                UwbClientSessionScopeRx.rangingResultsFlowable(clientSessionScope,
                        rangingParameters);

// Consume ranging results from Flowable using Disposable
Disposable disposable = rangingResultFlowable
   .delay(1, TimeUnit.SECONDS)
   .subscribeWith(new DisposableSubscriber<RangingResult> () {
      @Override public void onStart() {
          request(1);
      }
      
      @Override public void onNext(RangingResult rangingResult) {
             doSomethingWithRangingResult(rangingResult);
             request(1);
      }


      @Override public void onError(Throwable t) {
             t.printStackTrace();
      }


         @Override public void onComplete() {
            doSomethingOnEventsCompleted();
         }
   });

// Stop subscription
disposable.dispose();