tchigher / swift-apple-watch-heart-rate-pubnub-eon

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

This demo uses the Watch’s HealthKit to access the user’s heart rate data, publish it to PubNub, and then uses a Raspberry Pi as a medium through which LEDs light up based on heart rate. The iPhone app displays a realtime EON chart, and uses Twitter’s fabric to use their API as a way to login, use that Twitter handle as a unique ID, and let the user tweet at the end of a workout session (which must be instantiated in order to access heart rate data.)

apple-watch-heart-rate-monitor-raspberry-pi-eon

Installing PubNub with CocoaPods

If you have never installed a Pod before, a really good tutorial on how to get started can be found on the CocoaPods official website. Once a podfile is setup, it should contain something like this:
source 'https://github.com/CocoaPods/Specs.git'
use_frameworks!

target 'yourProject' do platform :ios, '8.0' pod 'PubNub', '~>4.0' pod 'Crashlytics' end

target 'yourWatchProject' do platform :watchos, '2.2' pod 'PubNub', '~> 4.0' end

target 'yourWatchProject Extension' do platform :watchos, '2.2' pod 'PubNub', '~>4.0' end

Setting up Fabric

To set up Fabric for your app, follow the directions here. You can use Pods or download the Fabric OS X app.

compose-tweet-fabric-swift-ios

Phone App

AppDelegate

In your AppDelegate file, besides importing the Fabric, PubNub, and TwitterKit libraries, import:

HealthKit, to give you access to methods you need to access and work with the heart rate data; WatchConnectivity, so your phone app can receive data from the watch.

The AppDelegate class should inherit from UIResponder, UIApplicationDelegate, PNObjectEventListener, and WCSessionDelegate.

WCSession contains a property observer that, when triggered, tries to unwrap the session. If successful in unwrapping it, it sets the session’s delegate, before activating it.

 var session: WCSession? {
    didSet {
        if let session = session {
            session.delegate = self
            session.activateSession()
        }
    }
}

Main.storyboard and TwitterViewController

animatedheart

To display a Twitter login button, paste this code into viewDidLoad():

let logInButton = TWTRLogInButton { (session, error) in
            if let unwrappedSession = session {
                self.twitterIDLabel.text = unwrappedSession.userName
                self.twitterUName = unwrappedSession.userName
                let alert = UIAlertController(title: "Logged In",
                    message: "User \(unwrappedSession.userName) has logged in",
                    preferredStyle: UIAlertControllerStyle.Alert
                )
                alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.Default, handler: nil))
                self.presentViewController(alert, animated: true, completion: nil)
            } else {
                NSLog("Login error: %@", error!.localizedDescription);
            }
            
        }
self.view.addSubview(logInButton)

HRViewController

Create a second ViewController. This ViewController is where the EON graph will be displayed, so you will need to set up a WKWebView. There is so much code for this, just visit the GitHub repo, but to fully understand the code, checkout this NSHipster documentation or this AppCoda tutorial. After you have you WKWebView set up here, don’t forget to go into your phone app’s plist, go down to App Transport Security Settings, and set Allow Arbitrary Loads to YES. You could also do this programmatically by opening up the PList in a text editor, finding NSAppTransportSecurity, and using the following code to set the boolean NSAllowsArbitraryLoads to true.
<key>NSAppTransportSecurity</key>
  <dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
  </dict>

To go more in depth with App Transport Security, check out this Apple documentation.

<h3><em>HTML Page for EON</em></h3>

Create a new empty HTML file. This demo calls it eon.html, and has the following code in the head:

 

<script type="text/javascript" src="http://pubnub.github.io/eon/v/eon/0.0.11/eon.js"></script>
<link type="text/css" rel="stylesheet" href="http://pubnub.github.io/eon/v/eon/0.0.11/eon.css" />

 

Next, you’ll create the graph that will subscribe to the PubNub channel, and display data it was subscribed to in realtime. It’ll attach to an HTML div.

 

var pbIOSWeb = PUBNUB({
subscribe_key: 'your-sub-key'
});
var chan = "your-channel";
eon.chart({
    pubnub: pbIOSWeb,
    channel: chan,
    history: false,
    flow: true,
    generate: {
      bindto: '#chart',
      data: {
        type: 'spline'
      },
      axis : {
        x : {
          type : 'timeseries',
          tick : {
            format :'%M'
          } //tick
        }, //x
        y : {
         max: 200,
         min: 10
        } //y
      } //axis
    }, //gen
    transform: function(dataVal) {
      return {
        eon: {
          dataVal: dataVal.heartRate
        }
      }
    }
  });

 

