Uno Platform and Bluetooth LE: An Easy-to-Follow Guide to Building a Bluetooth Explorer

Writing a cross-platform application with Uno Platform gives us access to the native capabilities of each platform. Using a cross-platform API, we can work directly with these capabilities from platform-agnostic code. One such example is Bluetooth Low Energy which allows our app to interact with a wide range of low-energy sensors and peripherals.

Concept

We will build an application which allows the user to select a nearby Bluetooth device and query the functionality available. This will provide an introduction you can apply when building for a specific scenario. For this sample, we are using 32feet.NET, which is a cross-platform API for working with Bluetooth. It exposes an API which will be familiar if you’ve ever used Web Bluetooth. In this way, you don’t need to worry about the specifics of the various implementations on different platforms. Currently, the library doesn’t support Web Assembly, but that will be available in a future version.

Prerequisites

Adding the library to our shared Uno project is just a case of adding the InTheHand.BluetoothLE package from NuGet. Then, there are a couple of platform-specific requirements to set up permissions. 

On iOS, we need to add the NSBluetoothAlwaysUsageDescription key to the info.plist with a concise message explaining why we need the Bluetooth functionality. This message is displayed in a system dialog when the Bluetooth functionality is first called, and the user can choose to allow or deny the request. We must be aware of this and only proceed to use Bluetooth functionality if permission is granted; otherwise, it will fail. 

				
					<key>NSBluetoothAlwaysUsageDescription</key> 
<string>This app requires Bluetooth to demonstrate the Bluetooth LE APIs</string> 
				
			

If we want to target devices earlier than iOS 13, we must also add the NSBluetoothPeripheralUsageDescription key. We can copy the same string for both. 

On Android, there are two layers – we must add the required permissions into the AndroidManifest.xml for the app. 

				
					<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/> 

<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" /> 

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" /> 

<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation"/> 

<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> 
				
			

The first line is optional but indicates that the app requires Bluetooth LE and will mean the app cannot be installed on a device without that capability. The following two items are permissions required for Bluetooth and device discovery on versions prior to Android 12. For Android 12 and above, the second pair of permissions are required. There is a special flag to set on BLUETOOTH_SCAN to request scan permission while not using it for physical location – so we don’t need to request location permission for these newer devices. 

Secondly, when targeting Android 10 and above, we must request permission at runtime. One of the interesting things about this process is that the permission will be automatically granted without a user prompt in many cases. The user can, of course, disable the permission later from the app settings. Because there is no prompt for the basic Bluetooth permission (the same is not true for other permissions), this can be called when our application starts up – so we can add an override to OnCreate in the MainActivity.Android.cs in the Android folder of the mobile project. 

				
					protected override void OnCreate(Bundle bundle) 
{ 
   base.OnCreate(bundle); 
   
   RequestBluetoothPermissions(); 
}

private void RequestBluetoothPermissions() 
{ 
   if (Android.OS.Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.M) 
   { 
      if (CheckSelfPermission(Manifest.Permission.Bluetooth) != Permission.Granted) 
      { 
         if (ShouldShowRequestPermissionRationale(Manifest.Permission.Bluetooth)) 
         { 
            AlertDialog.Builder builder = new AlertDialog.Builder(this); 
            builder.SetTitle("Permission") 
               .SetMessage("This app needs Bluetooth permissions to connect"); 
            builder.Create().Show(); 
         } 

         RequestPermissions(new string[] { Manifest.Permission.Bluetooth }, 1); 
      } 
   } 
} 
				
			

The first check ensures that this code is not run on older devices where it is not required. The second checks to see if the permission is already granted. Then we follow a standard pattern for requesting Android permissions. We ask the system if we should display an information message showing why we want permission. This is similar to the iOS mechanism, where we put a message in the manifest. After this message (if required), we call RequestPermissions. Android will then display a prompt, if required, to ask the user and give them a chance to allow or deny the request. 

For WinUI and macOS, the app is given complete trust, so we don’t have to do additional work to ask for permission. 

Shared Code

Now that the mobile platforms have their required permissions, the rest of the logic can be written entirely in the shared project. The UI comprises a button to initiate the connection process and a list control to display the discovered services and characteristics. We achieve this using a CollectionViewSource configured to allow a grouped source. To create this grouped collection, we have a couple of custom classes – BluetoothCharacteristic contains the key properties from each characteristic we discover. In addition, BluetoothService inherits ObservableCollection of BluetoothCharacteristic to contain all child items and Name property. 

