Solving Architectural Problems in Bluetooth Low Energy Mobile Apps
Learn how to create a solid BLE architecture, as solid as the Colosseum. 🏛
Published on January 27, 2021
by
&
Filed under development
On our most recent project, we’ve had the opportunity to work with Bluetooth Low Energy (BLE). Starting with BLE on Android and iOS is quite simple, but more complex use cases often led us to problems, if not addressed properly. Our app needed to be robust regarding the BLE connection and ensure that BLE operations are executed synchronously.
We would like to show you some issues we stumbled upon and the solutions we came up with. As an example, we'll use the OTA (over the air) firmware update functionality. This process is usually time-consuming and contains multiple interdependent operations.
In our case, to perform such an update, we had to read the device’s status to see if it’s ready to receive the update, subscribe to status characteristic, start sending multiple data chunks depending on status received, and unsubscribe when complete status is received. Any other non-related operations should not interfere with the update process. Let’s have a look at this simplified pseudocode.
connect(onComplete: {
authorize(onComplete: {
// OTA
readPeripheralStatus(onComplete: {
if peripheral is ready for update {
subscribeToPeripheralStatus(onNext: {
if peripheral is ready for next write {
writeNextDataChunk()
} else if OTA is completed {
unsubscribeFromPeripheralStatus()
disconnect()
}
})
writeNextDataChunk()
}
})
// Non related write operation
write(onComplete: {
disconnect()
})
})
})
Let’s state the obvious. The pseudocode is too complex and adding any new functionalities (e.g. retry, timeout) will make it even worse. We could identify multiple issues with the way it's written but that wouldn’t be the main problem.
Scheduling simple read and write operations is usually handled by many Bluetooth libraries, but as soon as we had to deal with operations that are dependent on each other, those libraries were not enough.
Remember, the callbacks are asynchronous which means, while we wait for them to complete, other parts of our code might be performing their own Bluetooth operations. As the code grows, you lose track of what and when it happens.
Can you find the bug in our simple example? The nonrelated write operation will be executed immediately after the peripheral status read operation. Interfering of nonrelated operations could easily happen in large and less straightforward apps or in apps in which these operations are invoked by UI components.
Diagram depicts such a scenario. Not only did the UI-invoked operations interfere and stop the OTA process, but they could have led the app into inconsistent states and caused crashes. Possible scenarios like this one are numerous and unpredictable so we shouldn’t disregard them as trivial. We shouldn’t count on the robustness of the firmware either. Sending unexpected data might cause issues even on the peripherals.
The central component of our solution is the command queue.
Command is a design pattern that turns a request into a stand-alone object that contains all information about the request.
This transformation lets us queue and schedule each request’s execution. By changing simple commands that are mutually dependent into more complex ones, we can ensure the overall execution order and improved robustness.
In our example, the minimum requirement for an object to be a Command is to conform to the Command interface.
interface Command {
function execute()
}
Let's have a look at how using the command queue enabled us to achieve our goals with our example. The diagram below shows all of the OTA’s underlying commands being uninterrupted by other non-related commands.
The queue executes each command when the previous one finishes so there is no more fear of asynchronous operation interfering with one another and therefore no need for nested callbacks at the point of use. Methods calls can now be simply written in the order in which we want them executed. If a command depends on the result of the previous one, you should consider making a new one that will encapsulate them both. Furthermore, the concerns are now separated as each of the operations is managed by its own command. Our code might now look like this:
queue.add(ConnectCommand())
queue.add(AuthorizationCommand())
queue.add(OTAUpdateCommand())
queue.add(DisconnectCommand())
Introducing new commands is quite easy and shouldn’t ever break the code you already have. Many of these commands can be written using generics which can significantly reduce code redundancy.
By introducing the Proxy pattern to our solution, we can enhance the functionalities of existing commands. For example, limiting execution time or retry mechanism can be easily applied. This was especially useful in solving unstable BLE connection problems.
var command = SomeRandomCommand()
var timeoutCommand = TimeoutCommand(command)
var retryTimeoutCommand = RetryCommand(timeoutCommand)
queue.add(retryTimeoutCommand)
You can also take advantage of the Decorator pattern when dealing with the command queue.
Let’s say you need to execute some commands before or after every operation. Again, as an example, we can use OTA. You’ll do something like this:
var queue = BasicCommandQueue()
queue.add(ConnectionCommand())
queue.add(AuthorizationCommand())
queue.add(OTAUpdateCommand())
queue.add(DisconnectCommand())
By introducing queue decorators, we can centralize the connection and authorization code in one place:
AuthorizeCommandQueue(commandQueue) {
var queue: BasicCommandQueue
func add(command: Command) {
queue.add(AuthorizationCommand())
queue.add(command)
}
}
ConnectionCommandQueue(commandQueue) {
var queue: BasicCommandQueue
func add(command: Command) {
queue.add(ConnectCommand())
queue.add(command)
queue.add(DisconnectCommand())
}
}
var commandQueue = BasicCommandQueue()
var authorizeCommandQueue = AuthorizeCommandQueue(commandQueue)
var connectionCommandQueue = ConnectionCommandQueue(authorizeCommandQueue)
From now on, add new operations to the queue without dealing with connection and authorization because it’s taken care of by the command queue decorators automatically.
connectionCommandQueue.add(OTAUpdateCommand())
And if for some reason we decide to disable or completely remove authorization from the system (change request for example), all we need to do is leave out the authorization queue decorator.
One more command pattern feature was of great value for us. Commands that are queued can be canceled. We’ve implemented it by creating a Cancelable interface with just the cancel() method.
cancelableCommand = OTAUpdateCommand() // conforms to Cancelable
queue.add(cancelableCommand)
// …
cancelableCommand.cancel()
Proposed solutions benefited us in many ways. We would definitely recommend it if your app requires even a bit more complex operations. If your app’s requirements change or the firmware guy decides to add an extra cool feature, you’ll be glad you’re all set and ready.
For those of you who are developing for Android and iOS, we prepared some sample code that should help you get things started:
Join our newsletter
Like what you see? Why not put a ring on it. Or at least your name and e-mail.
Have a project on the horizon?