/ adafruit_ble_cycling_speed_and_cadence.py
adafruit_ble_cycling_speed_and_cadence.py
1 # The MIT License (MIT) 2 # 3 # Copyright (c) 2020 Dan Halbert for Adafruit Industries LLC 4 # 5 # Permission is hereby granted, free of charge, to any person obtaining a copy 6 # of this software and associated documentation files (the "Software"), to deal 7 # in the Software without restriction, including without limitation the rights 8 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 # copies of the Software, and to permit persons to whom the Software is 10 # furnished to do so, subject to the following conditions: 11 # 12 # The above copyright notice and this permission notice shall be included in 13 # all copies or substantial portions of the Software. 14 # 15 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 # THE SOFTWARE. 22 """ 23 `adafruit_ble_cycling_speed_and_cadence` 24 ================================================================================ 25 26 BLE Cycling Speed and Cadence Service 27 28 29 * Author(s): Dan Halbert for Adafruit Industries 30 31 The Cycling Speed and Cadence Service is specified here: 32 https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Services/org.bluetooth.service.cycling_speed_and_cadence.xml 33 34 Implementation Notes 35 -------------------- 36 37 **Hardware:** 38 39 * Adafruit CircuitPython firmware for the supported boards: 40 https://github.com/adafruit/circuitpython/releases 41 * Adafruit's BLE library: https://github.com/adafruit/Adafruit_CircuitPython_BLE 42 """ 43 import struct 44 from collections import namedtuple 45 46 import _bleio 47 from adafruit_ble.services import Service 48 from adafruit_ble.uuid import StandardUUID 49 from adafruit_ble.characteristics import Characteristic, ComplexCharacteristic 50 from adafruit_ble.characteristics.int import Uint8Characteristic 51 52 __version__ = "0.0.0-auto.0" 53 __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BLE_Cycling_Speed_and_Cadence.git" 54 55 CSCMeasurementValues = namedtuple( 56 "CSCMeasurementValues", 57 ( 58 "cumulative_wheel_revolutions", 59 "last_wheel_event_time", 60 "cumulative_crank_revolutions", 61 "last_crank_event_time", 62 ), 63 ) 64 """Namedtuple for measurement values. 65 66 .. :attribute:: cumulative_wheel_revolutions: 67 68 Cumulative wheel revolutions (int). 69 70 .. :attribute:: last_wheel_event_time: 71 72 Time (int), units in 1024ths of a second, when last wheel event was measured. 73 This is a monotonically increasing clock value, not an interval. 74 75 .. :attribute:: cumulative_crank_revolutions: 76 77 Cumulative crank revolutions (int). 78 79 .. :attribute:: last_crank_event_time: 80 81 Time (int), units in 1024ths of a second, when last crank event was measured. 82 This is a monotonically increasing clock value, not an interval. 83 84 For example:: 85 86 wheel_revs = svc.csc_measurement_values.cumulative_wheel_revolutions 87 """ 88 89 90 class _CSCMeasurement(ComplexCharacteristic): 91 """Notify-only characteristic of speed and cadence data.""" 92 93 uuid = StandardUUID(0x2A5B) 94 95 def __init__(self): 96 super().__init__(properties=Characteristic.NOTIFY) 97 98 def bind(self, service): 99 """Bind to a CyclingSpeedAndCadenceService.""" 100 bound_characteristic = super().bind(service) 101 bound_characteristic.set_cccd(notify=True) 102 # Use a PacketBuffer that can store one packet to receive the SCS data. 103 return _bleio.PacketBuffer(bound_characteristic, buffer_size=1) 104 105 106 class CyclingSpeedAndCadenceService(Service): 107 """Service for reading from a Cycling Speed and Cadence sensor.""" 108 109 # 0x180D is the standard HRM 16-bit, on top of standard base UUID 110 uuid = StandardUUID(0x1816) 111 112 # Mandatory. 113 csc_measurement = _CSCMeasurement() 114 115 csc_feature = Uint8Characteristic( 116 uuid=StandardUUID(0x2A5C), properties=Characteristic.READ 117 ) 118 sensor_location = Uint8Characteristic( 119 uuid=StandardUUID(0x2A5D), properties=Characteristic.READ 120 ) 121 122 sc_control_point = Characteristic( 123 uuid=StandardUUID(0x2A39), properties=Characteristic.WRITE 124 ) 125 126 _SENSOR_LOCATIONS = ( 127 "Other", 128 "Top of shoe", 129 "In shoe", 130 "Hip", 131 "Front Wheel", 132 "Left Crank", 133 "Right Crank", 134 "Left Pedal", 135 "Right Pedal", 136 "Front Hub", 137 "Rear Dropout", 138 "Chainstay", 139 "Rear Wheel", 140 "Rear Hub", 141 "Chest", 142 "Spider", 143 "Chain Ring", 144 ) 145 146 def __init__(self, service=None): 147 super().__init__(service=service) 148 # Defer creating buffer until we're definitely connected. 149 self._measurement_buf = None 150 151 @property 152 def measurement_values(self): 153 """All the measurement values, returned as a CSCMeasurementValues 154 namedtuple. 155 156 Return ``None`` if no packet has been read yet. 157 """ 158 # uint8: flags 159 # bit 0 = 1: Wheel Revolution Data is present 160 # bit 1 = 1: Crank Revolution Data is present 161 # 162 # The next two fields are present only if bit 0 above is 1: 163 # uint32: Cumulative Wheel Revolutions 164 # uint16: Last Wheel Event Time, in 1024ths of a second 165 # 166 # The next two fields are present only if bit 10 above is 1: 167 # uint16: Cumulative Crank Revolutions 168 # uint16: Last Crank Event Time, in 1024ths of a second 169 # 170 171 if self._measurement_buf is None: 172 self._measurement_buf = bytearray( 173 self.csc_measurement.incoming_packet_length # pylint: disable=no-member 174 ) 175 buf = self._measurement_buf 176 packet_length = self.csc_measurement.readinto(buf) # pylint: disable=no-member 177 if packet_length == 0: 178 return None 179 flags = buf[0] 180 next_byte = 1 181 182 if flags & 0x1: 183 wheel_revs = struct.unpack_from("<L", buf, next_byte)[0] 184 wheel_time = struct.unpack_from("<H", buf, next_byte + 4)[0] 185 next_byte += 6 186 else: 187 wheel_revs = wheel_time = None 188 189 if flags & 0x2: 190 # Note that wheel revs is uint32 and and crank revs is uint16. 191 crank_revs = struct.unpack_from("<H", buf, next_byte)[0] 192 crank_time = struct.unpack_from("<H", buf, next_byte + 2)[0] 193 else: 194 crank_revs = crank_time = None 195 196 return CSCMeasurementValues(wheel_revs, wheel_time, crank_revs, crank_time) 197 198 @property 199 def location(self): 200 """The location of the sensor on the cycle, as a string. 201 202 Possible values are: 203 "Other", "Top of shoe", "In shoe", "Hip", 204 "Front Wheel", "Left Crank", "Right Crank", 205 "Left Pedal", "Right Pedal", "Front Hub", 206 "Rear Dropout", "Chainstay", "Rear Wheel", 207 "Rear Hub", "Chest", "Spider", "Chain Ring") 208 "Other", "Chest", "Wrist", "Finger", "Hand", "Ear Lobe", "Foot", 209 and "InvalidLocation" (if value returned does not match the specification). 210 """ 211 212 try: 213 return self._SENSOR_LOCATIONS[self.sensor_location] 214 except IndexError: 215 return "InvalidLocation" 216 217 218 # def set_cumulative_wheel_revolutions(self, value): 219 # self._control_point_request(self.pack("<BLBB", 1, value, 0, )