Garmin connectのストレス測定結果をPixela + Serverlessで草化

公開日:2018-10-28
最終更新:2018-11-17
※この記事は外部サイト(https://qiita.com/jagijagijag1/items/b0dda...)からのクロス投稿です
  • Garmin connectでは心拍数の計測をもとにストレスを数値化してくれる
  • アプリ内では,一覧でみたいときに折れ線グラフしかない + 最大4週間分しか見れない
  • 別の可視化方法として草化してみたい

作ったもの

  • Garmin connectの画面キャプチャをS3にアップロードすると,日付とストレス値をPixelaに記録するシステム

2018/11/17 追記: 下記を用いればiOSでスクリーンショットを取るだけでPixelaに記録できます. (iOS→Dropbox by IFTTT + Dropbox→S3 by Zapier)

結果:いい感じに草化できた気がする

  • 直近のストレスが高い,日曜は比較的ストレスが少ない
  • なるべく色がつかない(薄くなる)ようにしたいという逆モチベ

環境

  • MacOS Mojave
  • Go 1.11.1
  • Serverless Framework 1.32.0
  • iPhone 7(iOS 12.01) + Garmin Connect 4.12.0.14

Pixelaへのデータ投入方法の検討

  • iOS ヘルスケア
    • Garmin connectのアプリから連携されない
  • Garminの公式API (2種類)
    • Garmin Health API
      • 全データ取得可能かつ無料だが,企業向けのため利用不可
    • Garmin Connect API
      • 個人利用可だが,フィットネスデータのみが対象かつ有料($5,000)
  • アプリ画面から抽出
    • アプリ画面を都度キャプチャする必要あり
    • APIがなくても,直に情報を抽出可能
      • Amazon Rekognitionで試した感じ,行けそう

開発詳細

Serverless framework + Goで開始

  • $GOHOME/src配下で作業
$ serverless create -t aws-go-dep -p <project-name>
  • 東京リージョンにデプロイしたいのでserverless.ymlregionを追記
provider:
  name: aws
  runtime: go1.x
  region: ap-northeast-1
  • 以下でひとまずデプロイテスト可能
$ cd <project-name>
$ make
$ sls deploy

新規関数を作成

  • 関数を新規作成
    • 自動生成された関数は不要なので削除
    • garmin-stress2pixelaフォルダを作成し,main.goを作成
    • Makefilebuild:に以下を追記
    env GOOS=linux go build -ldflags="-s -w" -o bin/garmin-stress2pixela garmin-stress2pixela/main.go

serverless.ymlの修正

  • serverless.ymlの主な修正・追記点は以下
    • IAM RoleにRekognitionのDetectText実行許可と,画像を投入するS3バケットへのアクセス許可を追記
    • 新規作成した関数定義の追記 (+自動生成された関数定義の削除)
      • events下のs3: <bucket-name>は存在しないバケット名とすること (sls deployで新規作成されるため)
      • Lambda関数の環境変数(environment)にPixelaのユーザ/トークン/グラフ情報を与える
service: GarminStress2Pixela

frameworkVersion: ">=1.28.0 <2.0.0"

provider:
  name: aws
  runtime: go1.x
  region: ap-northeast-1
  # you can add statements to the Lambda function's IAM Role here
  iamRoleStatements:
    - Effect: "Allow"
      Action:
        - "rekognition:DetectText"
      Resource: "*"
    - Effect: "Allow"
      Action:
        - "s3:GetObject"
      Resource: [
        "arn:aws:s3:::pixela-datasource-stress-img-bucket",
        "arn:aws:s3:::pixela-datasource-stress-img-bucket/*"
      ]

package:
 exclude:
   - ./**
 include:
   - ./bin/**

functions:
  garmin-stress2pixela:
    handler: bin/garmin-stress2pixela
    events:
      - s3: <bucket-name>
    # you need to fill the followings with your own
    environment:
      PIXELA_USER: <user-id>
      PIXELA_TOKEN: <your-token>
      PIXELA_GRAPH: <your-graph-id-1>
    timeout: 10

関数本体を作成

  • 作っている最中の気づき,ポイントは以下
    • GoでのAWSイベントは以下にサンプルがあり,これを参照しHandlerの引数,入力情報処理を実装
    • InvalidS3ObjectException に当たった
      • InvalidS3ObjectException: Unable to get object metadata from S3. Check object key, region and/or access permissions.
      • S3 Objectへのアクセス権限をLambad関数にも付与すること (上記yamlにて済)
      • S3バケットのリージョンと,Rekognitionのリージョンを同じにすること
        • Rekognitionは同一リージョンのバケット内オブジェクトにしかアクセスできない模様
    • [作り込み・汎用性低] 日付・ストレス値は,事前に画像内での想定位置を与え,Rekognition.DetectTextの結果のうち,想定位置の最も近傍のテキストを選択
      • 想定位置(assumedDatePoint, assumedQuantityPoint)はiPhone 7(iOS 12.01),Garmin Connect 4.12.0.14にて実験的に抽出
    • データ投入先のPixelaの情報は環境変数(PIXELA_USER, PIXELA_TOKEN, PIXELA_GRAPH)から取得
    • GoでのPixela操作にはgainings/pixela-go-clientを利用
package main

import (
    "context"
    "fmt"
    "math"
    "os"
    "regexp"
    "strings"

    "github.com/aws/aws-lambda-go/events"
    "github.com/aws/aws-lambda-go/lambda"
    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/rekognition"
    pixela "github.com/gainings/pixela-go-client"
)

// Point is left & top positions of bounding box in the Rekognition result
type Point struct {
    Left float64
    Top  float64
}

// !! fixed number from experiment (maybe require to change your env) !!
var assumedDatePoint = Point{Left: 0.393, Top: 0.111}
var assumedQuantityPoint = Point{Left: 0.268, Top: 0.282}

// Handler is our lambda handler invoked by the `lambda.Start` function call
func Handler(ctx context.Context, s3Event events.S3Event) error {
    // for each s3 object
    for _, record := range s3Event.Records {
        // extract s3 object info
        bucket, key := getS3ObjectFromRecord(record)
        fmt.Printf("[%s] Bucket = %s, Key = %s \n", record.EventSource, bucket, key)

        // execute text detection of Rekognition
        res, rekerr := exeRekognitionDetectText(bucket, key)
        if rekerr != nil {
            fmt.Println("Error")
            fmt.Println(rekerr.Error())
        }

        // extract date & quantity from the above result
        date, quantity := getValueFromRekognitionResult(res.TextDetections)
        fmt.Printf("data: %s, quantity: %s\n", date, quantity)

        // record pixel
        perr := recordPixel(date, quantity)
        fmt.Println(perr)
    }

    return nil
}

func getS3ObjectFromRecord(record events.S3EventRecord) (string, string) {
    s := record.S3
    bucket := s.Bucket.Name
    rep := regexp.MustCompile(`[+]`)
    key := rep.ReplaceAllString(s.Object.Key, " ")

    return bucket, key
}

func exeRekognitionDetectText(bucket, key string) (*rekognition.DetectTextOutput, error) {
    // create Rekognition client
    sess := session.Must(session.NewSession())
    rc := rekognition.New(sess, aws.NewConfig().WithRegion("ap-northeast-1"))

    // set params
    params := &rekognition.DetectTextInput{
        Image: &rekognition.Image{
            S3Object: &rekognition.S3Object{
                Bucket: aws.String(bucket),
                Name:   aws.String(key),
            },
        },
    }
    fmt.Printf("params: %s", params)

    // execute DetectText
    return rc.DetectText(params)
}

func getValueFromRekognitionResult(results []*rekognition.TextDetection) (string, string) {
    dateHypot, quantityHypot := math.MaxFloat64, math.MaxFloat64
    date, quantity := "", ""

    // for each detected text
    for _, td := range results {
        left, top := *td.Geometry.BoundingBox.Left, *td.Geometry.BoundingBox.Top

        // calc hypot with assumed date pos & update value
        tmpDHypot := math.Hypot(math.Abs(left-assumedDatePoint.Left), math.Abs(top-assumedDatePoint.Top))
        if tmpDHypot < dateHypot {
            // if td is most-likely-result (nearest to the assumed point), keep the result (with removing "/")
            dateHypot, date = tmpDHypot, strings.Replace(*td.DetectedText, "/", "", -1)
        }

        // calc hypot with assumed quantity pos & update value
        tmpQHypot := math.Hypot(math.Abs(left-assumedQuantityPoint.Left), math.Abs(top-assumedQuantityPoint.Top))
        if tmpQHypot < quantityHypot {
            // if td is most-likely-result (nearest to the assumed point), keep the result
            quantityHypot, quantity = tmpQHypot, *td.DetectedText
        }
    }

    return date, quantity
}

func recordPixel(date, quantity string) error {
    user := os.Getenv("PIXELA_USER")
    token := os.Getenv("PIXELA_TOKEN")
    graph := os.Getenv("PIXELA_GRAPH")
    c := pixela.NewClient(user, token)

    // try to record
    err := c.RegisterPixel(graph, date, quantity)
    if err == nil {
        fmt.Println("recorded")
        return err
    }

    // if fail, try to update
    err = c.UpdatePixelQuantity(graph, date, quantity)
    if err == nil {
        fmt.Println("updated")
    }

    return err
}

func main() {
    lambda.Start(Handler)
}
記事が少しでもいいなと思ったらクラップを送ってみよう!
18
+1
@jagijagijag1の技術ブログ

よく一緒に読まれている記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう
目次をみる

技術ブログをはじめよう

Qrunch(クランチ)は、ITエンジニアリングに携わる全ての人のための技術ブログプラットフォームです。

技術ブログを開設する

Qrunchでアウトプットをはじめよう

Qrunch(クランチ)は、ITエンジニアリングに携わる全ての人のための技術ブログプラットフォームです。

Markdownで書ける

ログ機能でアウトプットを加速

デザインのカスタマイズが可能

技術ブログ開設

ここから先はアカウント(ブログ)開設が必要です

英数字4文字以上
.qrunch.io
英数字6文字以上
ログインする