@jonnyrider that’s such a bummer, would have been a really good addition!
so all UUIDs are working for you? and it’s only the issue that the numbers make no sense?
Yeah, most of the UUIDs bring back figures.
The problem is that the current implementation of the BLE client in ESPHome only implements a sensor and a switch, and the former interprets the characteristic as a float
number. For the battery service, this works, as the Battery Level characteristic just returns a number.
But if you read the specification of the Heart Rate Service, you’ll see that the Heart Rate Measurement characteristic is more complex than just a number. There’s a byte with some flags, the next one or two bytes is the heart rate, and then come some other bytes. For instance, here’s the relevant code in a non-ESPHome project I made that reads the heart rate value from a heart rate sensor:
To be able to read these values in ESPHome, the BLE client in ESPHome should be extended with another sensor component that reads the raw bytes from the characteristic and returns these as an array, comparable to what the BLE tracker component is doing now. Then you could add a lambda which gets the heart rate value in x[1]
, as it’s the second byte in the array.
Just pulled the spec for cycling power (the other sensor I tried) and it is the same story there…
The ble_client
component is designed to be pretty generic and extendable.
The initial inclusion of the sensor reads pure numeric values, though extending it to heart rate sensors and other non numeric formats seems easy.
However, given that heart rate sensors have standard uuids, it seems worthwhile to create a new ble_heartrate
component that only needs to have a ble_client
reference.
The same goes for other standard BLE types.
thanks for your assessment on this! As I am very much a beginner in programming I tried to look trough the files:
ble_client.cpp and ble_client.h those would be the ones to adapt, correct?
@buxtronix If I am trying to work on this a pull request would be the way to go?
@jeti your best bet is to look through the BLE sensor code, it fetches numerical values, and you can adapt to what’s returned by your device.
You can also look at Support for AM43 BLE blind motors by buxtronix · Pull Request #1744 · esphome/esphome · GitHub for another ble client based component as an example.
Now that pull request #1851 (Add optional lambda to BLESensor for raw data parsing) has been merged in esphome:dev, you can do this to monitor the heart rate if you install the esphome development version (pip3 install https://github.com/esphome/esphome/archive/dev.zip
):
esp32_ble_tracker:
ble_client:
- mac_address: AA:BB:CC:DD:EE:FF
id: heart_rate_monitor
sensor:
- platform: ble_client
ble_client_id: heart_rate_monitor
id: heart_rate_measurement
name: "Heart rate measurement"
service_uuid: '180d'
characteristic_uuid: '2a37'
notify: true
lambda: |-
uint16_t heart_rate_measurement = x[1];
if (x[0] & 1) {
heart_rate_measurement += (x[2] << 8);
}
return (float)heart_rate_measurement;
icon: 'mdi:heart'
unit_of_measurement: 'bpm'
I just tested this on my fitness tracker, and this works nicely, updating my heart rate every second.
Unfortunately trying to show the information on a display isn’t possible yet, as the BLE and display components together use too much memory (see issues #1336, #1731 and #2045). I tried this both on an M5Stack Core and a LilyGO TTGO T-Display, and both fail to get working Wi-Fi and end up in a boot loop. Otherwise it would be a really cool and much more maintainable solution than my Arduino code to create a heart rate monitor!
So I just created this project to display the heart rate on an M5Stack Core or a LilyGO TTGO T-Display ESP32:
It works if I disable Wi-Fi, and I don’t mind for this purpose. The result looks like this:
Or this:
this works like a charm! thanks for pointing this out!
Could you elaborate on what the code in your lambda is doing? I’ve tried sorting it out but I’m just not sure what it’s doing. I have a project I’m working on that deals with raw data processing on a BLE device and depending on what your code is doing, it could be very useful to my project.
heart_rate_measurement
name: "tickr.heartrate"
service_uuid: '180D'
characteristic_uuid: '2A37'
notify: true
lambda: |-
uint16_t heart_rate_measurement = x[1];
if (x[0] & 1) {
heart_rate_measurement += (x[2] << 8);
}
return (float)heart_rate_measurement;
icon: 'mdi:heart'
unit_of_measurement: 'bpm'
- platform: ble_client
ble_client_id: tickr_fit
name: "tickr.battery"
service_uuid: '180F'
characteristic_uuid: '2A19'
- platform: ble_rssi
mac_address: F0:13:C3:B1:AC:67
name: "tickr.RSSI"
ble_client:
- mac_address: F0:13:C3:B1:AC:67
id: tickr
My puzzle is “what does this do”
My guess is that x is the data pointed to by service 180d and char 2a37
x[0], x[1] and x[2] are the first three bytes (?) of the data.
The heart rate is x[1]
plus (if x[0] is 1) x[2] processed by whatever << 8
does.
I honestly just copied from koans yaml…
Sure! x
is a vector of bytes with the raw data read from the characteristic 2A37. Then x[0]
is the first byte, x[1]
is the second byte, and so on.
If you read the specification of the Heart Rate Service , you’ll see that the Heart Rate Measurement characteristic has a specific format. The first byte has some flags (see section 3.1.1.1), with bit 0 the “Heart Rate Value Format bit”:
The Heart Rate Value Format bit (bit 0 of the Flags field) indicates if the data format of
the Heart Rate Measurement Value field is in a format of UINT8 or UINT16.
When the Heart Rate Value format is sent in a UINT8 format, the Heart Rate Value
Format bit shall be set to 0. When the Heart Rate Value format is sent in a UINT16
format, the Heart Rate Value Format bit shall be set to 1.
With x[0] & 1
(because 20 = 1) I test whether the rightmost bit (bit 0) of this byte is set to 1.
The second byte read from the characteristic is a byte with the heart rate, and if the Heart Rate Value Format bit is 1, the third byte is the next byte of the heart rate.
If the heart rate is measured with two bytes, x[2]
has to be shifted 8 bits to the left, which is what x[2] << 8
does. The result is a 16-bit integer value constructed from the two bytes, with x[2]
the leftmost byte and x[1]
the rightmost byte.
On page 133 of the GATT Specification Supplement you can find a more structured depiction of the characteristic:
By the way, this forum thread inspired me to write a blog post a while ago about connecting to BLE devices with ESPHome. You’ll find some more background here:
Well you certainly improved my understanding, so thank you.
Thank you for the fantastic explanation! I will definitely be able to utilize your code for one of my projects.
Still working as of ESPhome 2023.7!
Just connected this with a plug to turn on my Zwift fan based on a heart rate threshold, giving me a chance to warm up before it starts