This guest post was written by Gus Workman, a Garmin intern and Ohio State University student.
Bluetooth mesh is a protocol that was released in 2017 by the Bluetooth SIG. Since that time, it has slowly started to gain traction for its many-to-many connection capabilities, enhanced range, and security features. The protocol is getting more attention with the big silicon manufacturers, and every week sees the release of new Bluetooth mesh IoT devices. Because this protocol is quickly rising in popularity, the team here at Garmin thought that it would be an interesting challenge to see if the Bluetooth mesh protocol could be supported on Connect IQ devices using the brand new BluetoothLowEnergy APIs. Out of this concept came two projects - a Bluetooth mesh library and an example application using the library.
The following is a guide to the Bluetooth mesh library created as a 2019 summer intern project. This library can be used by Connect IQ devices that support the 3.1 Bluetooth Low Energy APIs. It can be used to create a Bluetooth mesh network, provision devices to add them to the network, and control devices on the network. It does not turn a Connect IQ device into a node in the mesh network, since it only supports interacting with the network via a proxy node. The library has been designed to make it easy to either build off a implementation of a BluetoothLowEnergy delegate or to create a custom implementation in order to have more control or support other Bluetooth functionality outside of Bluetooth mesh.
Bluetooth mesh is by no means a simple protocol - there are many great features (such as the excellent security Bluetooth mesh provides), but the multitude of features and small details can make it seem very difficult. Fortunately, this Monkey Barrel makes Bluetooth mesh much easier while also giving the developer access to the lower-level layers of the protocol.
To get started building Bluetooth mesh applications on Connect IQ, you will first need to download the barrel and import it. To import it into your project, follow the steps outlined in the Programmer's Guide. After importing the barrel into your project, you only need to add the following pieces of code to get started with Bluetooth mesh:
- Create a class that extends MeshDelegate and implement the following methods:
- If you plan on supporting provisioning new devices, you also implement the following methods:
- Initialize a NetworkManager object and pass it into a new instance of your custom MeshDelegate class.
- (Optional) Call the networkManager.load() method to load the saved state, and put a call to networkManager.save() in the application's onStop() method to save the state on close.
- Import the BluetoothLowEnergy module with using BluetoothLowEnergy as Ble;
- In your app's onStart() method, call Ble.setDelegate() with the instance of your custom MeshDelegate class.
That's pretty much it! Integrating the Bluetooth mesh Monkey Barrel is fairly easy.
Connecting to Devices
To communicate with the mesh network, your device needs to connect to a proxy node. Simply call your custom MeshDelegate.startScanning() method with an argument of MODE_PROXY to begin the process. Over the next five seconds, the device will scan for devices and collect the results into the scanResults array. The scan results are automatically filtered to only include unique results where the device is broadcasting a Bluetooth Mesh Proxy (or Provisioning if you used MODE_PROVISION as the argument to startScanning()) Service UUID. Because your custom delegate class extends the base MeshDelegate, you have access to the scanResults instance variable right within your onScanFinished() method. In this method, you should choose one of the devices to connect to, then call the connectToDevice(index) function with the index of the result from the scanResults array. The recommend way to choose one is to iterate through the scanResults array and find the device with highest RSSI value. For some cases, such as during provisioning, you might want to present a list of the scan results to the user.
Sending and Receiving Data
What about using the application to send and receive data? Most of your application logic concerning receiving data back from the network will reside in the onNetworkPduReceived(networkPdu) method - this is where you will want to act on the payload opcode for the PDU. To send data, however, you will need to do the following:
- Instantiate a AccessPayload class with the opcode and parameters you wish to send (if no parameters, pass in an empty byte array).
- Create a new TransportAccessPDU, passing in the application key flag (true if you want to encrypt the packet with an application key, false to use the device key), MIC size (for almost all access messages, use MIC_SIZE_4), and the AccessPayload object you created in 1.
- Initialize a new NetworkPDU using the static method newInstance(networkManager, ctl, dst, transportPdu).
- Get the segmented and encrypted PDUs by calling the static method ProxyPDU.sesgment(PROXY_TYPE_NETWORK_PDU, networkPdu).
- Finally, now that you have your segmented data you can send it to the device with networkManager.send(segmentedData).
This is all most applications will require, but this library makes it easy to use more complex features like defining your own packet types and encryption schemes.
Provisioning new devices into the network is a little more involved since user input is required at some stages. The implementation is largely left up to the developer to figure out the best way to present the data to the user, and then to use callbacks to continue the provisioning process. For the example application created with this library, I chose to primarily use Menu2 and GenericPicker to present the information. Provisioning with the library requires the following steps:
- Scan and connect to the device with MODE_PROVISION as the argument to startScanning(mode). The InvitePDU is automatically sent to the device that was connected to.
- The first packet that is received in response is the CapabilitiesPDU. After this is received, the system will call the onProvisioningParamsRequested(capabilities) callback function in your custom MeshDelegate class. In this function you must call the ProvisioningManager.onProvisioningModeSelected(startPdu) method (use self.networkManager.provisioningManager from your custom MeshDelegate class). You can display a list of authentication options to the user, but note that not all modes have been implemented in the library yet. Check the docs for more information.
- If output OOB authentication mode was selected, you will need to get the output value from the user when the onAuthValueRequired() callback method is called. You should have some form of input from the user, then send that value back to the library by calling the ProvisioningManager.onAuthValueInput(authValue) method. This will continue the provisioning process.
- Finally, the provisioning will either fail and the onProvisioningFailed(reason) callback will be invoked, or provisioning will succeed with your application being notified through the onProvisioningComplete() callback. Either way, provisioning is over. You should probably call the self.disconnect() instance function of the base MeshDelegate class from here, because the device will no longer accept data from the Provisioning Data In characteristic. Reconnect to the network using startScanning(MODE_PROXY).
- Finally, while the device is provisioned, you likely still need to configure the device before it is useful. See the below instructions for doing so.
The configuration process involves adding app keys to the device, as well as binding those keys to specific models. Optionally, you can configure subscription and publication addresses for those elements. Because configuration is largely dependent on each use case, the library only provides the necessary tools to craft your own configuration scheme. There is no special ConfigurationManager class to handle this because all of the configuration PDUs are regular TransportAccessPDUs - the only difference is that the packets must be encrypted with the device key, and not an application key. So, make sure to set the akf field to false when initializing new TransportAccessPDU variables!
The library does provide some helper classes to piece together configuration data:
- Get the AccessPayload for retrieving the composition data of the device using CompositionData.getCompositionData(). Use this AccessPayload object as described above in the Sending and Receiving Data section above.
- When you receive the composition data back from the device, you can parse it using the CompositionData.decode static function.
- To add an app key to the device, use the AccessPayload from AppKeyConfig.getAddAppKeyConfig().
- For setting an element's publication or subscription status, use AppKeyConfig.getPublishAddConfig() or AppKeyConfig.getSubscribeAddConfig() to get the necessary AccessPayload objects.
- The AppKeyConfig class contains some additional opcode constants that are useful when receiving and parsing the acknowledgements and status PDUs back from the device that is being configured.
To send other configuration messages, you need to figure out the correct opcode and parameters from the Bluetooth Mesh Specification documents.
Bluetooth mesh is a powerful new way of interfacing with IoT devices. With the Bluetooth mesh barrel you can integrate Bluetooth mesh features into your own Connect IQ applications. As this technology becomes more prevalent, we can't wait to see what amazing apps the community creates!