/ 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, )