In this case, the data is formatted in the transform function, which takes in data dataVal. dataVal is the key, and the value being displayed on the chart in realtime is the heart rate, attached to dataVal by the dot. There is no need to publish the data as it’s already being published from the Watch, so only a subscription_key is needed. If no data is showing, it is (from my experience) either an error with App Transport Security or the transform function, so check the PList again or visit one of the links mentioned above.

Connecting the Phone and the Watch

The TwitterViewController gets a username, and sends it to the Watch. The Watch gets the user’s heart rate, publishes it to a PubNub channel, and sends it to the Phone, which shows an EON graph of the heart rate. The Raspberry Pi subscribes to that channel, and flashes different colors and at different speeds depending on the beats per minute of the heart rate.

This is perhaps the hardest part of this project. Much data gets transferred between devices, and sometimes, it does not send and it has nothing to do with you. The iPhone is not necessarily “reachable” during every WCSession, and, looking into this error from the console, it seems that quite a few other developers also have this problem.

InterfaceController

Mentally prepare yourself, because a lot happens in this file: publishing to PubNub, creating an instance of a Workout, asking permission to access HealthKit data (which includes data like heart rate), sending that data from the Watch to the Phone in a WatchConnectivity session, and more.

Just below your outlet labels, declare the following global variables:

 

var client: PubNub?
    
    let healthStore = HKHealthStore()
    
    let watchAppDel = WKExtension.sharedExtension().delegate! as! ExtensionDelegate
    var publishTimer = NSTimer()
    var wcSesh : WCSession!
    
    var currMoving:Bool = false //not working out
    
    var workoutSesh : HKWorkoutSession?
    let heartRateUnit = HKUnit(fromString: "count/min")
    var anchor = HKQueryAnchor(fromValue: Int(HKAnchoredObjectQueryNoAnchor))

 

Whenever you need to use HealthKit data, you’ll need an instance of HKHealthStore() to request permission, to start, stop, and manage queries, and more. Before we really dig deep into HealthKit, make sure your project’s HealthKit capabilities are enabled (this is done under Capabilities for your phone app, Watch app, and Watch app Extension targets.) It gives you access to HKQueryAnchor, which returns the last sample from a previous anchored query, or ones that were added or deleted since the last query.

Right after the global variables, initialize your PubNub Configuration:

 

override init() {
        let watchConfig = PNConfiguration(publishKey: "your-pub-key", subscribeKey: "your-sub-key")
        
        watchAppDel.client = PubNub.clientWithConfiguration(watchConfig)
        
        super.init()
        watchAppDel.client?.addListener(self)
    }
    

Create an override func willActivate(), which is also where you would check if a WatchConnectivity Session is supported (you used this code earlier, too, in the phone app). Based on what permissions your app has, different text will be displayed. That code would follow:

 

 

guard HKHealthStore.isHealthDataAvailable() == true else { //err checking/handling
            label.setText("unavailable🙀")
            return
        }
        
        guard let quantityType = HKQuantityType.quantityTypeForIdentifier(HKQuantityTypeIdentifierHeartRate) else {
            displayUnallowed() //only display if Heart Rate
            return
        }
        
        let dataTypes = Set(arrayLiteral: quantityType)
        healthStore.requestAuthorizationToShareTypes(nil, readTypes: dataTypes) { (success, error) -> Void in
            guard success == true else {
                //if success == false {
                self.displayUnallowed()
                return
            }
        }

 

It checks that we have permission to get heart rate data, requests permission, and creates a WatchConnectivity Session that you will use later to send an array of heart rate data to the phone. This is extra to decide when to tweet -- if you want a less specific tweet, no WatchConnectivity Session is necessary because the EON chart receives its data from PubNub.

eon-data-visualization-ios-heart-rate-monitor-iphone-screen

 

It checks that we have permission to get heart rate data, requests permission, and creates a WatchConnectivity Session that you will use later to send an array of heart rate data to the phone. This is extra to decide when to tweet -- if you want a less specific tweet, no WatchConnectivity Session is necessary because the EON chart receives its data from PubNub.

 

How do you get the heart rate? By creating a workout session and then a streaming query!

 

