Run Scripts on Your Laptop From The Smartphone Using Bluetooth LE
Create a framework to allow commanding the execution of scripts on a BLE central from your smartphone
In this article, I will talk about how you can command the execution of scripts on your laptop (acting as a Bluetooth LE Central) from your smartphone (acting as the Peripheral).
If you don’t know what BLE is, or what BLE central and peripheral are, or what GATT is, it is highly recommended that you read something about it first, and then come back to continue with the article.
Before starting to describe the details of the project, let’s start with a brief high-level overview of the goal.
Create a system that, from a smartphone, is able to broadcast-command the execution of scripts to a set of BLE centrals.
The goal is to create a system that, from a smartphone, can broadcast-command the execution of scripts to a set of BLE centrals, possibly supporting many different Operating Systems.
This project started from a project work assignment I did in university, and the “use case scenario” was that of an operator going around a factory and commanding the execution of “something” (a script) to all the machines nearby.
So, more specifically, what you are going to achieve if you follow the article is a “framework” in which an Android application is able to command the execution of scripts in all nearby devices running a client that continuously scan for “certain” peripherals, and connects to one whenever it finds one.
The Project’s Stack
The project consists of two main parts:
- the Android applications, implementing a GATT server
- the multi-platform client.
For the development of the Android application, it was used Kotlin as the language, and a permission management library I wrote myself to make it easier to manage permissions in the app.
For the client, on the other side, two different versions were developed:
- one using Python, and the Bleak library for Bluetooth
- the other using Rust, and the Btleplug library for Bluetooth.
Both implementations support Linux, Windows, and macOS, but the Python one is far more complete than the Rust one, partly because the Python library is more mature, partly because I’m lazy…
The Messages Exchanged by The Devices
The Peripheral exposes a service with UUID 0000ffe0-0000-1000-8000-00805f9b34fb
having two characteristics:
- one that will be used to command the script execution, and to which centrals subscribes to for notifications
- one that will be used by the centrals to inform the peripheral about the execution status (whether it has started, and the exit code).
The typical communication flow is shown in Figure 1.
To make it more clear, the script’s characteristic value is a string indicating what script to execute with the relative arguments (e.g. sum_two_numbers.sh 1 3
). When the central starts to execute the script, it makes a read to the status characteristic, to signal “hey! I’m about to execute your request” and, when it finishes, writes to the status characteristic the exit code of the script (the exit code, not the script’s output!).
The Roles
The Android app takes the role of BLE Peripheral, and is the one that is “producing” information (i.e. the script to run). In the project I created, it works by accepting connections from all the devices that request it, and broadcasts any new script execution request (that is, for example, if you run a script that start a server in localhost, all connected devices will try to start said server).
The “desktop” app takes the role of BLE Central, and is the one “gathering and consuming” information. The app continuously search for new device to connect to (i.e. devices exposing the service), and connect to the first available one. Once connected, it remains connected “forever” or until the connection is lost.
The Android Application (BLE Peripheral)
The application’s designed is very simple:
- on top, the list of connected devices, and info about their subscription and execution statuses will be shown
- in the lower part, a field to insert the script you want to execute and its parameters is present, as well as a “send notification” button, to command the script’s execution, and a “start/stop server” button, to start and stop exposing the service.
The link to the application repo is available at the end of the article.
The application uses a permissions' management library that I created to ensure that the necessary permissions are granted by the user, and also it checks that the Bluetooth LE is available on the device and turned on.
The app has various components organized into packages:
- the
ble.gattserver
package contains a set of components useful to manage the GATT server and its advertisement, that tries to be as general as possible - the
blescriptrunner
package, on the other hand, contains components that are specific to the actual use of the server that is made - the
fragments
package contains the connected devices fragment.
The two main Android components that are used to manage the Bluetooth LE GATT server are the BluetoothLeGattServer
and the BluetoothLeAdvertiser
.
In particular, the advertiser is moved to a Service, while the GATT Server is encapsulated in the GattServerManager
class, that tries to provide a (as much as possible) general implementation of a GATT server.
The Python Central
The Python application is very simple, and it is developed using the Bleak library to manage the Bluetooth LE.
The application continuously tries to discover and connect to devices exposing the script runner’s service UUID and, Once one of such devices is found, and the application connects to it, it enables the notification for the script characteristic, and waits to receive notifications.
Within the app, it is not possible to command the execution of any script, but each script that you want to be executable from the smartphone must be put in a specific directory.
Once a notification is received, the notified characteristic value is put into an asynchronous queue, that is consumed (also asynchronously) by the coroutine that has the task to try the script’s invocation. This task parses the characteristic value (in a POSIX compliant way), and then tries to execute the script obtained by prepending arg[0]
with the path to the script directory. If a script with that name does not exist in the script directory, the error is saved in the log file, but the program continues to run without being blocked.
The application is able to run scripts on Linux, Windows, and macOS and, if you provide different implementations of the same script (one for each platform) it is able to automatically add and/or correct the script’s extension to use the one that is correct for the central’s Operating System.
The Rust Central
The Rust application was created after the Python one, and because of limitations in the used library — Btleplug — and in my knowledge of Rust, it offers fewer features than the Python counterpart.
In particular, the Rust application is not able to:
- handle disconnections
- be configured via command line options
- add or correct the script extension.
Anyway, the core functionalities are still present, and a scenario with some centrals using the Python application and some others using the Rust one is perfectly tolerable.
Performance Assessment
In the tests conducted using the Python central app, the results when having a single laptop (MacBook Air M1, with macOS 12.6) connecting over 14 trials were:
- 1.28s — mean discovery latency (with a standard deviation of 1.58s)
- 0.76 — mean connection latency (with a standard deviation of 0.47s).
Another test was conducted, whose results are shown in Figures 5 and 6. The test was carried out using three different laptops trying to connect at the same time or one at a time. The three laptops were a MacBook Air M1, a laptop with Ubuntu installed (Linux1), and a laptop with Manjaro installed (Linux2).
The results of the performance analysis carried out on different devices, shows that the main factor influencing the latencies is indeed the Operating System mounted by the device (or how good is the Bleak backend for that OS). Indeed, as noticeable in Figures 5 and 6, the two Linux machines showed similar performances (despite being onboarded with different Bluetooth chipset, and different versions of Bluetooth too), while the macOS laptop had totally different performances.
In particular, despite being onboarded with a newer chipset, the macOS laptop had a significantly higher discovery latency, partly compensated on the other hand by a lower connection latency, compared with the two Linux laptops.
Moreover, using having all three laptops trying to connect simultaneously, or using them one at a time, did not show significant performance differences (at least, not that would be noticeable by a human user).
Conclusions
To conclude, by completing this project, I was able to explore more in depth the strengths and weaknesses of Bluetooth LE which is, in my opinion, a huge step forward compared to Bluetooth classic.
One of the biggest obstacles I faced while carrying out this project, was to find resources showing how to implement a GATT server. I have the impression that most resources focuses on how to create a Bluetooth LE Central, while the documentation and resources on how to create a service is much more scarce.
One of the biggest strengths of BLE is that it provides a fairly good communication range (5 to 10 meters in my experience), and a decent transmission speed, with a very low battery consumption.
On the other hand, I feel that more effort should be conveyed toward making Bluetooth LE easier to work with for developers (e.g. more and better resources and documentation), which is exactly what I am trying to achieve (at least a bit) with this article.
Let me know in the comments if you liked it or not, or if you have doubts or questions, I will try my best to answer!