Search code examples
iosswiftcore-bluetooth

can't get returned values in a structure in swift 3


I'm having this code:

//
//  Measurement.swift
//  BicycleSpeed

    import Foundation
    import CoreBluetooth

    // CSC Measurement
    // https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.csc_measurement.xml
    //
    //  Flags : 1 byte.  Bit 0: Wheel. Bit 1: Crank
    //  Cumulative Wheel revolutions: 4 bytes uint32
    //  Last wheel event time: 2 bytes. uint16 (1/1024s)
    //  Cumulative Crank revolutions: 2 bytes uint16
    //  Last cranck event time: 2 bytes. uint16 (1/1024s)


    struct Measurement : CustomDebugStringConvertible {

      let hasWheel:Bool
      let hasCrank:Bool
      let cumulativeWheel:UInt32
      let lastWheelEventTime:TimeInterval
      let cumulativeCrank:UInt16
      let lastCrankEventTime:TimeInterval
      let wheelSize:UInt32


      init(data:Data, wheelSize:UInt32) {

        self.wheelSize = wheelSize
        // Flags
        var flags:UInt8=0
        (data as NSData).getBytes(&flags, range: NSRange(location: 0, length: 1))

        hasWheel = ((flags & BTConstants.WheelFlagMask) > 0)
        hasCrank = ((flags & BTConstants.CrankFlagMask) > 0)

        var wheel:UInt32=0
        var wheelTime:UInt16=0
        var crank:UInt16=0
        var crankTime:UInt16=0

        var currentOffset = 1
        var length = 0

        if ( hasWheel ) {

          length = MemoryLayout<UInt32>.size
          (data as NSData).getBytes(&wheel, range: NSRange(location: currentOffset, length: length))
          currentOffset += length

          length = MemoryLayout<UInt16>.size
          (data as NSData).getBytes(&wheelTime, range: NSRange(location: currentOffset, length: length))
          currentOffset += length
        }

        if ( hasCrank ) {

          length = MemoryLayout<UInt16>.size
          (data as NSData).getBytes(&crank, range: NSRange(location: currentOffset, length: length))
          currentOffset += length

          length = MemoryLayout<UInt16>.size
          (data as NSData).getBytes(&crankTime, range: NSRange(location: currentOffset, length: length))
          currentOffset += length
        }

        cumulativeWheel     = CFSwapInt32LittleToHost(wheel)
        lastWheelEventTime  = TimeInterval( Double(CFSwapInt16LittleToHost(wheelTime))/BTConstants.TimeScale)
        cumulativeCrank     = CFSwapInt16LittleToHost(crank)
        lastCrankEventTime  = TimeInterval( Double(CFSwapInt16LittleToHost(crankTime))/BTConstants.TimeScale)

      }

      func timeIntervalForCurrentSample( _ current:TimeInterval, previous:TimeInterval ) -> TimeInterval {
        var timeDiff:TimeInterval = 0
        if( current >= previous ) {
            timeDiff = current - previous
        }
        else {
          // passed the maximum value
          timeDiff =  ( TimeInterval((Double( UINT16_MAX) / BTConstants.TimeScale)) - previous) + current
        }
        return timeDiff

      }

      func valueDiffForCurrentSample<T:UnsignedInteger>( _ current:T, previous:T , max:T) -> T {

        var diff:T = 0
        if  ( current >= previous ) {
          diff = current - previous
        }
        else {
           diff = ( max - previous ) + current
        }
        return diff
      }


      func valuesForPreviousMeasurement( _ previousSample:Measurement? ) -> ( cadenceinRPM:Double?, distanceinMeters:Double?, speedInMetersPerSecond:Double?)? {


        var distance:Double?, cadence:Double?, speed:Double?
        guard let previousSample = previousSample else {
          return nil
        }
        if ( hasWheel && previousSample.hasWheel ) {
          let wheelTimeDiff = timeIntervalForCurrentSample(lastWheelEventTime, previous: previousSample.lastWheelEventTime)
          let valueDiff = valueDiffForCurrentSample(cumulativeWheel, previous: previousSample.cumulativeWheel, max: UInt32.max)

          distance = Double( valueDiff * wheelSize) / 1000.0 // distance in meters
          if  distance != nil  &&  wheelTimeDiff > 0 {
            speed = (wheelTimeDiff == 0 ) ? 0 : distance! / wheelTimeDiff // m/s
          }
        }

        if( hasCrank && previousSample.hasCrank ) {
          let crankDiffTime = timeIntervalForCurrentSample(lastCrankEventTime, previous: previousSample.lastCrankEventTime)
          let valueDiff = Double(valueDiffForCurrentSample(cumulativeCrank, previous: previousSample.cumulativeCrank, max: UInt16.max))

          cadence = (crankDiffTime == 0) ? 0 : Double(60.0 * valueDiff / crankDiffTime) // RPM

        }
        print( "Cadence: \(String(describing: cadence)) RPM. Distance: \(String(describing: distance)) meters. Speed: \(String(describing: speed)) Km/h" )
        return ( cadenceinRPM:cadence, distanceinMeters:distance, speedInMetersPerSecond:speed)



      }

      var debugDescription:String {
        get {
            return "Wheel Revs: \(cumulativeWheel). Last wheel event time: \(lastWheelEventTime). Crank Revs: \(cumulativeCrank). Last Crank event time: \(lastCrankEventTime)"
        }
      }

        var myMeasurement = ((Measurement?) -> (cadenceinRPM: Double?, DistanceinMeters: Double?, speedinMetersPerSecond: Double?)).self


        struct dataVariables {

            static var mySpeed = myMeasurement.speedInMetersPerSecond


            static var myCadence : Double?
            static var miDistance : Double?

            static var myLastWheelEventTime : Double? = Measurement.valuesForPreviousMeasurement(lastWheelEventTime)

            static var myLastCrankEventTime : Double? = Measurement.valuesForPreviousMeasurement(lastCrankEventTime)


        }
    }


    }