The ListView uses a custom GroupStyle.HeaderTemplate, which binds to the Name property of the BluetoothService. The groups within the list will be data bound to BluetoothCharacteristic objects. Therefore, the ItemTemplate defines a layout and binds to Name, Properties and Value properties. 

The Click handler for the button contains the logic to connect and enumerate the services. The 32feet.NET API mirrors Web Bluetooth, and where you would use “navigator.bluetooth” in Javascript, there is a static Bluetooth class containing the main methods. 

It does this by first calling Bluetooth.GetAvailabilityAsync(). This will return true if Bluetooth is available and ready to use. Possible reasons for returning false include a device with no Bluetooth radio (rare), permission explicitly denied by the user or system, or the radio turned off (e.g., via flight mode). Next Bluetooth.RequestDeviceAsync() displays a platform-specific dialog to allow the user to choose an available device. 

Figure 1: Bluetooth picker on iOS
Figure 2: Bluetooth picker on Android

Note: if your app is reusing the same device, you don’t need to use this method each time – every BluetoothDevice has a unique platform-specific Id string which you can store and use to retrieve a device directly using BluetoothDevice.FromIdAsync(). 

Although, if successful, we now have a BluetoothDevice object, it’s not yet ready to use – we need to access the Gatt property for its RemoteGattServer and then call ConnectAsync. This process can take a while; the exact time depends on the platform and environmental factors. However, once the awaited method completes, we can check the IsConnected property to ensure it was successful. 

Now we can begin to enumerate the hierarchy of services and characteristics. GetPrimaryServicesAsync() will return all of the publicly available services. Each service has a unique UUID – this is either a 128-bit Guid for custom services or a 16bit ushort for official Bluetooth services. The 32feet library contains static classes containing common Uuids in the GattServiceUuids and GattCharacteristicUuids classes. These contain the helper method GetServiceName() which will return the string name of the service (as defined in the Bluetooth Assigned Numbers reference). Therefore, we will use this and fall back to the Uuid if it is not a known service. 

We repeat the same process for each service to enumerate all the characteristics. Here as well as getting the defined name, we can check for certain well-known characteristic Uuids and retrieve and format the value. There isn’t a generic way of doing this for all characteristics because the data’s meaning depends on the service’s specification. In the case of custom services, these may not be publicly documented. 

To illustrate retrieving values, we check for a couple of common characteristics. BatteryLevel contains a single byte containing the battery level as a percentage. DeviceName, ManufacturerNameString and ModelNumberString all contain UTF8 encoded strings, so we can retrieve these and store them in the relevant BluetoothCharacteristic to populate our UI. 

Testing the App

The complete solution contains a WinUI desktop Windows project, along with a mobile project supporting iOS, Android and macOS targets. Because it requires a physical Bluetooth adapter to function you can’t test the app on an emulator – you will need to deploy to a physical device. 

When you run the app on iOS you will get a prompt after clicking the Connect to device button to request permission. On Android, as mentioned, it is unlikely that you will be prompted for permission but this could vary between Android versions and device manufacturers. 

The device will display a platform specific picker dialog to select from the available devices. When you select one the app will connect to GATT and attempt to enumerate services and characteristics and populate the grouped list. Because this can take a few seconds a ProgressRing is used over the whole page and is removed once the list is populated. 

Figure 3: Bluetooth Explorer in action with a Bluetooth tracking device

Building your own App

Using Bluetooth to connect to your specific device need not be any more complicated than this. You’ll need to know the specification for the service you are connecting to – it could be either one of the Bluetooth standards or something device-specific. This will tell you how to interpret the raw byte array data for a characteristic. If you want to customize the UI further, you can use Bluetooth.ScanForDevicesAsync() method, which returns a collection of BluetoothDevice objects; you can then present these in any way you choose. 

The full source code for the sample is available here. 

Next Steps

To update to the latest Uno Platform release, simply upgrade your packages to 4.7 using the Visual Studio NuGet package manager. And if you’re new to Uno Platform, our official “Getting Started” guide is the quickest and easiest way to get started – it only takes 5 minutes to complete!

Tags:

Share this post:
Tune in Today at 12 PM EST for our free Uno Platform 5.0 Live Webinar
Watch Here