How to Connect to a Bluetooth Device with a MacBook and Python

Proto Bioengineering
12 min readFeb 23, 2023

--

Connect to smart watches, smart light bulbs, and Bluetooth LE sensors with Python and Bleak on your Mac OS computer.

Photo by Solen Feyissa on Unsplash

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:

  1. Install Bleak, a Python library for connecting to Bluetooth LE devices.
  2. Import Bleak into a basic Python file.
  3. 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:

  1. You have a main device (a laptop or mobile phone) and a Bluetooth device, like a smart watch or light bulb.
  2. You turn on Bluetooth on these two devices.
  3. 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?”
  4. You see the smaller Bluetooth device(s) in an “Available Devices” list on your laptop or smartphone.
  5. You click a button to agree to connect the devices.
Mac’s built-in Bluetooth found these devices. This menu is on your Mac under System Preferences > Bluetooth.

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
Your Terminal prompt may have a “%” or “$”. Write all commands in Terminal after this character.

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.

To find Terminal app, you can hit the “Command + Space” keys and type “Terminal” into the search bar.

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:

  1. Check if you have Pip already installed (type pip in Terminal)
  2. Download Pip, if you don’t have it.
  3. Install Bleak from the Terminal with pip install bleak
  4. 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 line import bleak
Our Python file (main.py) written using Sublime Text Editor.

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:

  1. Turn on Bluetooth on your laptop and the small Bluetooth device. (This step is manually done by a human.)
  2. Import a Bluetooth library like Bleak. (This is the first line in your Python file, import bleak.)
  3. Tell your code to scan for all Bluetooth LE devices near you and save them in a list.
  4. 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.
  5. 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 imported 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:

  1. Find the device (meaning “scan” for it).
  2. 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.

Bluetooth devices are always advertising their existence to the world.

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:

  1. import the asynchronous and Bleak libraries
  2. start the BleakScanner in discover mode, which will scan for Bluetooth devices for 5–10 seconds
  3. 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.

Our new scanner.py file with the Bleak Bluetooth scanner code.

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.

We type “python3 scanner.py” after the % character to run our script from Terminal.

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:

The script shows UUIDs and Bluetooth device names. The example is named “scanner.py” instead of “main.py”.

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 top right corner of your screen has a Bluetooth menu, as does System Preferences > Bluetooth.

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.

Xsens DOTs are about 1x1 inch (recently renamed to “Movella” DOTs).

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.

It will take 5–10 seconds for the program to connect and print “True” or “False.”

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.

--

--

Proto Bioengineering

Learn to code for science. “Everything simple is false. Everything complex is unusable.” — Paul Valery