and I'm trying to assign to static variables inside the struct dataVariables the returned values cadenceinRPM,distanceinMeters,speedinMetersPerSecond
as well as lastWheelEventTime and lastCrankEventTime, so I can access them in another class, but I'm having the following errors: on var mySpeed : Instance member'myMeasurement' cannot be used on type 'Measurement' on var myLastWheelEventTime and var myLastCrankEventTime : Instance member 'valuesForPreviousMeasurement' cannot be used on type 'Measurement' ; did you mean to use a value of this type instead? how can a reference to those returned values than? Can somebody explain the error? I searched for other similar questions but I'm not getting to a solution yet I have tried to change var myVariable to

var myMeasurement = Measurement.self

but the errors stayed the same.

and this other code

    //
//  CadenceSensor.swift

import Foundation
import CoreBluetooth

/*
 // Bluetooth  "Cycling Speed and Cadence"
 https://developer.bluetooth.org/gatt/services/Pages/ServiceViewer.aspx?u=org.bluetooth.service.cycling_speed_and_cadence.xml

 Service Cycling Speed and Cadence. Characteristic [2A5B]  // Measurement
 Service Cycling Speed and Cadence. Characteristic [2A5C]  // Supported Features
 Service Cycling Speed and Cadence. Characteristic [2A5D]  // Sensor location
 Service Cycling Speed and Cadence. Characteristic [2A55]  // Control Point

 */

public struct BTConstants {
    static let CadenceService         = "1816"
    static let CSCMeasurementUUID     = "2a5b"
    static let CSCFeatureUUID         = "2a5c"
    static let SensorLocationUUID     = "2a5d"
    static let ControlPointUUID       = "2a55"
    static let WheelFlagMask:UInt8    = 0b01
    static let CrankFlagMask:UInt8    = 0b10
    static let DefaultWheelSize:UInt32   = UInt32(myVariables.circonferenzaRuota!)  // In millimiters. 700x30 (by default my bike's wheels) :)
    static let TimeScale              = 1024.0
}

protocol CadenceSensorDelegate {

    func errorDiscoveringSensorInformation(_ error:NSError)
    func sensorReady()
    func sensorUpdatedValues( speedInMetersPerSecond speed:Double?, cadenceInRpm cadence:Double?, distanceInMeters distance:Double? )
}

class CadenceSensor: NSObject {

    let peripheral:CBPeripheral
    var sensorDelegate:CadenceSensorDelegate?
    var measurementCharasteristic:CBCharacteristic?
    var lastMeasurement:Measurement?
    let wheelCircunference:UInt32


    init(peripheral:CBPeripheral , wheel:UInt32=BTConstants.DefaultWheelSize) {
        self.peripheral = peripheral
        wheelCircunference = wheel
    }

    func start() {
        self.peripheral.discoverServices(nil)
        self.peripheral.delegate = self
    }


    func stop() {
        if let measurementCharasteristic = measurementCharasteristic {
            peripheral.setNotifyValue(false, for: measurementCharasteristic)
        }

    }

    func handleValueData( _ data:Data ) {

        let measurement = Measurement(data: data, wheelSize: wheelCircunference)
        print("\(measurement)")

        let values = measurement.valuesForPreviousMeasurement(lastMeasurement)
        lastMeasurement = measurement

        sensorDelegate?.sensorUpdatedValues(speedInMetersPerSecond: values?.speedInMetersPerSecond, cadenceInRpm: values?.cadenceinRPM, distanceInMeters: values?.distanceinMeters)
    }
}



