How to Connect to a Bluetooth Device with a MacBook and Python
Connect to smart watches, smart light bulbs, and Bluetooth LE sensors with Python and Bleak on your Mac OS computer.
This is a beginner tutorial on Python and Bluetooth LE (Low Energy). If you know Python and Bluetooth already, check out the short TL;DR version of this article.
We’ll be connecting to an Xsens DOT wearable movement sensor as an example device, though you can connect to any device you have.
Requirements
- A Mac OS computer (12.x+)
- Python 3
- Bleak (a Python Bluetooth LE library)
- A smart watch, smart lightbulb, or similar Bluetooth LE device
What This Tutorial Covers
We will connect to one Bluetooth LE device with Python 3 by writing code from scratch. This code will be a building block for adding Bluetooth functionality to your projects and can be used to make dashboards, tools, and data analysis pipelines for Bluetooth LE devices.
NOTE: This tutorial is for Bluetooth LE devices, not regular Bluetooth. Though they’re similar, Bluetooth LE and Bluetooth are not the same under the hood. The code that we write here cannot be used to connect to a regular Bluetooth (“Bluetooth Classic”) device.
If you don’t need an in-depth explanation on Bluetooth or AsyncIO, see our short version of this article here.
Steps
In this tutorial, we will:
- Install Bleak, a Python library for connecting to Bluetooth LE devices.
- Import Bleak into a basic Python file.
- Use Bleak’s built-in functions to find and connect to nearby Bluetooth LE devices.
The final Python file will be about 10 lines of code.
What is Bluetooth LE?
Bluetooth LE is a wireless way to connect to smart watches, smart lightbulbs, proximity tags, and more. Bluetooth allows these little devices to connect to your phone or computer so that they can send messages to each other about your heart rate, text messages you’ve received, or the location of your lost car keys. The “LE” stands for “Low Energy”, because it’s like the original Bluetooth but faster and lighter.
How Everyday Connection to Bluetooth Works
To everyday people (non-programmers), Bluetooth connection works like this:
- You have a main device (a laptop or mobile phone) and a Bluetooth device, like a smart watch or light bulb.
- You turn on Bluetooth on these two devices.
- Both devices send out wireless signals into the universe saying, “My name is [Joe’s Macbook or Sarah’s Smartwatch]. I can do Bluetooth! Who wants to connect with me?”
- You see the smaller Bluetooth device(s) in an “Available Devices” list on your laptop or smartphone.
- You click a button to agree to connect the devices.
Now, your laptop and your smart lightbulb or other small Bluetooth device are connected and exchanging data.
How to Connect to Bluetooth with Code
To connect with code however, we need to break the above 5 steps into multiple, smaller, more specific steps.
We don’t have to write everything from scratch though. We can use a few Python libraries that other people wrote (Bleak and AsyncIO) to make things a little easier.
Make Things Easier with the “Bleak” Library
Bleak is a Bluetooth LE library for Python that is free to use and comes with a bunch of pre-written code so that we can connect to our Bluetooth LE devices quickly. It can connect to Bluetooth LE devices from macOS, Windows, and Linux.
To use it, we’ll install it onto our Macbook (covered below) then import
it into our Python code like so:
# Our python code file, "main.py"
import bleak
...
# the rest of our code
Importing is the easiest step. It’s usually one or two lines of code.
There will be a few more lines needed to make Bleak work well, but that is the most basic way to include Bleak into our own Python code: install and import.
The installation step before importing is a little more intensive, due to needing a package manager like Pip.
Installing “Bleak” for Python
Bleak’s Github has an installation guide, which is as simple as opening Terminal on your Macbook and running:
pip install bleak
However, if this is your first time using Pip, it will take some install and setup (though this will make all of your future Python projects easier).
The official installation tutorial for Pip is linked here.
Pip is the package manager for Python, and any Python library of moderate popularity can be downloaded using Pip. If you downloaded Python through Python.org, Homebrew, or if it was already installed on your Macbook, you likely already have Pip. All Pip commands can be run by typing pip <your command>
into Terminal.
To check if you have Pip, you can open Terminal and simply type pip
. Press enter, and Pip will print a help menu if it is already installed.
Why download Pip? Pip is used in most other Python tutorials and packages that you will come across. Knowing Pip is key to being a good Python programmer.
If you don’t have Pip, you can either follow the official installation instructions or re-download Python 3. Pip will come included in the Python 3 install.
The above, pip install bleak
, will download and install Bleak onto your laptop. Then we’ll be able to go into our Python code (the files ending in .py
) and use the import
command to import Bleak and use it.
To reiterate, the steps are:
- Check if you have Pip already installed (type
pip
in Terminal) - Download Pip, if you don’t have it.
- Install Bleak from the Terminal with
pip install bleak
import bleak
into your code (e.g. put that line at the top of your Python file)
NOTE: the versions for Bleak and Python in this tutorial are the following:
bleak = 0.18.1
python = 3.9.6
Let’s Write Some Python Code
Now that we have Bleak installed, let’s make some of our own Python code from scratch.
- Create a new Python file called
main.py
in a text editor like Sublime or others. - At the top of
main.py
, import the Bleak library with the lineimport bleak
Now, we can use all of the capabilities of the bleak
library in the rest of main.py
.
A Brief Pause: the Steps Behind the Python Code
As a reminder, the steps for connecting to Bluetooth via code are more detailed than everyday Bluetooth connection.
In code, this looks as follows:
- Turn on Bluetooth on your laptop and the small Bluetooth device. (This step is manually done by a human.)
- Import a Bluetooth library like Bleak. (This is the first line in your Python file,
import bleak
.) - Tell your code to scan for all Bluetooth LE devices near you and save them in a list.
- Search through the list of saved Bluetooth LE devices for the one you want, which might have a real name like “Philips Lightbulb” or might only appear as a MAC address (e.g.
aa:bb:cc:11:22:33
) —which is like a permanent IP address for individual, wireless, electronic devices. - Send a request from your laptop to connect to the Bluetooth device.
As we can see, Bluetooth is more complicated under the hood.
Back to Writing Python Code: How to Use Bleak
We have import
ed Bleak. Now what to do we do with it?
We can interact with Bluetooth devices using functions built into Bleak. For example, we can find Bluetooth devices near us with:
bleak.BleakScanner()
# OR
bleak.BleakScanner.discover()
We can also connect to a specific device by the device’s address:
address = "ab:cd:ef:34:56:78"
device = bleak.BleakClient(address)
In our real code, we will have to write a few more lines due to the nature of wireless communication, but the above functions are the core of Bleak and Bluetooth LE.
A Real Code Example
This code “scans” for all Bluetooth LE devices in the area (within about 100 meters of you).
It uses Bleak to discover
all nearby devices, then print
them to your screen. It doesn’t connect to them just yet, but we’ll cover that below.
import asyncio
from bleak import BleakScanner
async def main():
devices = await BleakScanner.discover()
for device in devices:
print(device)
asyncio.run(main())
Why We Need to Use Asyncio
Usually, code runs from top to bottom, one line after the other. But what about code where we need to send a wireless message to a Bluetooth device and then we don’t know when we’ll hear back from that device? We can’t make our code pause and wait indefinitely, so what do we do?
We have to use asynchronous code, which in Python 3 is provided by the built-in library, asyncio
. Asyncio allows us to run multiple parts of code at the same time, which makes it easy to do things like scan for and talk to Bluetooth devices that may take a few seconds to respond to us. (More info on “scanning” is below.)
In fact, Bleak will not work without asyncio
. The example code from Bleak’s documentation shows how this is done:
# Connect to a single Bluetooth device
import asyncio
from bleak import BleakClient
async def main():
async with BleakClient("XX:XX:XX:XX:XX:XX") as client:
# Read a characteristic, etc.
...
# Device will disconnect when block exits.
...
# Using asyncio.run() is important to ensure that device disconnects on
# KeyboardInterrupt or other unhandled exceptions.
asyncio.run(main())
Note: using from bleak import BleakClient
is equivalent to doing import bleak
then referring to bleak.BleakClient()
each time in the rest of your code. An import that looks like from X import X
allows us to leave off the bleak
part in front of every Bleak class or function we want to use.
Without asyncio, the code will send out messages to Bluetooth devices, but since it doesn’t wait and listen to any messages from those devices, the code moves on immediately and quits with no devices found or messages received.
The Overall Steps for Bluetooth Code
To connect to a Bluetooth device, we need to do two things:
- Find the device (meaning “scan” for it).
- Request to connect to it.
If you happen to know your device’s MAC address or UUID, you can skip to the “Device Connection” section below. Scanning only tells you what devices are in your area and what their addresses are.
- MAC addresses look like:
ab:cd:ef:12:34:56
- UUIDs look like:
123e4567-e89b-12d3-a456–426614174000
Note: “MAC addresses” have nothing to do with Mac computers. A MAC address is a wireless address so computers know which wireless device they’re talking to.
How to Scan for Bluetooth Devices with Code
Let’s write a scanner for Bluetooth LE devices.
All Bluetooth LE devices that are on but not connected to anything are sending out wireless signals all the time saying, “Hey, I exist and want to connect!” We’re going to write code that listens for these “Hey!” messages and prints them to our computer screen.
In the end, we’ll have a list of names and addresses for all available Bluetooth LE devices in our area.
The meat of the code is going to be an async
function, inside of which is one line where we call BleakScanner.discover()
. We collect all of the discovered devices in the devices
variable. Then once scanning is done (5–10 seconds later, the default for discover()
) we use a for
loop to print out all the devices found.
Below is the simplest way to use Bleak to scan for Bluetooth devices:
import asyncio
from bleak import BleakScanner
async def main():
devices = await BleakScanner.discover()
for device in devices:
print(device)
asyncio.run(main())
From top to bottom, we:
- import the asynchronous and Bleak libraries
- start the
BleakScanner
indiscover
mode, which will scan for Bluetooth devices for 5–10 seconds print
out all the Bluetooth devices found
All of the async
,asyncio
, and await
statements are specific ways that we tell Python which parts of our code can be run asynchronously while the rest of the code runs as normal. Basically all of our code is asynchronous here, but bigger programs will have more synchronous pieces instead.
The last line, asyncio.run(main())
, tells Python that we want the main function (async def main():
) to be run asychronously when the program, main.py
, starts. Otherwise, asyncio
isn’t wrapping any part of our code and the await
and async
keywords become meaningless.
Let’s run our scanner.
With the code above saved in a scanner.py
file, we’ll run the code from the command line (AKA “Terminal”), the same app we used to pip install bleak
.
In Terminal, navigate to the folder that scanner.py
is saved in. For example, if scanner.py
is saved in your Documents folder, you will open Terminal, then type cd Documents
(cd
= “change directory”) to navigate to “Documents”, then you can run scanner.py
.
To run your code, type python scanner.py
. The program will start listening for Bluetooth devices. After 10 seconds, it will write a bunch of devices (their UUIDs and names) to the screen. Note: Some systems require you to write python3
instead of python
, depending on your install.
The output of the scanner:
Thanks to our scanner, we can see all Bluetooth devices near us, with their UUIDs and names.
On the first line in the picture, we run the scanner (python3 scanner.py
).
The second line shows our first Bluetooth device found. Its address, or UUID, is E06699287-436C-XXXX-XXXX-XXXXXXXXXXXX
, and it’s name. (There are a lot of Unknown
names, since not every Bluetooth device will give it’s name out.)
Why are there UUIDs and no MAC addresses? This is a quirk of Mac OS computers that was implemented for security reasons. Macs can connect using MAC addresses if you happen to know your device’s address, but Mac defaults to using UUIDs.
If you’re having problems scanning:
- make sure your Macbook’s Bluetooth is on
- try re-running
scanner.py
Bluetooth is tricky and won’t always tell you when it is off or why it is failing. Turning things “off and on again” solves many Bluetooth problems.
The point of scanning is to find a Bluetooth device’s address (either its MAC address or UUID).
As a reminder:
- MAC addresses are
xx:xx:xx:xx:xx:xx
- UUIDS are
XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
Finding your device’s address takes trial and error, especially if it reports its own name as “Unknown” like the scanner example above. Some devices have a MAC address printed on their circuit board or their plastic casing, but that is not the norm. You can turn your Bluetooth device on and off as you re-perform your Bluetooth scan multiple times to see what appears or disappears, though this is hacky.
The Actual Device Connection Step
To connect to a Bluetooth device, all we need to do is give the device’s MAC address back to BleakClient
.
This will be similar to the code in the examples above. We’ll put this code in a separate file, named connect.py
. For this example, we’ll try to connect to one of the Xsens DOTs found by the scanner, which are tiny body movement tracking devices.
Our connection code works as follows:
# connect.py
import asyncio
from bleak import BleakClient, BleakScanner
async def main():
address = "338312FA-C3D1-183F-325A-0726AFDBEB78" # Xsens DOT UUID
async with BleakClient(address) as client:
print(client.is_connected) # prints True or False
asyncio.run(main())
Above, we use the device’s UUID or MAC address to asynchronously connect to the device, then check whether it’s actually connected or not with client.is_connected
. The program should print True
or False
and then disconnect and quit.
If it prints True
, our code worked. If it’s False
, the device wasn’t found. If a device isn’t found, either the address was wrong, the device didn’t respond, the Mac’s Bluetooth was off, or something similar. To troubleshoot, try running the code again to confirm, double-checking your Bluetooth address, or waking your Bluetooth device, since Bluetooth can sometimes sleep or fail inexplicably.
That is all we need to connect to a Bluetooth LE device.
What else can we do?
It’s not enough to only connect to devices. What about getting and sending data from these Bluetooth devices?
We can stream data, make dashboards with our data, and control our smart lightbulbs and other devices. Check out the next article, which covers how to read data from a Bluetooth LE device. You can read the status of smart lights, smart ovens, and more. Even more tutorials are upcoming. Stay tuned.
Read More in Our eBook
We now have an e-book about how to control and stream real-time data from Bluetooth LE devices. Pay What You Want until June 1st, 2024 on Ko-fi!
Questions and Feedback
If you have questions or comments, email us at protobioengineering@gmail.com or message us on Instagram (@protobioengineering).
If you liked this article, consider supporting us by donating a coffee.