Recording Exercises with the Watch

After digging through Apple’s docs and googling around, I came across a sample app that Apple created which uses Core Motion to track forehand and backhand tennis swings.

https://developer.apple.com/library/content/samplecode/SwingWatch/Introduction/Intro.html.

We’ll use this project as a starting point, but before we start editing it, let’s first decide how we’re going to process the motion data from the watch. One way would be to send the data from the watch to the phone’s app, hit an API endpoint, and store it in a database for us to fetch. That seems like overkill.

Instead, we can log these data points and extract them later on. A nice bonus would be to show these measurements on the app for debugging purposes. In order to do this, let’s edit the InterfaceController and set up a couple of labels to display the device motion on the home screen. If you’re new to this, take a look at getting started with Swift.

@IBOutlet weak var gravityLabel: WKInterfaceLabel!

@IBOutlet weak var userAccelLabel: WKInterfaceLabel!

@IBOutlet weak var rotationLabel: WKInterfaceLabel!

@IBOutlet weak var attitudeLabel: WKInterfaceLabel!

Next, let’s create a function to update the text.

func updateLabels() {

// The active check is set when we start and stop recording.

if active {

gravityLabel.setText(gravityStr)

userAccelLabel.setText(userAccelStr)

rotationLabel.setText(rotationRateStr)

attitudeLabel.setText(attitudeStr)

}

}

Now that we have a way to display this data, we can figure out how to connect these UI components to the sensor updates. For that we’ll need to use Core Motion’s Manager. The update interval tells how often we should record a measurement.

let motionManager = CMMotionManager()

motionManager.deviceMotionUpdateInterval = 1.0 / 50

Since we’re actually updating a UI element, 50hz (10 measurements / second) is good enough. If we need more granularity later on we can increase the frequency.

When we start recording, we’ll start reading the data from the sensors, log it, and dispatch it to update the UI.

motionManager.startDeviceMotionUpdates(to: queue) { (deviceMotion: CMDeviceMotion?, error: Error?) in

if error != nil {

print("Encountered error: \(error!)")

} if deviceMotion != nil {

self.processDeviceMotion(deviceMotion!)

}

}

}

Again we’ll extract the raw values out of the logs after we finish recording. Let’s walk through this (comments inline)

func processDeviceMotion(_ deviceMotion: CMDeviceMotion) { // 1. These strings are to show on the UI. Trying to fit

// x,y,z values for the sensors is difficult so we’re

// just going with one decimal point precision.

gravityStr = String(format: "X: %.1f Y: %.1f Z: %.1f" ,

deviceMotion.gravity.x,

deviceMotion.gravity.y,

deviceMotion.gravity.z)

userAccelStr = String(format: "X: %.1f Y: %.1f Z: %.1f" ,

deviceMotion.userAcceleration.x,

deviceMotion.userAcceleration.y,

deviceMotion.userAcceleration.z)

rotationRateStr = String(format: "X: %.1f Y: %.1f Z: %.1f" ,

deviceMotion.rotationRate.x,

deviceMotion.rotationRate.y,

deviceMotion.rotationRate.z)

attitudeStr = String(format: "r: %.1f p: %.1f y: %.1f" ,

deviceMotion.attitude.roll,

deviceMotion.attitude.pitch,

deviceMotion.attitude.yaw) // 2. Since this is timeseries data, we want to include the

// time we log the measurements (in ms since it's

// recording every .02s)

let timestamp = Date().millisecondsSince1970 // 3. Log this data so we can extract it later os_log("Motion: %@, %@, %@, %@, %@, %@, %@, %@, %@, %@, %@, %@",

String(timestamp),

String(deviceMotion.gravity.x),

String(deviceMotion.gravity.y),

String(deviceMotion.gravity.z),

String(deviceMotion.userAcceleration.x),

String(deviceMotion.userAcceleration.y),

String(deviceMotion.userAcceleration.z),

String(deviceMotion.rotationRate.x),

String(deviceMotion.rotationRate.y),

String(deviceMotion.rotationRate.z),

String(deviceMotion.attitude.roll),

String(deviceMotion.attitude.pitch),

String(deviceMotion.attitude.yaw)) // 4. update values in the UI

updateMetricsDelegate();

}

Finally updateMetricsDelegate just passes the strings to display on the labels we set up earlier.

func updateMetricsDelegate() {

delegate?.didUpdateMotion(self,gravityStr:gravityStr, rotationRateStr: rotationRateStr, userAccelStr: userAccelStr, attitudeStr: attitudeStr)

}

Here’s the finished code on GitHub. When we run it, here’s the result:

Taking the SwingWatch sample as a starting point, we’ve created a simple, “hello world” program to play around with Core Motion. We devised a simple way to capture the device motion so that we can extract it later on for analysis.