Using UWB on Android
This post is purely concerned with the Android implementation. For Android–iOS interop you cpould use a combination of NINearbyAccessoryConfiguration on iOS and BLE GATT using the Nordic UART Service (6E400001-B5A3-F393-E0A9-E50E24DCCA9E) on Android.
Source code for this example project is available at https://github.com/CrazyChaoz/Minimal-Android-UWB-App.
Setup
Bill of materials
You will need
- a UWB-capable Android phone, and
- another UWB device for communication, which, for simplicity, can be a second phone.
A list of potential devices can be found e.g. on Wikipedia.
Dependencies
- The main UWB library:
androidx.core.uwb:uwb:1.0.0-alpha08 - Since this project is implemented entirely in Java, we also need compatibility dependencies for rxjava3:
androidx.core.uwb:uwb-rxjava3:1.0.0-alpha08
UI
This implementation focuses on functionality over aesthetics, so we use a minimalistic UI for simplicity.
Implementation
Concept
Using UWB on Android involves 6 essential steps:
- Initialize the UWB middleware.
- Select a role: either as a controller or controllee.
- Start a session.
- Use an out-of-band medium to transmit session confguration parameters.
- Connect with the partner device.
- Begin communication.
Code
Step 1 (View in Source)
The initialization is done via
UwbManager uwbManager = UwbManager.createInstance(this);
The uwbManager object is used in all further interactions with the middleware.
The creation of an UwbManager instance also takes an instance of type context.
In our case this is already our app context.
Steps 2 and 3 (View in Source)
Role selection depends on the use-case. Most phone-to-accessory connections need a specific setup, but for this demo the only necessity is that both a controller and a controllee are present.
This selection is done via a simple android.widget.Switch that switches an AtomicReference<UwbClientSessionScope> from UwbManagerRx.controllerSessionScopeSingle(uwbManager).blockingGet() to UwbManagerRx.controleeSessionScopeSingle(uwbManager).blockingGet() and back.
Before switching the scope, potentially existing ranging sessions get killed, since they might still continue to run in the background otherwise, and this is a simple 1:1 implementation.
Step 4 (View in Source)
Before you can communicate with a partner, you need to know how to reach them. In UWB there is no in-band session configuration defined, so you need to use arbitrary out-of-band mechanisms. This is commonly done via Bluetooth, but in our case a simpler approach is taken: user interaction.
Accessing the previously created UwbClientSessionScope, we can gather the necessary information and present it to the user, who has to input the data on the partner device.
The relevant information is:
- the UWB channel (this can be set statically):
controllerSessionScope.getUwbComplexChannel().getChannel(), - the channel preamble:
controllerSessionScope.getUwbComplexChannel().getPreambleIndex(), - your address for the partner:
controllerSessionScope.getLocalAddress().getAddress(), and - if special keys are set, these will also need to be transmitted.
To better transmit the UwbAddress we convert the data (a ByteArray) into a short integer.
For the most basic setup the controllee needs the address of the controller and the channel preamble index, and the controller.
Step 5 (View in Source)
After the values have been transferred to the partner device, you can set up the actual communication.
At first, you need to establish the RangingParameters that define the ranging session.
Here parameters like the channel and the partner address(es), together with other UWB related parameters are set.
The communication happens as soon as the UwbClientSessionScopeRx.rangingResultsObservable(currentUwbSessionScope.get(), partnerParameters) is called, but it is not doing anything as long as there is no subscriber registered to that observable.
Step 6 (View in Source)
The actual data from the connection can be accessed in a callback that receives a androidx.core.uwb.RangingResult.
This callback is set as a subscriber to the previously defined observable and consumes all new ranging events.
The RangingResult can either be of type RangingResultPosition where actual position data is stored, or RangingResultPeerDisconnected where the UwbDevice that has disconnected is shown.