1

I am trying to create an iOS app that reads a cadence sensor (Wahoo fitness cadence). This is bluetooth characteristic 0x2A5B (CSC Measurement). In this example, cadence is how fast the pedals rotate on a bike.

I am using the code below in Swift to read the characteristic from the sensor: Version 1:

private func cadence(from characteristic: CBCharacteristic) -> Int {

    guard let characteristicData = characteristic.value else {return -1 }
    let byteArray = [UInt8](characteristicData)
    print(byteArray)

    let firstBitValue = byteArray[1] & 0x01 //set bit 1 (not 0)
    if firstBitValue == 1 { //IF crank revolution data is present, 1==true
        return Int(byteArray[2])
    } else {
        return 0
    }
}

When I print byteArray, I get "[2, 1, 0, 152, 11]". the "2" and "0" never change. the "1" position increases and never decreases. The "152" and "11" positions seem to be completely random, never changing to 0. They do not change either when the crank is completely stopped. When reading the documentation, I expected the "11" to be the last event crank time. But it appears to not change despite how slow I spin the sensor.

How can I use this data to get the cadence from the sensor?

After working with Paul's help, I have made changes to the code and ended up at the result below:

Version 2

    func cadence(from characteristic:CBCharacteristic, previousRotations:Int = 0) -> (rpm:Double, rotations:Int)? {
        guard let characteristicData = characteristic.value else {
            return nil
        }

        let byteArray = [UInt8](characteristicData)
        if  byteArray[0] & 0x02 == 2 {
            // contains cadence data
            let rotations = (Int(byteArray[2]) << 8) + Int(byteArray[1])
            var deltaRotations = rotations - previousRotations
            if deltaRotations < 0 {
                deltaRotations += 65535
            }
            let timeInt = (Int(byteArray[4]) << 8) + Int(byteArray[3])
            let timeMins =  Double(timeInt) / 1024.0 / 60.0
            let rpm = Double(deltaRotations) / timeMins

            return (rpm:rpm, rotations: rotations)
        }
        return nil
}

The RPM's being returned are currently below expected values, at around 53 being the highest, and 3 being the lowest. These values are compared to the sensor developer's app which indicates around 50-70 rpm.

Version 3:

func cadence(from characteristic:CBCharacteristic, previousTime: Int=0, previousRotations:Int = 0) -> (rpm:Double, time: Int, rotations:Int)? {
            guard let characteristicData = characteristic.value else {
                return nil
            }

            let byteArray = [UInt8](characteristicData)
            if  byteArray[0] & 0x02 == 2 {
                // contains cadence data
                let rotations = Int(byteArray[2])<<8 + Int(byteArray[1])
                var deltaRotations = rotations - previousRotations
                if deltaRotations < 0 {
                    deltaRotations += 65535
                }
                let timeInt = Int(byteArray[4])<<8 + Int(byteArray[3])
                var timeDelta = timeInt - previousTime
                if (timeDelta < 0) {
                    timeDelta += 65535
                }

                let timeMins =  Double(timeDelta) / 1024.0 / 60
                let rpm = Double(deltaRotations) / timeMins

                return (rpm:rpm, time: timeInt, rotations: rotations)
            }
            return nil
        }
  • Please don't edit your question to replace the original code with code from an answer; it can confuse other readers. – Paulw11 Jun 10 '18 at 23:37
  • If you want to show new code, add it to the bottom of your question – Paulw11 Jun 10 '18 at 23:40
  • Take a look at this library: https://github.com/erndev/BicycleSpeed – Paulw11 Jun 11 '18 at 01:32
  • After spending several hours debugging and reading the code, that library is simply too complex and not portable enough to work without rewriting it. It was written in a previous version of Swift as well. –  Jun 15 '18 at 12:29
  • Anyone have a solution to this? i'm currently trying to crack the same problem but with React Native. – Alex Jun 09 '19 at 03:30

1 Answers1

1

This is what I ended up doing