extension CadenceSensor : CBPeripheralDelegate {


    func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
        guard error == nil else {
            sensorDelegate?.errorDiscoveringSensorInformation(NSError(domain: CBErrorDomain, code: 0, userInfo: [NSLocalizedDescriptionKey:NSLocalizedString("Error receiving measurements updates", comment:"")]))

            return
        }
        print("notification status changed for [\(characteristic.uuid)]...")
    }

    func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {

        print("Updated [\(characteristic.uuid)]...")

        guard error == nil  , let data = characteristic.value  else {

            return
        }

        handleValueData(data)

    }

    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        guard error == nil  else {
            sensorDelegate?.errorDiscoveringSensorInformation(error! as NSError)
            return
        }
        // Find the cadence service
        guard let cadenceService =  peripheral.services?.filter({ (service) -> Bool in
            return service.uuid == CBUUID(string: BTConstants.CadenceService)
        }).first else {

            sensorDelegate?.errorDiscoveringSensorInformation(NSError(domain: CBErrorDomain, code: NSNotFound, userInfo: [NSLocalizedDescriptionKey:NSLocalizedString("Cadence service not found for this peripheral", comment:"")]))
            return
        }
        // Discover the cadence service characteristics
        peripheral.discoverCharacteristics(nil, for:cadenceService )
        print("Cadence service discovered")

    }

    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {

        guard let characteristics = service.characteristics else {
            sensorDelegate?.errorDiscoveringSensorInformation(NSError(domain: CBErrorDomain, code: NSNotFound, userInfo: [NSLocalizedDescriptionKey:NSLocalizedString("No characteristics found for the cadence service", comment:"")]))
            return

        }

        print("Received characteristics");

        // Enable notifications for the measurement characteristic
        for characteristic in characteristics {

            print("Service \(service.uuid). Characteristic [\(characteristic.uuid)]")

            if characteristic.uuid == CBUUID(string: BTConstants.CSCMeasurementUUID) {

                print("Found measurement characteristic. Subscribing...")
                peripheral.setNotifyValue(true, for: characteristic)
                measurementCharasteristic  = characteristic

            }
        }
        sensorDelegate?.sensorReady()

    }

}

and this other code

    //
//  MainViewController.swift
//  BicycleSpeed


import UIKit
import CoreBluetooth

class MainViewController: UIViewController {


  struct Constants {

    static let ScanSegue = "ScanSegue"
    static let SensorUserDefaultsKey = "lastsensorused"
  }

  var bluetoothManager:BluetoothManager!
  var sensor:CadenceSensor?
  weak var scanViewController:ScanViewController?
  var infoViewController:InfoTableViewController?
  var accumulatedDistance:Double?

  lazy var distanceFormatter:LengthFormatter = {

    let formatter = LengthFormatter()
    formatter.numberFormatter.maximumFractionDigits = 1

    return formatter
  }()

  //@IBOutlet var labelBTStatus:UILabel!
  @IBOutlet var scanItem:UIBarButtonItem!
  @IBOutlet weak var idLabel: UILabel!


  override func viewDidLoad() {

      bluetoothManager = BluetoothManager()
      bluetoothManager.bluetoothDelegate = self
      scanItem.isEnabled = false

  }

  deinit {
    disconnectSensor()
  }

  @IBAction func unwindSegue( _ segue:UIStoryboardSegue ) {
      bluetoothManager.stopScan()
    guard let sensor = (segue as? ScanUnwindSegue)?.sensor else {
      return
    }
    print("Need to connect to sensor \(sensor.peripheral.identifier)")
    connectToSensor(sensor)

  }

  func disconnectSensor( ) {
    if sensor != nil  {
      bluetoothManager.disconnectSensor(sensor!)
      sensor = nil
    }
    accumulatedDistance = nil
  }

  func connectToSensor(_ sensor:CadenceSensor) {

    self.sensor  = sensor
    bluetoothManager.connectToSensor(sensor)
    // Save the sensor ID
    UserDefaults.standard.set(sensor.peripheral.identifier.uuidString, forKey: Constants.SensorUserDefaultsKey)
    UserDefaults.standard.synchronize()

  }
  // TODO: REconnect. Try this every X seconds
  func checkPreviousSensor() {

    guard let sensorID = UserDefaults.standard.object(forKey: Constants.SensorUserDefaultsKey)  as? String else {
      return
    }
    guard let sensor = bluetoothManager.retrieveSensorWithIdentifier(sensorID) else {
      return
    }
    self.sensor = sensor
    connectToSensor(sensor)

  }

