Bluetooth Low Energy on KaiOS

Posted by Tom Barrasso on (updated on )

Working with Bluetooth Low Energy (BLE) on KaiOS

Use Cases

Bluetooth Low Energy (BLE) is a recent version of Bluetooth designed for very low power consumption. Compared to Bluetooth Enhanced Data Rate (EDR), BLE is designed to transfer much smaller amounts of data between devices via an ephemeral connection, not a durable pairing. Common BLE applications include:

  • Healthcare devices - Thermometer, heart rate and glucose monitor, etc
  • Proximity sensing - Tile, AirTag, “Find my” device
  • Contact tracing - People near me
  • Remote sensing - Battery level, volume, etc

Accessing Bluetooth APIs

Permissions

The "bluetooth" permission is needed to access Bluetooth Low Energy APIs on KaiOS. Although the permission is available to both privileged and certified apps, many Bluetooth features are only available in certified apps.

Availability

While most KaiOS devices support Bluetooth, it’s possible your app might get installed on a device without Bluetooth. KaiOS has no direct analog to the uses-feature element in AndroidManifest.xml, so it’s not possible to prevent installation on unsupported devices. However, you can detect Bluetooth support at runtime.

The feature-detection permission exposes an API to check the availability of specific device features and capabilities. For Bluetooth, check the device.bt key using the code below:

1navigator.hasFeature('device.bt')
2    .then((hasBluetooth) => {
3        if (hasBluetooth) {
4            /* Use Bluetooth */
5        }
6    });

Note: feature detection returns static values that describe what the device model is reportedly capably of, not what capabilities are presently available.

BT Interfaces

Bluetooth APIs are accessible via the BluetoothManager interface for both KaiOS 2.5 & 3.0.

KaiOS 2.5: navigator.mozBluetooth
KaiSO 3.0: navigator.b2g.mozBluetooth

Runtime Check: If your app was not granted the bluetooth permission, then these APIs will not be available. Confirm with a simple check, i.e. navigator.mozBluetooth !== null.

Note: KaiOS does not support the Web Bluetooth API. Instead, use BluetoothAdapter from FirefoxOS/ Boot2Gecko (B2G). The APIs are largely unchanged on KaiOS.

Bluetooth Adapter

Although KaiOS APIs support multiple BluetoothAdapters, all devices available today have a single antenna. The default adapter is accessible via mozBluetooth.defaultAdapter. However, the default BluetoothAdapter may not be immediately available after DOMContentLoaded. To listen for availability without polling, listen for the attributechanged event on BluetoothManager. The event passed to the callback is a BluetoothAttributeEvent with an extra attrs property, listing all the attributes that have changed.

The example below can be used at any point in the DOM rendering lifecycle and returns a Promise that will resolve to the default BluetoothAdapter, when it becomes available.

 1function getBluetoothAdapter() {
 2    if (navigator.mozBluetooth.defaultAdapter) {
 3        return Promise.resolve(navigator.mozBluetooth.defaultAdapter);
 4    }
 5
 6    return new Promise((resolve) => {
 7        navigator.mozBluetooth.onattributechanged = (e) => {
 8            if (e.attrs.some((attr) => attr === 'defaultAdapter')) {
 9                navigator.mozBluetooth.onattributechanged = null;
10                resolve(navigator.mozBluetooth.defaultAdapter);
11            }
12        };
13    });
14}

Note: a more robust version of this function would include a timeout, in the event that the BluetoothAdapter does not become available.

Bluetooth Low Energy

BLE Interfaces

1mozBluetooth.defaultAdapter.gattServer;
2mozBluetooth.defaultAdapter.startLeScan(serviceUuids: string[]): Promise<BluetoothDiscoveryHandle>;
3mozBluetooth.defaultAdapter.stopLeScan(discoveryHandle: BluetoothDiscoveryHandle);

Discovering (Peripheral)

Scan for nearby BLE devices with startLeScan. It takes an optional list of GATT Service UUIDs to filter results and returns a Promise that resolves to a handle that can be passed to stopLeScan to stop discovering devices.

1mozBluetooth.defaultAdapter.startLeScan(serviceUuids: string[]): Promise<BluetoothDiscoveryHandle>;
2mozBluetooth.defaultAdapter.stopLeScan(discoveryHandle: BluetoothDiscoveryHandle);

The BluetoothDiscoveryHandle published a devicefound that can be listened to when new devices are discovered. For BLE devices, the event passed to the callback is a BluetoothLeDeviceEvent with a few important properties, namely device which is a BluetoothDevice and rssi (received signal strength indication) which is a relative index of how close or far the device is.

