The examples in the BlueZ test directory use the dbus-python library which is not the best of python libraries according to https://wiki.python.org/moin/DbusExamples.
A library from that page is pydbus https://pypi.org/project/pydbus/ which seems to work well with the BlueZ DBus API for a GATT client.
I don't have an ESP32 or Heart Rate Sensor, so I will do an example with a BBC micro:bit and hopefully it will be easy to modify to your needs.
The BlueZ DBus API documentation is available at:
https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/adapter-api.txt
https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/device-api.txt
https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/gatt-api.txt
Some useful things to know to get you started:
- The Dbus service for bluez is called
org.bluez
- The Bluetooth adapter on a Raspberry Pi normally has the DBus object path of
/org/bluez/hci0
- The DBus Object path to a device is the adapter path plus the mac address prepended by 'dev_' and the semi-colons replaced with underscores. i.e. 'DE:82:35:E7:43:BE' would be found at
/org/bluez/hci0/dev_DE_82_35_E7_43_BE
This script assumes that the device has already been paired with Raspberry Pi. As the pairing is a one-off provisioning step, it makes things simplier to do it manually. I usually use bluetoothctl
on the command line.
import pydbus
from gi.repository import GLib
# Setup of device specific values
dev_id = 'DE:82:35:E7:43:BE'
btn_a_uuid = 'e95dda90-251d-470a-a062-fa1922dfa9a8'
temp_reading_uuid = 'e95d9250-251d-470a-a062-fa1922dfa9a8'
temp_period_uuid = 'e95d1b25-251d-470a-a062-fa1922dfa9a8'
# DBus object paths
bluez_service = 'org.bluez'
adapter_path = '/org/bluez/hci0'
device_path = f"{adapter_path}/dev_{dev_id.replace(':', '_')}"
bus = pydbus.SystemBus()
adapter = bus.get(bluez_service, adapter_path)
device = bus.get(bluez_service, device_path)
# Assume device has been paired already so can use connect
device.Connect()
# Get commands and properties available
print(dir(adapter))
print(dir(device))
To read and write you need to find the path to the characteristic which is a little more work. Typically you know the UUID of the characteristic you are interested in so to get the value from button A on a BBC micro:bit it would be:
mngr = bus.get(bluez_service, '/')
def get_characteristic_path(dev_path, uuid):
mng_objs = mngr.GetManagedObjects()
for path in mng_objs:
chr_uuid = mng_objs[path].get('org.bluez.GattCharacteristic1', {}).get('UUID')
if path.startswith(dev_path) and chr_uuid == uuid:
return path
char_path = get_characteristic_path(device._path, btn_a_uuid)
btn = bus.get(bluez_service, char_path)
print(btn.ReadValue({}))
# [0]
Writing to a characteristic is similar. Here is an example of reading and writing to the Temperature Period on a BBC micro:bit
tmp_period_path = get_characteristic_path(device._path, temp_period_uuid)
tmp_period = bus.get(bluez_service, tmp_period_path)
print(tmp_period.ReadValue({}))
# Result is:
# [232, 3]
# To get it as an integer:
print(int.from_bytes(tmp_period.ReadValue({}), byteorder='little'))
# 1000
# To write a new value of 1500
new_value = int(1500).to_bytes(2, byteorder='little')
tmp_period.WriteValue(new_value, {})
print(tmp_period.ReadValue({}))
# [220, 5]
device.Disconnect()
If you want to run this in an eventloop with notifications from the remote device then remove the disconnect above and add the following code:
temp_reading_path = get_characteristic_path(device._path, temp_reading_uuid)
temp = bus.get(bluez_service, temp_reading_path)
# Enable eventloop for notifications
def temp_handler(iface, prop_changed, prop_removed):
"""Notify event handler for temperature"""
if 'Value' in prop_changed:
print(f"Temp value: {as_int(prop_changed['Value'])} \u00B0C")
mainloop = GLib.MainLoop()
temp.onPropertiesChanged = temp_handler
temp.StartNotify()
try:
mainloop.run()
except KeyboardInterrupt:
mainloop.quit()
temp.StopNotify()
device.Disconnect()
Hope that is helpful.