Help us understand the problem. What is going on with this article?

Watchアプリのデバイスモーションから機械学習用のデータセットを作る。

More than 3 years have passed since last update.

前置き

自分自身はもともとiOSアプリを書いていたんですが、最近はデータ解析系の方に重きをおいていてあまりiOSは書いていませんでした。
で、今回一年ぶりくらいにiOSを書いたのですが、これは学内でのコンペで筋トレを細分化したときにある運動に対してより精緻なカロリー計算を行うことが出来ないか、という仮説からAppleWatchのデータを元に機械学習を用いて推論したろ、という感じの目的でアプリを作りました。

久々にSwiftを書いたのとWatchOSでの開発が初めてだったのととりあえず動けば良いという感じなので、そこんところご容赦ください。


アプリケーションの設計的にはこんな感じです。

image.png

だいぶ雑だけどこんな感じ。
サーバー上で作ったモデルは後々CoreMLに渡して端末内で完結させたい。
ちなみに今回DBを使わずにスプレッドシートを使ったのはDBを作るのがめんどくさかった後々CoreMLで端末内で完結させたいのでDB作り込んでもなあ、という意図があったりなかったり。

で、今回説明するのは、図の中でも以下の部分。

image.png

AppleWatchでデータを取ってiOSに渡してiOSからスプレッドシートに渡す部分です。
コードはこちら。
inoue0426/sendMotionApp

では以下からはじめます。


環境

Swift 4.0.2
Xcode 9.1
WatchOS 4.1
iOS 11.1.2

Project作成

一応説明しておくと、Xcode上部のFile→New→Project→iOS App with WatchKit Appをクリックで雛形は作成できます。

実装

import WatchKit
import Foundation
import WatchConnectivity
import CoreMotion

class InterfaceController: WKInterfaceController, WCSessionDelegate {

    let motionManager = CMMotionManager()
    let queue = OperationQueue()

    var applicationDict = [String: String]()
    var attitude = ""
    var gravity = ""
    var rotationRate = ""
    var userAcceleration = ""

    override func awake(withContext context: Any?) {
        super.awake(withContext: context)
        activateSession()

        if !motionManager.isDeviceMotionAvailable {
            print("Device Motion is not available.")
            return
        }

        motionManager.startDeviceMotionUpdates(to: queue) { (deviceMotion: CMDeviceMotion?, error: Error?) in
            if error != nil {
                print("Encountered error: \(error!)")
            }

            if deviceMotion != nil {
                self.attitude = "\(deviceMotion!.attitude)"
                self.gravity = "\(deviceMotion!.gravity)"
                self.rotationRate = "\(deviceMotion!.rotationRate)"
                self.userAcceleration = "\(deviceMotion!.userAcceleration)"
            }
            sleep(UInt32(0.5))
            self.sendMessage()
        }
    }

    func activateSession(){
        if WCSession.isSupported() {
            let session = WCSession.default
            session.delegate = self
            session.activate()
        }
    }

    func sendMessage(){
        if WCSession.default.isReachable {
            applicationDict = [
                "attitude": attitude,
                "gravity": gravity,
                "rotationRate": rotationRate,
                "userAcceleration": userAcceleration
            ]

            WCSession.default.sendMessage(applicationDict, replyHandler: {(reply) -> Void in
                print(reply)
            }){(error) -> Void in
                print(error)
            }
        }
    }

    @available(watchOS 2.2, *)
    func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
        print("activationDidComplete")
    }

    func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {

    }
}

WatchOSからの実装を先に説明すると、ほしいデータはCoreMotionというライブラリが持っているのでインポートしていて、AppleWatchがawake下段階でCMMotionManagerが使えるかどうか聞いて、使えたらstartDeviceMotionUpdatesという関数を呼んでいてその中でデータを取っています。(今回で言うとattitude、gravity、rotationRate、userAccelerationの4つ)

