15
8

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-10-28
  • Garmin connectでは心拍数の計測をもとにストレスを数値化してくれる

garmin-stress-sample.png

  • アプリ内では一覧でみたいときに折れ線グラフしかない + 最大4週間分しか見れない
  • 別の可視化方法として草化してみたい

作ったもの

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

GarminStress2Pixela.png

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

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

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

pixela-result.png

環境

  • 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で試した感じ,行けそう

rekognition-result.png

開発詳細

Serverless framework + Goで開始

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

新規関数を作成

  • 関数を新規作成
    • 自動生成された関数は不要なので削除
    • garmin-stress2pixelaフォルダを作成し,main.goを作成
    • Makefilebuild:に以下を追記
Makefile
    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のユーザ/トークン/グラフ情報を与える
serverless.yml
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へのアクセス権限をLambda関数にも付与すること (上記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を利用
garmin-stress2pixela/main.go
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)
}
15
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
8