Adafruit_CircuitPython_BLE_.../adafruit_ble_heart_rate.py
2020-04-30 00:22:43 -04:00

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"