func onCSC(from characteristic: CBCharacteristic) -> Double{
    guard let characteristicData = characteristic.value else { return -1 }
    let byteArray = [UInt8](characteristicData)
    let firstBitValue  = byteArray[0] & 0x01 // Bit1 [2] == 0010 & 0000 == 0000 == 0 (Dec) Wheel Rev FALSE (For Spd)
    let secondBitValue = byteArray[0] & 0x02 // Bit2 [2] == 0010 & 0010 == 0010 == 2 (Dec) Crank Rev TRUE  (For Cad)
    
    if firstBitValue > 0 {
      let cumWheelRev   = Int(byteArray[3])<<8 + Int(byteArray[2])<<8 + Int(byteArray[1])<<8 + Int(byteArray[0])
      let lastWheelTime = Int(byteArray[5])<<8 + Int(byteArray[4])
      spdCSC = Double(getWheelRpm(pCumWheelRev: cumWheelRev,
                                  pLastWheelTime: lastWheelTime,
                                  pPrevCumWheelRev: prevCSCCumWheelRev,
                                  pPrevWheelTime: prevCSCWheelTime,
                                  hasPower: false)
                          )        

    // Store the current WheelRev & WheelTime for next iteration
    prevComboCSCCumWheelRev = cumWheelRev
    prevComboCSCWheelTime = lastWheelTime
    }
    
    if secondBitValue > 0 {
      let cumCrankRev   = Int(byteArray[2])<<8 + Int(byteArray[1])
      let lastCrankTime = Int(byteArray[4])<<8 + Int(byteArray[3])
      
      var deltaRotations = cumCrankRev - prevCumCrankRev
      if deltaRotations < 0 { deltaRotations += 65535 }
      
      var timeDelta = lastCrankTime - prevCrankTime
      if (timeDelta < 0) { timeDelta += 65535 }
      // In Case Cad Drops, we use PrevRPM 
      // to substitute (up to 2 seconds before reporting 0)
      if (timeDelta != 0) {
        prevCrankStaleness = 0
        let timeMins =  Double(timeDelta) / 1024.0 / 60
        rpm = Double(deltaRotations) / timeMins
        prevRPM = rpm
      } else if (timeDelta == 0 && prevCrankStaleness < 2 ) {
        rpm = prevRPM
        prevCrankStaleness += 1
      } else if (prevCrankStaleness >= 2) {
        rpm = 0.0
      }

      prevCumCrankRev = cumCrankRev
      prevCrankTime = lastCrankTime
      return rpm
    }
    return -1
  }




func getWheelRpm( pCumWheelRev: Int, pLastWheelTime: Int, pPrevCumWheelRev: Int, pPrevWheelTime: Int, hasPower: Bool ) -> Double {
    let cumWheelRev = pCumWheelRev
    let lastWheelTime = pLastWheelTime
    let prevCumWheelRev = pPrevCumWheelRev
    let prevWheelTime = pPrevWheelTime
    var wheelRpm: Double = -1.0
    
    let deltaRotations = cumWheelRev - prevCumWheelRev
    var timeDelta = lastWheelTime - prevWheelTime
    if (timeDelta < 0) { timeDelta += 65535 }
    
    if (timeDelta != 0 && prevCumWheelRev != 0) { // Ignore the first data point cos prevCumWheeRev is always 0
      // Alternate Calculation
      // let timeMins =  Double(timeDelta) / 1024.0 / 60
      // rpm1 = Double(deltaRotations) / timeMins
      wheelRpm =  (hasPower ? 2048.0 : 1024.0) * (Double(deltaRotations) * 60.0) / Double(timeDelta)
    }
    //    print("1 getWheelRpm wheelRpm:\(wheelRpm) deltaRotations:\(deltaRotations)[\(cumWheelRev) - \(prevCumWheelRev)] timeDelta:\(timeDelta) [\(lastWheelTime) - \(prevWheelTime)]")
    
    return wheelRpm
  }
}

According to the specs from bluetooth.com

  //  byte1 uint8: flags
  //   bit 0 = 1: Wheel Revolution Data is present
  //   bit 1 = 1: Crank Revolution Data is present
  //
  //   byte2/3 The next two fields are present only if bit 0 above is 1:
  //    uint32: Cumulative Wheel Revolutions
  //    uint16: Last Wheel Event Time, in 1024ths of a second
  //
  //   byte 3/4 The next two fields are present only if bit 10 above is 1:
  //    uint16: Cumulative Crank Revolutions
  //    uint16: Last Crank Event Time, in 1024ths of a second

  //    Flag       : 2 (0x2)
  //    CumWheel   : 6 (0x6)
  //    LastWheel  : 0 (0x0)
  //    CumCrank   : 231 (0xe7)
  //    LastCrankTm: 30 (0x1e)
app4g
  • 670
  • 4
  • 24
  • Curious if you were ever able to come up with a speed calculation, or are you still waiting to revisit later? :) – Zachary Bell Oct 28 '22 at 18:34
  • 1
    @ZacharyBell updated the code.. it's some-what different from my actual code as The above does not take into account Speed + Cadence COMBO sensors.. This is for my app -> https://apple.co/3uWdtxh – app4g Oct 30 '22 at 08:24
  • Thanks for filling in the gaps. Actually based on another SO question you answered I was able to pull some info from golden cheetahs code to write something close to what you have here. I'm also working on an indoor training app. Trying to come up with a UI optimized for iPads. – Zachary Bell Oct 31 '22 at 11:57