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.