Here is a simple example putting it all together that scans for 5 seconds and logs devices to the console.

 1navigator.mozBluetooth.defaultAdapter.startLeScan()
 2    .then((handle) => {
 3        handle.ondevicefound = (leDeviceEvent) => {
 4            let nameOrAddress = leDeviceEvent.device.name || leDeviceEvent.device.address;
 5            console.log(`Device found ${nameOrAddress} RSSI = ${leDeviceEvent.rssi}`);
 6        };
 7
 8        setTimeout(() => {
 9            navigator.mozBluetooth.defaultAdapter.stopLeScan(handle);
10        }, 5000)
11    });

Connecting

Once a device has been discovered, the next step is to connect to it. BluetoothDevice has the gatt property (type BluetoothGatt) that help a few helpful methods:

  • connect and disconnect to connect/cisconnect to the remote BLE device.
  • discoverServices to discover services, characteristics, descriptors offered by the remote GATT.
  • readRemoteRssi to read RSSI for the remote BLE device.

Note: All of the above methods return a Promise that might reject depending on the connection state of the remote device.

Services and Characteristics

Once connected, the next step is to find the service(s) of interest and read/ write values. The services array of BluetoothGattService lists services that can be identified via UUID. Each service itself has an array of BluetoothGattCharacteristic from which we can finally read or write values, depending on permissions.

Reading and Writing

The value of a characteristic is stored as an ArrayBuffer via the value property. Calling readValue will return a Promise that resolves to the remote characteristic value. Writing works via the writeValue method.

1handle.ondevicefound = (leDeviceEvent) => {
2    let device = leDeviceEvent.device;
3    device.gatt.connect()
4        .then(() => device.gatt.discoverServices())
5        .then(() => device.gatt.services[0].characteristics[0].readValue())
6        .then((valueBuffer) => /* TODO: read ArrayBuffer */)
7};

Notifications and Indications

KaiOS does not support notifications or indications! During runtime testing across a number of devices, I have been unable to receive notifications or indications via the startNotifications method. No characteristicchanged events (or any BLE events) are published for notifications.

GATT Server (Central)

KaiOS devices can also act as central devices and provide GATT services. The first step is to create a BluetoothGattService using the constructor, then add characteristics to the service with the appropriate properties and permissions.

1const SERVICE_UUID = '57bb7cb8-7700-4971-a20b-74a1f0c070c0';
2let service = new BluetoothGattService({
3    isPrimary: true,
4    uuid: SERVICE_UUID,
5});
6let defaultAdapter = navigator.mozBluetooth.defaultAdapter;

Note: addCharacteristic returns a Promise, so it’s best to wait for this to resolve before adding the service to the BluetoothGattServer.

 1service.addCharacteristic(
 2    'e8ef9b49-3b70-411b-9ded-f8bf6d651f38',
 3    { read: true, readEncrypted: true },
 4    { read: true, broadcast: true },
 5    new ArrayBuffer()
 6)
 7.then(() => defaultAdapter.gattServer.addService(service))
 8.then(() => defaultAdapter.setDiscoverable(true))
 9.then(() => defaultAdapter.gattServer.startAdvertising({
10    includeDevName: true,
11    includeTxPower: true
12    serviceUuid: SERVICE_UUID,
13    serviceUuids: [SERVICE_UUID],
14}));

Once the service is added, make sure the device is discoverable and finally, call startAdvertising to begin advertising the GATT service to nearby devices. startAdvertising accepts a BluetoothAdvertisingData object with some optional properties like includeDevName, which will broadcast the device’s name (i.e. “Nokia 8110 4G” by default) and includeTxPower, which will broadcast the Transmit (Tx) power. You can even set the name yourself via the defaultAdapter.setName method.

Note: during runtime testing, I have found that only one service can be active and broadcast at a time. The service can have multiple characteristics, but KaiOS does not appear to reliably support broadcasting multiple services.

Conclusion

With different Bluetooth standard versions and configurations, it can be a challenge to leverage complex networking technologies like Bluetooth. Always test your code on actual devices, since KaiOS phones may not perform as expected or as documented. Finally, test connectivity with a range of devices and prototype quickly with free utility apps like Bluetility (Mac OS) and LightBlue (iOS).

Tip: It can be helpful to convert the Firefox WebIDL interface definitions into TypeScript type definitions to more easily and safely code with unfamliar KaiOS Bluetooth APIs.

Credits

Thank you to Jan Frydendal of MochaSoft who provided insights from his own testing of Bluetooth Low Energy compatability and support on KaiOS.

Contact Author

Bluetooth Low Energy (BLE) is a very useful technology for discovering and connecting to nearby devices to exchange small pieces of information. If you are interested in extending your company’s Bluetooth offering to KaiOS, learn more about the author and find contact information on the About page.