  override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if let infoVC = segue.destination as? InfoTableViewController {
        infoViewController = infoVC
    }
    if segue.identifier == Constants.ScanSegue {

        // Scan segue
      bluetoothManager.startScan()
      scanViewController  = (segue.destination as? UINavigationController)?.viewControllers.first as? ScanViewController
    }

  }

}

extension MainViewController : CadenceSensorDelegate {

  func errorDiscoveringSensorInformation(_ error: NSError) {
      print("An error ocurred disconvering the sensor services/characteristics: \(error)")
  }

  func sensorReady() {
    print("Sensor ready to go...")
    accumulatedDistance = 0.0
  }

  func updateSensorInfo() {
    let name = sensor?.peripheral.name ?? ""
    let uuid = sensor?.peripheral.identifier.uuidString ?? ""

    OperationQueue.main.addOperation { () -> Void in
      self.infoViewController?.showDeviceName(name , uuid:uuid )
    }
  }


  func sensorUpdatedValues( speedInMetersPerSecond speed:Double?, cadenceInRpm cadence:Double?, distanceInMeters distance:Double? ) {

    accumulatedDistance? += distance ?? 0
    let distanceText = (accumulatedDistance != nil && accumulatedDistance! >= 1.0) ? distanceFormatter.string(fromMeters: accumulatedDistance!) : "N/A"
    let speedText = (speed != nil) ? distanceFormatter.string(fromValue: speed!*3.6, unit: .kilometer) + NSLocalizedString("/h", comment:"(km) Per hour") : "N/A"
    let cadenceText = (cadence != nil) ? String(format: "%.2f %@",  cadence!, NSLocalizedString("RPM", comment:"Revs per minute") ) : "N/A"

    OperationQueue.main.addOperation { () -> Void in

      self.infoViewController?.showMeasurementWithSpeed(speedText , cadence: cadenceText, distance: distanceText )
    }
  }


}

extension MainViewController : BluetoothManagerDelegate {

  func stateChanged(_ state: CBCentralManagerState) {
    print("State Changed: \(state)")
    var enabled = false
    var title = ""
    switch state {
    case .poweredOn:
        title = "Bluetooth ON"
        enabled = true
        // When the bluetooth changes to ON, try to reconnect to the previous sensor
        checkPreviousSensor()

    case .resetting:
        title = "Reseeting"
    case .poweredOff:
      title = "Bluetooth Off"
    case .unauthorized:
      title = "Bluetooth not authorized"
    case .unknown:
      title = "Unknown"
    case .unsupported:
      title = "Bluetooth not supported"
    }
    infoViewController?.showBluetoothStatusText( title )
    scanItem.isEnabled = enabled
  }


  func sensorConnection( _ sensor:CadenceSensor, error:NSError?) {
      print("")
    guard error == nil else {
      self.sensor = nil
      print("Error connecting to sensor: \(sensor.peripheral.identifier)")
      updateSensorInfo()
      accumulatedDistance = nil
      return
    }
    self.sensor = sensor
    self.sensor?.sensorDelegate = self
    print("Sensor connected. \(String(describing: sensor.peripheral.name)). [\(sensor.peripheral.identifier)]")
    updateSensorInfo()

    sensor.start()
  }


  func sensorDisconnected( _ sensor:CadenceSensor, error:NSError?) {
    print("Sensor disconnected")
    self.sensor = nil
  }

  func sensorDiscovered( _ sensor:CadenceSensor ) {
      scanViewController?.addSensor(sensor)
  }



}

in MainViewController.swift, there is this func sensorUpdatedValues that converts the three values that I want, to strings and initialize the the func showMeasurementWithSpeed ins the InfoTableViewController.swift .

Could I just return the three values inside function sensorUpdateValues instead to be able to store them into new variables?


Solution

  • The better way to solve this is by passing on the sensor object to the InfoTableViewController in the prepare(forSegue:) and then inside InfoTableViewControlleryou can call sensor.lastMeasurement.speedInMetersPerSecond or any other var that is inside there. Since the class is passed on by reference it will retain the data even when you transition to a new ViewController.

      override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if let infoVC = segue.destination as? InfoTableViewController {
        infoVC.sensor = self.sensor
    }
    if segue.identifier == Constants.ScanSegue {
    
        // Scan segue
      bluetoothManager.startScan()
      scanViewController  = (segue.destination as? UINavigationController)?.viewControllers.first as? ScanViewController
    }
    

    } Then of course you can do whatever you want with this data in the new VC( assign the values to labels or whatnot)