211 lines
7.7 KiB
Python
211 lines
7.7 KiB
Python
# The MIT License (MIT)
|
|
#
|
|
# Copyright (c) 2020 Dan Halbert for Adafruit Industries LLC
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
# of this software and associated documentation files (the "Software"), to deal
|
|
# in the Software without restriction, including without limitation the rights
|
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
# copies of the Software, and to permit persons to whom the Software is
|
|
# furnished to do so, subject to the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be included in
|
|
# all copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
# THE SOFTWARE.
|
|
"""
|
|
`adafruit_ble_heart_rate`
|
|
================================================================================
|
|
|
|
BLE Heart Rate Service
|
|
|
|
|
|
* Author(s): Dan Halbert for Adafruit Industries
|
|
|
|
The Heart Rate Service is specified here:
|
|
https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Services/org.bluetooth.service.heart_rate.xml
|
|
|
|
Implementation Notes
|
|
--------------------
|
|
|
|
**Hardware:**
|
|
|
|
* Adafruit CircuitPython firmware for the supported boards:
|
|
https://github.com/adafruit/circuitpython/releases
|
|
* Adafruit's BLE library: https://github.com/adafruit/Adafruit_CircuitPython_BLE
|
|
"""
|
|
import struct
|
|
from collections import namedtuple
|
|
|
|
import _bleio
|
|
from adafruit_ble.services import Service
|
|
from adafruit_ble.uuid import StandardUUID
|
|
from adafruit_ble.characteristics import Characteristic, ComplexCharacteristic
|
|
from adafruit_ble.characteristics.int import Uint8Characteristic
|
|
|
|
__version__ = "0.0.0-auto.0"
|
|
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BLE_Heart_Rate.git"
|
|
|
|
HeartRateMeasurementValues = namedtuple(
|
|
"HeartRateMeasurementValues",
|
|
("heart_rate", "contact", "energy_expended", "rr_intervals"),
|
|
)
|
|
"""Namedtuple for measurement values.
|
|
|
|
* `HeartRateMeasurementValues.heart_rate`
|
|
|
|
Heart rate (int), in beats per minute.
|
|
|
|
* `HeartRateMeasurementValues.contact`
|
|
|
|
``True`` if device is contacting the body, ``False`` if not,
|
|
``None`` if device does not support contact detection.
|
|
|
|
* `HeartRateMeasurementValues.energy_expended`
|
|
|
|
Energy expended (int), in kilo joules, or ``None`` if no value.
|
|
|
|
* `HeartRateMeasurementValues.rr_intervals`
|
|
|
|
Sequence of RR intervals, measuring the time between
|
|
beats. Oldest first, in ints that are units of 1024ths of a second.
|
|
This sequence will be empty if the device does not report the intervals.
|
|
*Caution:* inexpensive heart rate monitors may not measure this
|
|
accurately. Do not use for diagnosis.
|
|
|
|
For example::
|
|
|
|
bpm = svc.measurement_values.heart_rate
|
|
"""
|
|
|
|
|
|
class _HeartRateMeasurement(ComplexCharacteristic):
|
|
"""Notify-only characteristic of streaming heart rate data."""
|
|
|
|
uuid = StandardUUID(0x2A37)
|
|
|
|
def __init__(self):
|
|
super().__init__(properties=Characteristic.NOTIFY)
|
|
|
|
def bind(self, service):
|
|
"""Bind to a HeartRateService."""
|
|
bound_characteristic = super().bind(service)
|
|
bound_characteristic.set_cccd(notify=True)
|
|
# Use a PacketBuffer that can store one packet to receive the HRM data.
|
|
return _bleio.PacketBuffer(bound_characteristic, buffer_size=1)
|
|
|
|
|
|
class HeartRateService(Service):
|
|
"""Service for reading from a Heart Rate sensor."""
|
|
|
|
# 0x180D is the standard HRM 16-bit, on top of standard base UUID
|
|
uuid = StandardUUID(0x180D)
|
|
|
|
# uint8: flags
|
|
# bit 0 = 0: Heart Rate Value is uint8
|
|
# bit 0 = 1: Heart Rate Value is uint16
|
|
# bits 2:1 = 0 or 1: Sensor Contact Feature not supported
|
|
# bits 2:1 = 2: Sensor Contact Feature supported, contact is not detected
|
|
# bits 2:1 = 3: Sensor Contact Feature supported, contacted is detected
|
|
# bit 3 = 0: Energy Expended field is not present
|
|
# bit 3 = 1: Energy Expended field is present. Units: kilo Joules
|
|
# bit 4 = 0: RR-Interval values are not present
|
|
# bit 4 = 1: One or more RR-Interval values are present
|
|
#
|
|
# next uint8 or uint16: Heart Rate Value
|
|
# next uint16: Energy Expended, if present
|
|
# next uint16 (multiple): RR-Interval values, resolution of 1/1024 second
|
|
# in order of oldest to newest
|
|
#
|
|
# Mandatory for Heart Rate Service
|
|
heart_rate_measurement = _HeartRateMeasurement()
|
|
# Optional for Heart Rate Service.
|
|
body_sensor_location = Uint8Characteristic(
|
|
uuid=StandardUUID(0x2A38), properties=Characteristic.READ
|
|
)
|
|
|
|
# Mandatory only if Energy Expended features is supported.
|
|
heart_rate_control_point = Uint8Characteristic(
|
|
uuid=StandardUUID(0x2A39), properties=Characteristic.WRITE
|
|
)
|
|
|
|
_BODY_LOCATIONS = ("Other", "Chest", "Wrist", "Finger", "Hand", "Ear Lobe", "Foot")
|
|
|
|
def __init__(self, service=None):
|
|
super().__init__(service=service)
|
|
# Defer creating buffer until needed.
|
|
self._measurement_buf = None
|
|
|
|
@property
|
|
def measurement_values(self):
|
|
"""All the measurement values, returned as a HeartRateMeasurementValues
|
|
namedtuple.
|
|
|
|
Return ``None`` if no packet has been read yet.
|
|
"""
|
|
if self._measurement_buf is None:
|
|
self._measurement_buf = bytearray(
|
|
self.heart_rate_measurement.packet_size # pylint: disable=no-member
|
|
)
|
|
buf = self._measurement_buf
|
|
packet_length = self.heart_rate_measurement.readinto( # pylint: disable=no-member
|
|
buf
|
|
)
|
|
if packet_length == 0:
|
|
return None
|
|
flags = buf[0]
|
|
next_byte = 1
|
|
|
|
if flags & 0x1:
|
|
bpm = struct.unpack_from("<H", buf, next_byte)[0]
|
|
next_byte += 2
|
|
else:
|
|
bpm = struct.unpack_from("<B", buf, next_byte)[0]
|
|
next_byte += 1
|
|
|
|
if flags & 0x4:
|
|
# True or False if Sensor Contact Feature is supported.
|
|
contact = bool(flags & 0x2)
|
|
else:
|
|
# None (meaning we don't know) if Sensor Contact Feature is not supported.
|
|
contact = None
|
|
|
|
if flags & 0x8:
|
|
energy_expended = struct.unpack_from("<H", buf, next_byte)[0]
|
|
next_byte += 2
|
|
else:
|
|
energy_expended = None
|
|
|
|
rr_values = []
|
|
if flags & 0x10:
|
|
for offset in range(next_byte, packet_length, 2):
|
|
rr_val = struct.unpack_from("<H", buf, offset)[0]
|
|
rr_values.append(rr_val)
|
|
|
|
return HeartRateMeasurementValues(bpm, contact, energy_expended, rr_values)
|
|
|
|
@property
|
|
def location(self):
|
|
"""The location of the sensor on the human body, as a string.
|
|
|
|
Note that the specification describes a limited number of locations.
|
|
But the sensor manufacturer may specify using a non-standard location.
|
|
For instance, some armbands are meant to be worn just below the inner elbow,
|
|
but that is not a prescribed location. So the sensor will report something
|
|
else, such as "Wrist".
|
|
|
|
Possible values are:
|
|
"Other", "Chest", "Wrist", "Finger", "Hand", "Ear Lobe", "Foot", and
|
|
"InvalidLocation" (if value returned does not match the specification).
|
|
"""
|
|
|
|
try:
|
|
return self._BODY_LOCATIONS[self.body_sensor_location]
|
|
except IndexError:
|
|
return "InvalidLocation"
|