func beginWorkout() {
        self.workoutSesh = HKWorkoutSession(activityType: HKWorkoutActivityType.CrossTraining, locationType: HKWorkoutSessionLocationType.Indoor)
        self.workoutSesh?.delegate = self
        healthStore.startWorkoutSession(self.workoutSesh!)
    }
    
    func makeHRStreamingQuery(workoutStartDate: NSDate) -> HKQuery? {
        
        guard let quantityType = HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierHeartRate) else { return nil }
        
        let heartRateQuery = HKAnchoredObjectQuery(type: quantityType, predicate: nil, anchor: anchor, limit: Int(HKObjectQueryNoLimit)) { (query, sampleObjects, deletedObjects, newAnchor, error) -> Void in
            guard let newAnchor = newAnchor else {return}
            self.anchor = newAnchor
            self.updateHeartRate(sampleObjects)
        }
        
        heartRateQuery.updateHandler = {(query, samples, deleteObjects, newAnchor, error) -> Void in
            self.anchor = newAnchor!
            self.updateHeartRate(samples)
         }
        return heartRateQuery
    }

 

It is with quantity type that you could get access to other health data like steps, but in this tutorial, we just want HKQuantityTypeIdentifierHeartRate. We need a WKWorkoutSession to request the heart rate data because that is all developers have access to at the moment. We don’t access the actual heart rate sensors, so the heart rate data is not received in real-time, even with PubNub.

The heartRateQuery is the HealthKit heart rate data received, being constantly streamed until, in the case of this tutorial, the start/stop button is pressed.

The code to publish this data should look something like this:

 

watchAppDel.client?.publish(hrValToPublish, toChannel: "Olaf", withCompletion: { (status) -> Void in
           if !status.error {
               print("\(self.hrVal) has been published")
               
           } //if
               
           else {
               print(status.debugDescription)
           } //else
       })

 

apple-watch-heart-rate-monitor-watch-face

This is called from another function, the publishTimerFunc() function, because you do not need to publish heart rate data constantly; does it really matter if the value goes up by one or stays the same for a few seconds? That function contains just one line:

publishTimer = NSTimer.scheduledTimerWithTimeInterval(10.0, target: self, selector: #selector(InterfaceController.publishHeartRate), userInfo: nil, repeats: true)
 
The first parameter of scheduledTimerWithTimeInterval takes the number of seconds between each execution of the function you want to run in intervals. That function is called in selector.

To continuously update the heart rate label, you should use Grand Central Dispatch, which is Swift’s version of background threading. This is what happens in the updateHeartRate() function.

 

 

func updateHeartRate(samples: [HKSample]?) {
        guard let heartRateSamples = samples as? [HKQuantitySample] else {return} 
        
        dispatch_async(dispatch_get_main_queue()) {
            guard let sample = heartRateSamples.first else{return}
            self.hrVal = sample.quantity.doubleValueForUnit(self.heartRateUnit)
            let lblTxt = String(self.hrVal)
            self.label.setText(lblTxt)
            self.maxHRLabel.setText("max: " + String(self.maxArr))
            self.deprecatedHRVal = Double(round(1000*self.hrVal)/1000)
            self.arrayOfHR.append(self.deprecatedHRVal)
            self.maxArr = self.arrayOfHR.maxElement()!
            print("maxArr: " + String(self.maxArr))
            repeat {
                self.publishTimerFunc()
            } while(self.currMoving == false)
        } //dispatch_async
    } //func

 

Here, the heart rate label on the Watch is updated, and an array is created of heart rate values. The maximum is found, and also updated on the Watchface.

Monitoring the Heart Rate with Raspberry Pi

The final part of this project is the hardware. To get started, check out this tutorial. You need 4 M-F jumper wires, a Common-cathode RGB-LED, 3 resistors, a breadboard, and a Raspberry Pi. First, hook up the Raspberry Pi to the breadboard with cables as shown below.

raspberry-pi-diagram  

Based on heart rate, the LED will flash on and off at different speeds.

Conclusion

There are many components to this app, and hopefully you learned something about developing for the Watch, using EON on platforms other than web, building mobile apps with Fabric, and got some inspiration for future projects.

How can this be taken further?

-Do more with Fabric. I was surprised at how easy it was to integrate Twitter API features into this app, and there is so much to do with other features like Crashlytics, Answers, and more.

-Work more with hardware and displaying or doing something based on heart rate. I looked at music APIs, and getting BPM in songs, which could also be fun to work with.

About

License:MIT License


Languages

Language:Objective-C 73.1%Language:Swift 21.8%Language:HTML 2.7%Language:Python 1.8%Language:Ruby 0.4%Language:C 0.2%