self.attitude = "\(deviceMotion!.attitude)"

この部分で前段階で空文字列を渡していたところにモーションのデータを渡しています。
ここで一旦Motion取得に関しては終わりです。

test.gif
プリントすると得られるデータはこんな感じ。
時系列データがサクサク出来ますね、楽しい。

次にWatchConnectivtyに関して。

func sendMessage(){
        if WCSession.default.isReachable {
            applicationDict = [
                "attitude": attitude,
                "gravity": gravity,
                "rotationRate": rotationRate,
                "userAcceleration": userAcceleration
            ]

            WCSession.default.sendMessage(applicationDict, replyHandler: {(reply) -> Void in
                print(reply)
            }){(error) -> Void in
                print(error)
            }
        }
    }

WatchConnectivityの肝となる部分はこのsendMessage部分で、先程用意した変数をDictionary形式にまとめて、WCSession.default.sendMessageの引数として渡しています。
これを先程awake時に呼んでいたdeviceMotionの下で呼び出します。

override func awake(withContext context: Any?) {
        super.awake(withContext: context)
        activateSession()

        if !motionManager.isDeviceMotionAvailable {
            print("Device Motion is not available.")
            return
        }

        motionManager.startDeviceMotionUpdates(to: queue) { (deviceMotion: CMDeviceMotion?, error: Error?) in
            if error != nil {
                print("Encountered error: \(error!)")
            }

            if deviceMotion != nil {
                self.attitude = "\(deviceMotion!.attitude)"
                self.gravity = "\(deviceMotion!.gravity)"
                self.rotationRate = "\(deviceMotion!.rotationRate)"
                self.userAcceleration = "\(deviceMotion!.userAcceleration)"
            }
            sleep(UInt32(0.5))
            self.sendMessage()
        }
    }

関数を呼び出す前にSleepを入れているのは入れないと早々にスプレッドシートが埋まりそうな気がする…ってくらい更新が行われていたためです。
delegateもちょろっと変えないといけないんですが、githubを見ていただけると幸いです。

で、次にiOSの方ですね。

import UIKit
import WatchConnectivity

class ViewController: UIViewController, WCSessionDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()
        activateSession()
    }

    func activateSession(){
        if WCSession.isSupported() {
            let session = WCSession.default
            session.delegate = self
            session.activate()
        }
    }

    func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
        replyHandler(["reply" : message["OK"] as! String ])
    }

    @available(iOS 9.3, *)
    func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
        print("activationDidComplete")
    }

    func sessionDidBecomeInactive(_ session: WCSession) {
        print("sessionDidBecomeInactive")
    }

    func sessionDidDeactivate(_ session: WCSession) {
        print("sessionDidDeactivate")
    }
}

そのまんまなんですが、まずactiveSessionの関数をviewDidLoad内で呼びます。
あとはdidReceiveMessageした時にreplyHandlerで何かしら値を返して通信を確認し、関数内で何かしらの処理をmessage変数に施す感じになります。

この段階でアノテーションをしてからスプレッドシートなりDBなりに飛ばすと前処理の時にやりやすいようなきがする🤔

スプレッドシートへPOSTして書き込んでサーバーで予測したものをGetして、みたいなところは来週再来週あたりで随時更新していきたいと思います!!!!

参考文献

watchOSプログラミングガイド iOSの技術の活用
watchOSプログラミングガイド Watchアプリケーションのアーキテクチャ
watchOSプログラミングガイド データの共有

WCSession
Core Motion

watchOS Watch Connectivity
RealmデータをwatchOSとiOSとの間でやりとりする(3)
[watchOS 3] Watch Connectivity のデータ受け取りをバックグラウンドで行う
AppleWatchとiPhoneの通信 watchkit swift 開発

Apple Watch に Core Motion を使って色々なデータを取得して表示させてみる
[watchOS 3] Apple Watch でデバイスモーションを取得する

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away