Difference between revisions of "Setting Up Ubertooth on macOS for Bluetooth Hacking"

From Embedded Lab Vienna for IoT & Security
Jump to navigation Jump to search
 
(50 intermediate revisions by the same user not shown)
Line 6: Line 6:


* Hardware tools: Ubertooth One: Firmware version: 2020-12-R1 (API:1.07)
* Hardware tools: Ubertooth One: Firmware version: 2020-12-R1 (API:1.07)
* Smartwatch: Polar Vantage M
* Phone: Samsung Galaxy J3
* Unix-based Operating System: macOS (macOS Sequoia 15.1.1), on a MacBook Air with an Apple M1 chip.
* Unix-based Operating System: macOS (macOS Sequoia 15.1.1), on a MacBook Air with an Apple M1 chip.
* Virtual Environment: Python Environment: Python 3.12
* Virtual Environment: Python Environment: Python 3.12
Line 14: Line 12:
* Software Tools: Crackle
* Software Tools: Crackle


These setup steps for the Ubertooth tool are also summarized here [[Ubertooth - Build Guide - https://ubertooth.readthedocs.io/en/latest/build_guide.html]].
For the setup this guide was used and adjusted to the given scenario. [[https://ubertooth.readthedocs.io/en/latest/build_guide.html Ubertooth - Build Guide]].


== Description ==
== Description ==
Line 23: Line 21:


  /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
  /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
echo bar
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zshrc
  eval "$(/opt/homebrew/bin/brew shellenv)"
  eval "$(/opt/homebrew/bin/brew shellenv)"


=== Step 2 - Install Python Environment ===
=== Step 2 - Install Python Environment ===
Line 40: Line 35:


  brew install libusb wget cmake pkg-config
  brew install libusb wget cmake pkg-config


=== Step 4 - Install the Bluetooth baseband library ===
=== Step 4 - Install the Bluetooth baseband library ===
Line 58: Line 52:
  pip install setuptools
  pip install setuptools


After installing the setuptools also change the "from distutils import setup" line in the setup.py and setup.py.in file if necessary. As well as the version within setup().
After installing the setuptools also change the "from distutils import setup" line in the the libbtbb-2020-12-R1/build/python/pcaptools/setup.py file to this:


  from setuptools import setup
  from setuptools import setup
Also update the version in that same file to this:


  version = '2020.12.R1'
  version = '2020.12.R1'


If necessary set the pythonpath in case you have different python versions:
export PYTHONPATH=/Users/<username>/myenv/lib/python3.12/site-packages:$PYTHONPATH


sudo make install
Then retry:


If you get errors regarding the environment variables showing that your library wasnt found you need to set the library path via the MacOs's dynamic linker.
make


export DYLD_LIBRARY_PATH=/usr/local/lib:$DYLD_LIBRARY_PATH
sudo make install


or make it persistent in shell configurations:
=== Step 5 - Install the Ubertooth repository ===
 
~/.zshrc


For this part you can attach the Ubertooth One to the MacBook. BUT DO NOT USE WITHOUT ANTENNA! More information can be found here: [[https://ubertooth.readthedocs.io/en/latest/ubertooth_one.html Ubertooth - Read the docs]]


=== Step 5 - Install the Ubertooth repository ===


Enter these commands in the shell
Enter these commands in the shell
Line 87: Line 84:
  cmake ..
  cmake ..
  make
  make
If libusb module not found errors occur go to ubertooth-2020-12-R1/host/CMakeLists.txt an add your libusb-path manually:
include_directories(/opt/homebrew/include)
link_directories(/opt/homebrew/lib)
Additionally again change the "from distutils import setup" line in the the ubertooth-2020-12-R1/host/build/python/specan_ui/setup.py file to this:
from setuptools import setup
Also update the version in that same file to this:
version = '2020.12.R1'
Then retry:
make
  sudo make install
  sudo make install


If you get errors regarding the environment variables showing that your library wasnt found you need to set the library path via the MacOs's dynamic linker.
export DYLD_LIBRARY_PATH="/usr/local/lib:$DYLD_LIBRARY_PATH"
or make it persistent in shell configurations:
~/.zshrc
Afterwards confirm the ubertooth library is working correctly:
ubertooth-util
=== Step 6 - Test Ubertooth One commands ===
Test these commands:
ubertooth-util -H
ubertooth-util -v
ubertooth-util -p
ubertooth-util -s
Update Ubertooth firmware in ubertooth-2020-12-R1/ubertooth-one-firmware-bin:
sudo ubertooth-dfu -d bluetooth_rxtx.dfu -r
In case the libubertooth library is not loading check if the paths are correct.
In ubertooth-2020-12-R1/host the following command leads to getting a list of sniffed Bluetooth packets with their Lower Address Parts (LAP):
ubertooth-rx
For collecting Upper Address Parts (UAP) from the captured packets add -z:
ubertooth-rx -z
Let it run for a while and look at the output. It will be summarized as Survey Results at the end of the output. There you can see the significant part of Bluetooth addresses of devices that were scanned.
=== Step 7 - Use BTLE to follow the connection of the Bluetooth device you found. (Replace with device Bluetooth Address) ===
ubertooth-btle -f B0:67:B5:CF:E5:10 -r test.pcap
Or create a pipe for viewing the captures directly in Wireshark:
mkfifo /tmp/pipe
ubertooth-btle -f B0:67:B5:CF:E5:10 -c /tmp/pipe
Find packages that involve the Polar Smartwatch
systime=1728247529 freq=2402 addr=8e89bed6 delta_t=34.320 ms rssi=-60
20 1b c9 28 84 1a 9e a0 02 01 06 03 02 ee fe 0d ff 6b 00 72 08 00 00 00 00 00 00 00 00 47 e9 1f
Advertising / AA 8e89bed6 (valid)/ 27 bytes
    Channel Index: 37
    Type:  ADV_IND
    AdvA:  a0:9e:1a:84:28:c9 (public)
    AdvData: 02 01 06 03 02 ee fe 0d ff 6b 00 72 08 00 00 00 00 00 00 00 00
        Type 01 (Flags)
            00000110
                LE General Discoverable Mode
                BR/EDR Not Supported
        Type 02 (16-bit Service UUIDs, more available)
            feee
        Type ff (Manufacturer Specific Data)
            Company: Polar Electro OY
            Data: 72 08 00 00 00 00 00 00 00 00
    Data:  c9 28 84 1a 9e a0 02 01 06 03 02 ee fe 0d ff 6b 00 72 08 00 00 00 00 00 00 00 00
    CRC:  47 e9 1f
=== Step 8 - Use Crackle to crack packages ===
Enter these commands:
git clone https://github.com/mikeryan/crackle.git
cd crackle
make
Now try cracking your pcap.file


=== Step 6 - Install Crackle ===
./crackle -i <your_file.pcap> -o <your_new_output_file.pcap>
 
In this case it wasnt possible to crack any packages from the smartwatch.
 
=== Step 9 - Install bleak library ===
 
pip install bleak
 
More information can be found in the bleak library documentation. [[https://bleak.readthedocs.io/en/latest/usage.html Bleak library]] It also includes sample scripts which were used in the following setup.
 
=== Step 10 - Write python script for reading device UUID ===


Enter these commands in the shell
Enter these commands in the shell


  python3.12 -m venv myenv
mkdir BLE-scripts
  source myenv/bin/activate
cd BLE-scripts
nano ble_scan.py
 
Write simple script to read device UUID using BleakScaner.discover() method:
import asyncio
from bleak import BleakScanner
async def scan():
    devices = await BleakScanner.discover()
    for d in devices:
        print(d)
asyncio.run(scan())
 
Run a scan:
 
  python3 BLE-scripts/ble_scan.py
 
Here the device UUID of the Polar smartwatch was found:
 
C6C36C4A-7F8F-52B2-2BD7-C4F6690424E7: Polar Vantage M 8428C92D
 
=== Step 11 - Check GATT services  ===
 
With the gathered device UUID check for the devices services:
 
nano BLE-scripts/ble_services.py
 
import asyncio
from bleak import BleakClient
device_uuid = "C6C36C4A-7F8F-52B2-2BD7-C4F6690424E7"
async def connect_to_device(device_uuid):
    async with BleakClient(device_uuid) as client:
        connected = client.is_connected
        print(f"Connected: {connected}")
        services = client.services
        if services is None:
            await client.get_services()
        for service in services:
            print(f"Service: {service}")
loop = asyncio.get_event_loop()
loop.run_until_complete(connect_to_device(device_uuid))
 
Run a services scan:
 
python3 BLE-scripts/ble_services.py
 
Now the service UUIDs that were found are going to be listed like this:
 
Service: 0000180a-0000-1000-8000-00805f9b34fb (Handle: 14): Device Information
Service: 0000feee-0000-1000-8000-00805f9b34fb (Handle: 29): Polar Electro Oy
Service: 0000180d-0000-1000-8000-00805f9b34fb (Handle: 38): Heart Rate
Service: 0000180f-0000-1000-8000-00805f9b34fb (Handle: 42): Battery Service
 
=== Step 12 - Get characteristics UUIDs  ===
 
  nano BLE-scripts/ble_characteristics.py
 
import asyncio
from bleak import BleakClient
device_uuid = "C6C36C4A-7F8F-52B2-2BD7-C4F6690424E7"
async def connect_to_device(device_uuid):
    async with BleakClient(device_uuid) as client:
        connected = client.is_connected
        print(f"Connected: {connected}")
        services = client.services
        if services is None:
            await client.get_services()
        for service in services:
            print(f"Service: {service.uuid} (Handle: {service.handle}): {service.description}")
            for characteristic in service.characteristics:
                print(f"  Characteristic: {characteristic.uuid} (Handle: {characteristic.handle}) - Properties: {cha$
loop = asyncio.get_event_loop()
loop.run_until_complete(connect_to_device(device_uuid))
 
Now run the script:
 
python3 BLE-scripts/ble_characteristics.py


=== Step 7 - Install bleak library ===
Expected output:


Enter these commands in the shell
Service: 0000180a-0000-1000-8000-00805f9b34fb (Handle: 14): Device Information
  Characteristic: 00002a29-0000-1000-8000-00805f9b34fb (Handle: 15) - Properties: ['read']
  Characteristic: 00002a24-0000-1000-8000-00805f9b34fb (Handle: 17) - Properties: ['read']
  Characteristic: 00002a25-0000-1000-8000-00805f9b34fb (Handle: 19) - Properties: ['read']
  Characteristic: 00002a27-0000-1000-8000-00805f9b34fb (Handle: 21) - Properties: ['read']
  Characteristic: 00002a26-0000-1000-8000-00805f9b34fb (Handle: 23) - Properties: ['read']
  Characteristic: 00002a28-0000-1000-8000-00805f9b34fb (Handle: 25) - Properties: ['read']
  Characteristic: 00002a23-0000-1000-8000-00805f9b34fb (Handle: 27) - Properties: ['read']
Service: 0000feee-0000-1000-8000-00805f9b34fb (Handle: 29): Polar Electro Oy
  Characteristic: fb005c51-02e7-f387-1cad-8acd2d8df0c8 (Handle: 30) - Properties: ['write-without-response', 'write', 'notify']
  Characteristic: fb005c52-02e7-f387-1cad-8acd2d8df0c8 (Handle: 33) - Properties: ['notify']
  Characteristic: fb005c53-02e7-f387-1cad-8acd2d8df0c8 (Handle: 36) - Properties: ['write-without-response', 'write']
Service: 0000180d-0000-1000-8000-00805f9b34fb (Handle: 38): Heart Rate
  Characteristic: 00002a37-0000-1000-8000-00805f9b34fb (Handle: 39) - Properties: ['notify']
Service: 0000180f-0000-1000-8000-00805f9b34fb (Handle: 42): Battery Service
  Characteristic: 00002a19-0000-1000-8000-00805f9b34fb (Handle: 43) - Properties: ['read', 'notify']


python3.12 -m venv myenv
=== Step 13 - Read data ===
source myenv/bin/activate


=== Step 8 - Test Ubertooth One ===
Now add a script for reading the actual data. Make sure to use the characteristics UUIDs you have found out.


Enter these commands in the shell
nano BLE-scripts/ble_read.py


  python3.12 -m venv myenv
  import asyncio
  source myenv/bin/activate
from bleak import BleakClient
device_uuid = "C6C36C4A-7F8F-52B2-2BD7-C4F6690424E7"
HEART_RATE_CHAR_UUID = "00002a37-0000-1000-8000-00805f9b34fb"
BATTERY_LEVEL_CHAR_UUID = "00002a19-0000-1000-8000-00805f9b34fb"
async def connect_and_read_data(device_uuid):
    async with BleakClient(device_uuid) as client:
        connected = client.is_connected
        print(f"Connected: {connected}")
        if connected:
            try:
                heart_rate = await client.read_gatt_char(HEART_RATE_CHAR_UUID)
                heart_rate = int(heart_rate_data[1])
                print(f"Heart Rate: {heart_rate} bpm")
            except Exception as e:
                print(f"Failed to read heart rate: {e}")
            try:
                battery_level = await client.read_gatt_char(BATTERY_LEVEL_CHAR_UUID)
                print(f"Battery Level: {int(battery_level[0])}%")
            except Exception as e:
                print(f"Failed to read battery level: {e}")
loop = asyncio.get_event_loop()
  loop.run_until_complete(connect_and_read_data(device_uuid))


=== Step 9 - Read characteristics ===
Run script:


Enter these commands in the shell
python3 BLE-scripts/ble_read.py


python3.12 -m venv myenv
In the output the battery level was succesfully read and printed out. The heart rate couldn't be accessed because it only has the notify property and no option for reading:
source myenv/bin/activate


=== Step 10 - Subscribe to characteristics ===
Connected: True
Failed to read heart rate: Failed to read characteristic 39: Error Domain=CBATTErrorDomain Code=2 "Reading is not permitted." UserInfo={NSLocalizedDescription=Reading is not permitted.}
Battery Level: 16%


Enter these commands in the shell
=== Step 14 - Subscribe to heart rate notifications ===


  python3.12 -m venv myenv
  nano BLE-scripts/ble_subscribe.py
source myenv/bin/activate


=== Step 11 - Manipulate characteristics ===
import asyncio
from bleak import BleakClient
device_uuid = "C6C36C4A-7F8F-52B2-2BD7-C4F6690424E7"
HEART_RATE_CHAR_UUID = "00002a37-0000-1000-8000-00805f9b34fb"
def heart_rate_handler(sender, data):
    heart_rate = int(data[1]) if len(data) > 1 else None
    print(f"Heart Rate from {sender}: {heart_rate} bpm")
async def connect_and_subscribe(device_uuid):
    async with BleakClient(device_uuid) as client:
        connected = client.is_connected
        print(f"Connected: {connected}")
        if connected:
            try:
                await client.start_notify(HEART_RATE_CHAR_UUID, heart_rate_handler)
                print("Subscribed to heart rate notifications.")
                await asyncio.sleep(60)
                await client.stop_notify(HEART_RATE_CHAR_UUID)
                print("Stopped heart rate notifications.")
            except Exception as e:
                print(f"Failed to subscribe to heart rate notifications: {e}")
asyncio.run(connect_and_subscribe(device_uuid))


Enter these commands in the shell
python3 BLE-scripts/ble_subscribe.py


python3.12 -m venv myenv
It was not possible to get the heart rate data even though the notify property was assigned. That is because typically battery status data is on a lower securiy level than heart rate data. More information on GATT can be found here: https://www.bluetooth.com/de/bluetooth-resources/intro-to-bluetooth-gap-gatt/
source myenv/bin/activate


This is the output of the failed try:


Connected: True
Subscribed to heart rate notifications.
Stopped heart rate notifications.


== Used Hardware ==
== Used Hardware ==


[[Device to be used with this documentation]]
* [[Ubertooth One, 2.4 GHz wireless development platform]]
[[Maybe another device to be used with this documentation]]
 
* [[Polar Vantage M]]
 
* [[Apple iPhone 13 mini]]
 
* [[MacBook Air M1, 2020]]
 
* [[Samsung Galaxy J3]]


== Courses ==
== Courses ==


* [[A course where this documentation was used]] (2017, 2018)
* [[Selected IT-Security Chapters]] (2024)
* [[Another one]] (2018)
 
== Author ==
 
Are Maksimović


== References ==
== References ==


* https://wikipedia.org
* https://ubertooth.readthedocs.io/en/latest/
* https://google.com
* https://www.greatscottgadgets.com/ubertoothone/
* https://codemuch.net/posts/bluetooth-hacking/
* https://bleak.readthedocs.io/en/latest/usage.html
* https://www.bluetooth.com/de/bluetooth-resources/intro-to-bluetooth-gap-gatt/
* https://support.polar.com/en/vantage-m#all
* https://wiki.elvis.science/index.php?title=B-LE_-_GATT_Architectural_Overview


[[Category:Documentation]]
[[Category:Documentation]]

Latest revision as of 16:12, 5 December 2024

Summary

This is a step-by-step guide on how to sniff Bluetooth Low Energy (BLE) packets between a Polar Vantage Smartwatch and a phone using the Ubertooth One. It will go into detail on the setup on a MacBook Air with MacOS using a python environment for the libraries that are used for the setup. Additionally every step of the actual sniff will be outlined. This guide is based on the seminar paper "Smartwatch Hacking Attack" that was written within the FH-course "Selected IT-Security Chapters".

Requirements

  • Hardware tools: Ubertooth One: Firmware version: 2020-12-R1 (API:1.07)
  • Unix-based Operating System: macOS (macOS Sequoia 15.1.1), on a MacBook Air with an Apple M1 chip.
  • Virtual Environment: Python Environment: Python 3.12
  • Packages: libusb, wget, cmake, pkg-config, setuptools, pyqt5, numpy
  • Libraries: libbtbb, bleak
  • Software Tools: Crackle

For the setup this guide was used and adjusted to the given scenario. [Ubertooth - Build Guide].

Description

Step 1 - Install homebrew

Enter these commands in the shell

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
eval "$(/opt/homebrew/bin/brew shellenv)"

Step 2 - Install Python Environment

Enter these commands in the shell

python3.12 -m venv myenv
source myenv/bin/activate

Step 3 - Install libusb, wget, cmake, pkg-config

Enter these commands in the shell

brew install libusb wget cmake pkg-config

Step 4 - Install the Bluetooth baseband library

Enter these commands in the shell

wget https://github.com/greatscottgadgets/libbtbb/archive/2020-12-R1.tar.gz -O libbtbb-2020-12-R1.tar.gz
tar -xf libbtbb-2020-12-R1.tar.gz
cd libbtbb-2020-12-R1
mkdir build
cd build
cmake ..
make

If errors occur with installing distutils switch to setuptools instead.

pip install setuptools

After installing the setuptools also change the "from distutils import setup" line in the the libbtbb-2020-12-R1/build/python/pcaptools/setup.py file to this:

from setuptools import setup

Also update the version in that same file to this:

version = '2020.12.R1'

If necessary set the pythonpath in case you have different python versions:

export PYTHONPATH=/Users/<username>/myenv/lib/python3.12/site-packages:$PYTHONPATH

Then retry:

make
sudo make install

Step 5 - Install the Ubertooth repository

For this part you can attach the Ubertooth One to the MacBook. BUT DO NOT USE WITHOUT ANTENNA! More information can be found here: [Ubertooth - Read the docs]


Enter these commands in the shell

wget https://github.com/greatscottgadgets/ubertooth/releases/download/2020-12-R1/ubertooth-2020-12-R1.tar.xz
tar -xf ubertooth-2020-12-R1.tar.xz
cd ubertooth-2020-12-R1/host
mkdir build
cd build
cmake ..
make

If libusb module not found errors occur go to ubertooth-2020-12-R1/host/CMakeLists.txt an add your libusb-path manually:

include_directories(/opt/homebrew/include)
link_directories(/opt/homebrew/lib)

Additionally again change the "from distutils import setup" line in the the ubertooth-2020-12-R1/host/build/python/specan_ui/setup.py file to this:

from setuptools import setup

Also update the version in that same file to this:

version = '2020.12.R1'

Then retry:

make
sudo make install

If you get errors regarding the environment variables showing that your library wasnt found you need to set the library path via the MacOs's dynamic linker.

export DYLD_LIBRARY_PATH="/usr/local/lib:$DYLD_LIBRARY_PATH"

or make it persistent in shell configurations:

~/.zshrc

Afterwards confirm the ubertooth library is working correctly:

ubertooth-util

Step 6 - Test Ubertooth One commands

Test these commands:

ubertooth-util -H
ubertooth-util -v
ubertooth-util -p
ubertooth-util -s

Update Ubertooth firmware in ubertooth-2020-12-R1/ubertooth-one-firmware-bin:

sudo ubertooth-dfu -d bluetooth_rxtx.dfu -r

In case the libubertooth library is not loading check if the paths are correct.

In ubertooth-2020-12-R1/host the following command leads to getting a list of sniffed Bluetooth packets with their Lower Address Parts (LAP):

ubertooth-rx

For collecting Upper Address Parts (UAP) from the captured packets add -z:

ubertooth-rx -z

Let it run for a while and look at the output. It will be summarized as Survey Results at the end of the output. There you can see the significant part of Bluetooth addresses of devices that were scanned.

Step 7 - Use BTLE to follow the connection of the Bluetooth device you found. (Replace with device Bluetooth Address)

ubertooth-btle -f B0:67:B5:CF:E5:10 -r test.pcap

Or create a pipe for viewing the captures directly in Wireshark:

mkfifo /tmp/pipe
ubertooth-btle -f B0:67:B5:CF:E5:10 -c /tmp/pipe

Find packages that involve the Polar Smartwatch

systime=1728247529 freq=2402 addr=8e89bed6 delta_t=34.320 ms rssi=-60
20 1b c9 28 84 1a 9e a0 02 01 06 03 02 ee fe 0d ff 6b 00 72 08 00 00 00 00 00 00 00 00 47 e9 1f 
Advertising / AA 8e89bed6 (valid)/ 27 bytes
    Channel Index: 37
    Type:  ADV_IND
    AdvA:  a0:9e:1a:84:28:c9 (public)
    AdvData: 02 01 06 03 02 ee fe 0d ff 6b 00 72 08 00 00 00 00 00 00 00 00
        Type 01 (Flags)
           00000110
               LE General Discoverable Mode
               BR/EDR Not Supported
        Type 02 (16-bit Service UUIDs, more available)
           feee
        Type ff (Manufacturer Specific Data)
           Company: Polar Electro OY
           Data: 72 08 00 00 00 00 00 00 00 00
    Data:  c9 28 84 1a 9e a0 02 01 06 03 02 ee fe 0d ff 6b 00 72 08 00 00 00 00 00 00 00 00
    CRC:   47 e9 1f

Step 8 - Use Crackle to crack packages

Enter these commands:

git clone https://github.com/mikeryan/crackle.git
cd crackle
make

Now try cracking your pcap.file

./crackle -i <your_file.pcap> -o <your_new_output_file.pcap>

In this case it wasnt possible to crack any packages from the smartwatch.

Step 9 - Install bleak library

pip install bleak

More information can be found in the bleak library documentation. [Bleak library] It also includes sample scripts which were used in the following setup.

Step 10 - Write python script for reading device UUID

Enter these commands in the shell

mkdir BLE-scripts
cd BLE-scripts
nano ble_scan.py

Write simple script to read device UUID using BleakScaner.discover() method:

import asyncio
from bleak import BleakScanner
async def scan():
    devices = await BleakScanner.discover()
    for d in devices:
        print(d)
asyncio.run(scan())

Run a scan:

python3 BLE-scripts/ble_scan.py

Here the device UUID of the Polar smartwatch was found:

C6C36C4A-7F8F-52B2-2BD7-C4F6690424E7: Polar Vantage M 8428C92D

Step 11 - Check GATT services

With the gathered device UUID check for the devices services:

nano BLE-scripts/ble_services.py
import asyncio
from bleak import BleakClient
device_uuid = "C6C36C4A-7F8F-52B2-2BD7-C4F6690424E7"
async def connect_to_device(device_uuid):
    async with BleakClient(device_uuid) as client:
        connected = client.is_connected
        print(f"Connected: {connected}")
        services = client.services
        if services is None:
            await client.get_services()
        for service in services:
            print(f"Service: {service}")
loop = asyncio.get_event_loop()
loop.run_until_complete(connect_to_device(device_uuid))

Run a services scan:

python3 BLE-scripts/ble_services.py

Now the service UUIDs that were found are going to be listed like this:

Service: 0000180a-0000-1000-8000-00805f9b34fb (Handle: 14): Device Information
Service: 0000feee-0000-1000-8000-00805f9b34fb (Handle: 29): Polar Electro Oy
Service: 0000180d-0000-1000-8000-00805f9b34fb (Handle: 38): Heart Rate
Service: 0000180f-0000-1000-8000-00805f9b34fb (Handle: 42): Battery Service

Step 12 - Get characteristics UUIDs

nano BLE-scripts/ble_characteristics.py
import asyncio
from bleak import BleakClient
device_uuid = "C6C36C4A-7F8F-52B2-2BD7-C4F6690424E7"
async def connect_to_device(device_uuid):
    async with BleakClient(device_uuid) as client:
        connected = client.is_connected
        print(f"Connected: {connected}")
        services = client.services
        if services is None:
            await client.get_services()
        for service in services:
            print(f"Service: {service.uuid} (Handle: {service.handle}): {service.description}")
            for characteristic in service.characteristics:
                print(f"  Characteristic: {characteristic.uuid} (Handle: {characteristic.handle}) - Properties: {cha$
loop = asyncio.get_event_loop()
loop.run_until_complete(connect_to_device(device_uuid))

Now run the script:

python3 BLE-scripts/ble_characteristics.py

Expected output:

Service: 0000180a-0000-1000-8000-00805f9b34fb (Handle: 14): Device Information
 Characteristic: 00002a29-0000-1000-8000-00805f9b34fb (Handle: 15) - Properties: ['read']
 Characteristic: 00002a24-0000-1000-8000-00805f9b34fb (Handle: 17) - Properties: ['read']
 Characteristic: 00002a25-0000-1000-8000-00805f9b34fb (Handle: 19) - Properties: ['read']
 Characteristic: 00002a27-0000-1000-8000-00805f9b34fb (Handle: 21) - Properties: ['read']
 Characteristic: 00002a26-0000-1000-8000-00805f9b34fb (Handle: 23) - Properties: ['read']
 Characteristic: 00002a28-0000-1000-8000-00805f9b34fb (Handle: 25) - Properties: ['read']
 Characteristic: 00002a23-0000-1000-8000-00805f9b34fb (Handle: 27) - Properties: ['read']
Service: 0000feee-0000-1000-8000-00805f9b34fb (Handle: 29): Polar Electro Oy
 Characteristic: fb005c51-02e7-f387-1cad-8acd2d8df0c8 (Handle: 30) - Properties: ['write-without-response', 'write', 'notify']
 Characteristic: fb005c52-02e7-f387-1cad-8acd2d8df0c8 (Handle: 33) - Properties: ['notify']
 Characteristic: fb005c53-02e7-f387-1cad-8acd2d8df0c8 (Handle: 36) - Properties: ['write-without-response', 'write']
Service: 0000180d-0000-1000-8000-00805f9b34fb (Handle: 38): Heart Rate
 Characteristic: 00002a37-0000-1000-8000-00805f9b34fb (Handle: 39) - Properties: ['notify']
Service: 0000180f-0000-1000-8000-00805f9b34fb (Handle: 42): Battery Service
 Characteristic: 00002a19-0000-1000-8000-00805f9b34fb (Handle: 43) - Properties: ['read', 'notify']

Step 13 - Read data

Now add a script for reading the actual data. Make sure to use the characteristics UUIDs you have found out.

nano BLE-scripts/ble_read.py
import asyncio
from bleak import BleakClient
device_uuid = "C6C36C4A-7F8F-52B2-2BD7-C4F6690424E7"
HEART_RATE_CHAR_UUID = "00002a37-0000-1000-8000-00805f9b34fb"
BATTERY_LEVEL_CHAR_UUID = "00002a19-0000-1000-8000-00805f9b34fb"
async def connect_and_read_data(device_uuid):
    async with BleakClient(device_uuid) as client:
        connected = client.is_connected
        print(f"Connected: {connected}")
        if connected:
            try:
                heart_rate = await client.read_gatt_char(HEART_RATE_CHAR_UUID)
                heart_rate = int(heart_rate_data[1])
                print(f"Heart Rate: {heart_rate} bpm")
            except Exception as e:
                print(f"Failed to read heart rate: {e}")
            try:
                battery_level = await client.read_gatt_char(BATTERY_LEVEL_CHAR_UUID)
                print(f"Battery Level: {int(battery_level[0])}%")
            except Exception as e:
                print(f"Failed to read battery level: {e}")
loop = asyncio.get_event_loop()
loop.run_until_complete(connect_and_read_data(device_uuid))

Run script:

python3 BLE-scripts/ble_read.py

In the output the battery level was succesfully read and printed out. The heart rate couldn't be accessed because it only has the notify property and no option for reading:

Connected: True
Failed to read heart rate: Failed to read characteristic 39: Error Domain=CBATTErrorDomain Code=2 "Reading is not permitted." UserInfo={NSLocalizedDescription=Reading is not permitted.}
Battery Level: 16%

Step 14 - Subscribe to heart rate notifications

nano BLE-scripts/ble_subscribe.py
import asyncio
from bleak import BleakClient
device_uuid = "C6C36C4A-7F8F-52B2-2BD7-C4F6690424E7"
HEART_RATE_CHAR_UUID = "00002a37-0000-1000-8000-00805f9b34fb"
def heart_rate_handler(sender, data):
    heart_rate = int(data[1]) if len(data) > 1 else None
    print(f"Heart Rate from {sender}: {heart_rate} bpm")
async def connect_and_subscribe(device_uuid):
    async with BleakClient(device_uuid) as client:
        connected = client.is_connected
        print(f"Connected: {connected}")
        if connected:
            try:
                await client.start_notify(HEART_RATE_CHAR_UUID, heart_rate_handler)
                print("Subscribed to heart rate notifications.")
                await asyncio.sleep(60)
                await client.stop_notify(HEART_RATE_CHAR_UUID)
                print("Stopped heart rate notifications.")
            except Exception as e:
                print(f"Failed to subscribe to heart rate notifications: {e}")
asyncio.run(connect_and_subscribe(device_uuid))
python3 BLE-scripts/ble_subscribe.py

It was not possible to get the heart rate data even though the notify property was assigned. That is because typically battery status data is on a lower securiy level than heart rate data. More information on GATT can be found here: https://www.bluetooth.com/de/bluetooth-resources/intro-to-bluetooth-gap-gatt/

This is the output of the failed try:

Connected: True
Subscribed to heart rate notifications.
Stopped heart rate notifications.

Used Hardware

Courses

Author

Are Maksimović

References