fitbit senseの心拍データをリアルタイムで取得する

<!-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -->
# アプリケーション例: 心拍数に応じて強さを調整する五目並べAI
[![IMAGE ALT TEXT HERE](http://img.youtube.com/vi/yTEW9g88QYk/0.jpg)](https://youtu.be/yTEW9g88QYk)
[モンテカルロ木探索による五目並べAI](https://qiita.com/toyohisa/items/e9f218909214c3a98ce2)にて紹介した五目並べAIの強さを、心拍数に応じて調整する。具体的には、心拍数が上がるとAIは弱くなり、逆に心拍数が下がるとAIは強くなる。

<!-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -->
# 目的

<dl>
  <dt>fitbitデバイスを用いて、リアルタイムに心拍データを取得する</dt>
    <dd>ただし後述する「現時点での制限」のように、実行時におけるいくつかの制限がある。</dd>
  <dt>主な使用用途</dt>
    <dd>心拍データを用いたゲームのプロトタイプ作成</dd>
    <dd>心理学的実験のための簡易心拍取得装置</dd>
</dl>


<!-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -->
# 環境

<dl>
  <dt>心拍の取得（身に付けるもの）</dt>
    <dd><a href="https://www.fitbit.com/jp/sense">fitbit sense</a> (他のfitbitでもたぶん可)</dd>
    <dd><a href="https://play.google.com/store/apps/details?id=com.fitbit.FitbitMobile&hl=ja&gl=US">スマートフォンのfitbit app</a> (ここで試したのはAndroid)</dd>
  <dt>開発、実行の制御（開発するもの。また現時点では実行時にも必要なもの）</dt>
    <dd>windows 10 home (開発だけでなく実行の制御も現時点ではしている)</dd>
    <dd>docker (websocket serverの構築のために使用)</dd>
  <dt>websocket server（心拍データを受け取り、配信するもの）</dt>
    <dd><a href="https://console.cloud.google.com/?hl=ja">Google Cloud Platform</a> (この中のContainer Registryでdocker imageを管理し、Cloud Runでそのimageを実行している)</dd>
</dl>

<!-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -->
# 構成
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/577900b0-47df-24ba-63bd-1c045666a433.png">

+ 図の黒い矢印は開発時の関係、黄色の矢印は実行時のデータ経路を示す。
+ fitbitのデバイスから直接、サーバ等に心拍データを送信することができないため、スマートフォンのfitbit appにデプロイしたcompanion（[fitbit companion api](https://dev.fitbit.com/build/reference/companion-api/)を用いて開発したfitbit appの中で動くもの）経由でサーバにデータを送信する。
+ サーバへの接続方法は、[http get/post](https://dev.fitbit.com/build/reference/companion-api/fetch/)と[websocket](https://dev.fitbit.com/build/reference/companion-api/websocket/)が用意されている。ここでは通信効率の良いと思われるwebsocketを使用する。
+ websocketのサーバには、googleの[cloud run](https://cloud.google.com/run?hl=ja)を使用する。cloud runは、[docker](https://www.docker.com/)のイメージをそのまま実行できる環境である。dockerとは、OSを含めて実行環境をひとまとめにパックできるようなもので、ローカルPCのOSに依存せずにサーバの開発などができる。今回は、debianディストリビューションのlinuxをOSとしたpythonの環境を[docker hubのpython images](https://hub.docker.com/_/python)から参照して、そこにpython上でASGI(asynchronous server gateway interface): 非同期サーバゲートウェイインターフェースを構築するための[uvicorn](https://www.uvicorn.org/)によってwebsocketサーバを構築する。

<!-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -->
# 現時点での制限

+ 約2分間の実行後にfitbitデバイスのアプリが終了する。現時点では、fitbitデバイスをタップしてスリープさせないようにする必要がある。fitbit device apiの[appTimeoutEnabled](https://dev.fitbit.com/build/reference/device-api/appbit/#interface-appbit-)をfalseにしてアプリのタイムアウトを無効化しても、なぜか約2分でアプリは落ちる。fitbitデバイスの画面設定で「常に画面をON」をつけると、少し実行できる時間は伸びて、一度試してみたところ、3分半ぐらいになった。
+ Websocketサーバとして使用するGoogle Cloud Runは、デフォルトで5分間で接続が切れる。Cloud Runのマニュアルには、非アクティブが5分続くとタイムアウトするとの説明があるが、[Cloud Runのwebsocketでの使用に関するマニュアル](https://cloud.google.com/run/docs/triggering/websockets?hl=ja)にも示されているように、Websocketを通信し続けていても、5分で接続は切れる。[Cloud Runの設定](https://cloud.google.com/run/docs/configuring/request-timeout?hl=ja)でタイムアウトを15分までなら伸ばすことができるとの説明があるが試してはいない。クライアントでwebsocketの接続断イベントを取得し、再接続を行うことによって疑似的な永続的接続を構築することは出来るが、再接続までの受信データをロストするなどの問題はある。
+ スマホのfitbit appを立ち上げてスリープさせない方が良い。スリープ状態になると、websocketの通信が途絶えることがある。



<!-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -->
# fitbitデバイスとスマートフォンで動作するfitbit app内のアプリの開発
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/13584abd-fd6e-8f48-98e4-685b77b166b6.png" />

# fitbit studioによるアプリ開発

<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/666ac507-6992-d2a6-205c-7fc164401942.png" />

[fitbit studio](https://studio.fitbit.com/)にアクセスし、fitbitアカウントでログインする。

<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/4747eea6-854d-1d64-9041-8f6e4d4aa8ad.png" />

Starterテンプレートを用いて新規プロジェクトを作成する。

<img width="600px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/89675027-966b-9065-0193-df65b1900ba0.png" />

左側のappはfitbitデバイスで動作するもの、companionはスマートフォンのfitbit appの中で動作するもの。resourcesはスタイル等の設定である。

<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/44cfea12-ad84-f1de-609a-7628d71d6948.png" />

左側で app / index.js を選択して、以下のコードを入力する。

```javascript:index.js
/*
  fitbitデバイスで動作する。
*/
import * as messaging from "messaging";
import { HeartRateSensor } from "heart-rate";
import { me } from "appbit";

// fitbit appとの通信を開始する。
messaging.peerSocket.addEventListener("open", (evt) => {
  start_heartRateSensor();
});
messaging.peerSocket.addEventListener("error", (err) => {
  console.error(`Connection error: ${err.code} - ${err.message}`);
});
// fitbit appと接続後に、心拍データの取得を行う。
function start_heartRateSensor(){
  const hrm = new HeartRateSensor({ frequency: 1, batch: 1 });
  hrm.addEventListener("reading", () => {
    sendMessage({
      heartRate: hrm.heartRate ? hrm.heartRate : 0
    });
  });
  hrm.start();
  //なぜか効かない。
  //me.appTimeoutEnabled = false;
}
// fitbit appに心拍データを送信する。
function sendMessage(data) {
  if (messaging.peerSocket.readyState === messaging.peerSocket.OPEN) {
    messaging.peerSocket.send(data);
  }
}
```

importされているmessagingは、スマートフォン fitbit appとの通信を行う、HeartRateSensorは心拍の取得、meはこのアプリのタイムアウトの設定を行うためのものである。ただし、meを用いたタイムアウトの設定はなぜか効果がないため、現在はコメントアウトして使用していない。

HeartRateSensorの生成時に frequencyで心拍取得の間隔を指定している。現在は1秒間隔で心拍情報を取得する。心拍情報とは、1分間に波打つ回数である。

<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/50301160-48a7-deba-e7ec-9b8f59abea37.png" />

次に、companion / index.jsを選択する。

```javascript:index.js
/*
  スマートフォンのfitbit app内で動作する。
*/
import * as messaging from "messaging";

// websocketの接続先
const wsUri = "wss://_______________________________________/xx";
const websocket = new WebSocket(wsUri);

// それぞれのリスナを設定する。
websocket.addEventListener("open", onOpen);
websocket.addEventListener("close", onClose);
websocket.addEventListener("message", onMessage);
websocket.addEventListener("error", onError);
function onOpen(evt) {
  console.log("CONNECTED");
  websocket.send("connected")
}
function onClose(evt) {
  console.log("DISCONNECTED");
}
function onMessage(evt) {
  console.log(`from websocket server: ${evt.data}`);
}
function onError(evt) {
  console.error(`ERROR: ${evt.data}`);
}

// デバイスからのデータを受信したときに、同データをwebsocketサーバに送信する。
messaging.peerSocket.addEventListener("message", (evt) => {
  if(websocket.readyState===WebSocket.OPEN){
    const data = JSON.stringify(evt.data);
    websocket.send(data);
  }
});
```

import の messagingは、fitbitデバイスのアプリと通信を行う。wsUriにwebsocketのサーバのアドレスとエンドポイントを設定する。デバイスからの心拍情報を取得したら、そのままwebsocketでサーバに送信するというプログラムになっている。

<img width="600px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/efb70f68-78dd-6149-7e1c-27d2cac0d369.png" />

最後に、package.jsonにて Heart RateとInternetのRequested Permissionsのチェックを付ける。


## アプリのデプロイと実行確認

### スマートフォンでの設定
<img width="200px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/b13e62a6-cd73-694d-d1d1-72b5fc8c9db5.png" />

スマートフォンのfitbitアプリを起動して、左上のアカウントをタップする。

<img width="200px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/d4e008a1-354e-f3d4-f45e-49a33db2aa92.png" />

心拍を取るデバイスをタップする。

<img width="200px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/ee6d8405-0954-1a6b-ce2d-9dad0c40166c.png" />

開発者メニューをタップする。

<img width="200px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/364cf3db-da0c-b9c4-f891-14d89d0b369a.png" />

開発者用ブリッジ接続をONにする。

# fitbitデバイスでの設定

<img width="130px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/549ae8de-c8f9-8f5a-1229-315bb431da7e.png" />

画面を横にスライドする。

<img width="130px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/22b53b16-cc46-4e92-7e1e-903009204f57.png" />

設定をタップする。

<img width="130px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/9472096d-ddc3-ed6c-15fd-8956d31165b0.png" />

開発者用ブリッジ接続をタップする。

<img width="130px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/628ab946-5077-25e7-edf6-083ea6ceb6ab.png" />

オンにする。

### fitbit studioでの設定

<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/45bd5eff-7806-dce3-20ba-ccd63c8e5b1e.png" />

fitbitデバイス、スマートフォンのfitbit appと接続するために、Select a phone Select a deviceメニューを開く。Phones Devicesが見えていなければ、Refreshを選択し、表示されるそれぞれのデバイスをクリックして接続する。

<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/f6adc25a-c595-9f8c-a0b8-18117e069c5a.png" />

接続すると、それぞれのデバイス名の横に Connectedと表示される。


### 実行確認

<img width="500px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/08bd11d7-4c4f-3640-09d7-a884ee6c5b8b.png" />

fitbit studioの Runボタンを押すと、コンソールに心拍情報が表示される。ただし、現時点での制限で述べたように約2分間で、アプリは終了する。その時には、もう一度 Runボタンを押す必要がある。
またfitbitデバイスには、fitbit studioのresourcesで設定したデフォルトのHello World!が表示されている。

一度、fitbit studioでアプリをデプロイした後には、fitbitデバイスからアプリを起動することができる。この際に、スマートフォンのfitbit appは、起動しておいた方が良い。

<img width="130px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/549ae8de-c8f9-8f5a-1229-315bb431da7e.png" />

fitbitデバイスで横にスクロールさせていくと、最後にデプロイしたアプリが表示される。

<img width="130px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/b0052621-f891-40e1-eabc-2640c091a807.png" />

このアプリをタップすると、起動する。この時に、自動的にスマートフォンのfitbit app内の開発したアプリも起動する。


<!-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -->
# dockerによるwebsocketサーバの構築

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/599e190c-dab0-e742-83fe-cc7095512cfd.png" />

## windows 10 homeへの dockerのインストール
<img width="600px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/539d1119-3b19-3c3e-9c93-242e6d8b0917.png" />

[Docker Desktop](https://www.docker.com/products/docker-desktop)からDownload for Windowsをクリックしてダウンロード、インストールを開始する。インストールオプションは、基本デフォルトで行う。


<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/d5934d3b-ddb2-9079-254a-7ee4d85bfb57.png" />

インストール終了後にWSL 2のインストールができていないとのメッセージが表示されるので、そのメッセージにあるURLをクリックする。

<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/71caad23-9e66-e667-fab6-aa6629773f2f.png" />

[表示されるサイト](https://docs.microsoft.com/ja-jp/windows/wsl/install-win10#step-4---download-the-linux-kernel-update-package)のx64マシン用 WSL2 Linuxカーネル更新プログラム パッケージをクリックし、インストールする。

<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/55e571af-6653-b1a4-30c9-5d1d5d9695ad.png" />

Dockerが立ち上がるとチュートリアルの開始となるが、とりあえずスキップして問題はない。

<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/2e292acb-634e-633f-32b7-f83fb4b1e5ad.png" />

インストールが完了する。左側のContainers / Appsは、それぞれのコンピューティング環境、Imagesはその環境を構築するための元（昔のインストールCDのようなもの）である。

## websocketサーバの構築

適当なフォルダを作成して、次に示す3つのファイルを作成する。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/a1a93874-58cd-ddd4-95f1-9e9fca02b65d.png" />

```text:Dockerfile
FROM python:3.8-alpine

WORKDIR /usr/src/app

COPY requirements.txt ./
RUN apk add --no-cache build-base \
 && pip install --no-cache-dir --trusted-host pypi.python.org -r requirements.txt \
 && apk del build-base

COPY . .

CMD [ "uvicorn", "simple_main:app", "--reload", "--host", "0.0.0.0", "--port", "8080" ]
```
Dockerによるコンピューティング環境構築用のDockerファイルであり、FROM python:3.8-alpineという指定によって[docker hubのpython](https://hub.docker.com/_/python)の上に構築している。このpythonは、DebianディストリビューションのlinuxをOSとし、pythonをインストールしてくれる。タグと呼ばれる3.8-alpineは、pythonのバージョンを3.8とし、alpineというタグ付けされたpythonの最小構成を作るものである。
最後のCMDで指定された uvicornの実行は、ポート番号を 8080 にする必要がある。これは、このwebsocketサーバを最終的にはgoogle cloud runで実行するために必要なことであり、cloud runはデフォルトで8080ポートを使用するために、これに合わせるものである。cloud runは、外向きのインターフェースは、https / wss の433であるが、そこに到達したデータは、内部の8080に転送されるという仕組みになっている。よって開発するサーバは、8080ポートで待っている必要がある。


```text:requirements.txt
uvicorn[standard]
fastapi
```
requirements.txtファイルはDockerファイルから参照されて、必要なpythonコンポーネントを記述しておく。

```python:simple_main.py
from fastapi import FastAPI
from starlette.websockets import WebSocket, WebSocketDisconnect

# fast app
app = FastAPI()

# client websockets list
clients = []

# notify to all clients
async def notify(msg:str):
  for websocket in clients:
    await websocket.send_text(msg)

# notification generator
async def notification_generator():
  while True:
    msg = yield
    await notify(msg)
notification = notification_generator()

# websocket endpoint
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
  await websocket.accept()
  clients.append(websocket)
  try:
    while True:
      data = await websocket.receive_text()
      await notification.asend(data)
  except WebSocketDisconnect:
    clients.remove(websocket)

# startup (preparing notification generator)
@app.on_event("startup")
async def startup():
  await notification.asend(None)
```
Websocketサーバのプログラムで、[fastapi](https://fastapi.tiangolo.com/ja/)と呼ばれるpythonのwebフレームワーク、[uvicorn](https://www.uvicorn.org/)というASGI(Asynchronous Server Gateway Interface) :非同期サーバーゲートウェイインターフェイスのインプリメンテーションを使用する。
Websocketの接続先であるエンドポイントは、 @app.websocket("/ws")で指定した /ws とし、そこに接続されるwebsocketをすべて保存し、あるwebsocketから送られてきたデータは、その送信元websocketも含めて、全ての接続先websocketへ配信するようになっている。

## docker build
Dockerfileのあるフォルダで、次のコマンドを実行してdocker imageを作成する。

```
docker build -t websocket_server .
```
-tオプションのwebsocket_serverは作成するdocker imageの名前である。

<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/2f85239a-25d7-c4f7-91e8-128b1b7af74b.png" />

ビルドが成功すると、Docket画面のimagesに、websocket_serverというイメージが作成されている。

## 実行確認

次のコマンドを実行すると、websocket_server イメージからコンテナが作成されてコンピューティング環境が実行される。

```
docker run -p 80:8080 websocket_server
```
オプションの -p は、dockerの外であるwindowsPCからdockerの中にあるアプリに接続するためにポートを転送する設定であり、この場合は、windows pcの80ポートを、dockerの8080ポートに転送するとなる。

```html:websocket_client_test.html

<!DOCTYPE html>
<!--------------------------
    websocket client html
    ・フォームからサーバにデータを送信できる。
    ・サーバから受信したデータを表示する。
-->
<html>
    <head>
        <title>My Heart Rate</title>
    </head>
    <body>
        <label for="test_form">テストデータの送信</label>
        <form action="" onsubmit="sendMessage(event)" id="test_form">
            <input type="text" id="messageText" autocomplete="off"/>
            <button>Send</button>
        </form>
        <ul id='messages'>
        </ul>
        <script>
            //const ws = new WebSocket("wss://_______________________________________/xx");
            const ws = new WebSocket("ws://localhost/ws");
            ws.onmessage = function(event) {
                const messages = document.getElementById('messages')
                const message = document.createElement('li')
                const content = document.createTextNode(event.data)
                message.appendChild(content)
                messages.appendChild(message)
            };
            function sendMessage(event) {
                const input = document.getElementById("messageText")
                ws.send(input.value)
                event.preventDefault()
            }
        </script>
    </body>
</html>
```
localhostのwsに接続するhtml, javascriptを作成し、ブラウザで表示する。


<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/87b4e00b-0828-ca66-b49c-f1138949ad84.png" />

サーバに送信したデータがループバックで戻ってきて、リスト表示される。同時に複数のブラウザを開けば、1つのブラウザから送信したデータは、全てのブラウザに戻ってくることが確認できる。

<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/bf48c9d5-aa64-b443-5456-cc8ca9c8a37d.png" />

この時に、dockerのコンソールには、このような感じで表示されている。

<!-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -->
# Google Cloud Runによるwebsocketサーバの公開

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/4d593f7e-721b-58b9-ba17-8ff4570e3dfb.png" />

## Google Cloud Platformでプロジェクトの作成

<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/e357f540-b542-04d1-1a97-da6895eff62d.png" />

[Google Cloud Platform](https://console.cloud.google.com/?hl=JA)のプロジェクト選択から新規プロジェクトを作成する。もちろん、既にあるプロジェクトに新たなサービスを追加する場合は、その既存プロジェクトを選択してもよい。

<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/65664556-25f5-23dc-05e7-411d5b1c54fa.png" />

プロジェクト名を入力して作成する。

おそらく課金の設定についてもする必要がある。

## Container Registryの設定

直接、Cloud Runを操作してdocker imageからアプリを起動することも可能であるが、ここでは、docker imageのcontainer registryへのアップロード、そしてcontainer registryにあるimageからcloud runのアプリを生成、と段階を踏んで行っていくこととする。

<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/25d473c0-38cd-2359-5ae7-6c733bd81f8d.png" />

左側からContainer Registryを選択する。

<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/a116f29d-6ae1-af2a-1c43-e298ce594fe7.png" />

Container registryを有効化する。

```
docker tag [docker image id] gcr.io/[google cloud project id]/[docker image name]
```
dockerコマンドによって、先に作成したdocker imageにタグ付けを行う。image id, project id, image nameの確認方法は、次に示す。またgcr.ioは、とりあえずcloud runのデプロイ先をus-central1 (アイオア)にすることを前提としている。

<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/f32691ac-f574-7cb5-561b-815020ac62d6.png" />

google cloud project idの確認は、google cloud platformのプロジェクトの選択等で行うことができる。

<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/913e2d8b-0f3f-eee2-49ef-00520207b7d1.png" />

docketのイメージ名、image idの確認は、docker画面の左側のimagesタブから行う。


<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/b7ca8d45-ff55-0945-9940-9f8749390742.png" />

docker imageへのタグ付けが成功すると、docker画面ではこのように表示されている。

```
docker push gcr.io/[google cloud project id]/websocket_server
```
タグ付けされたimageをdocker pushでgoogleにアップしようとすると、以下のように権限が無いとのエラーが表示される。


<img width="600px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/f05f3ed9-1859-e4c3-21e2-2c29c0807cc9.png" />

次に、権限付与のために[Google Cloudの認証方法のサイト](http://cloud.google.com/container-registry/docs/advanced-authentication)の説明に従ってまず、Google Cloud SDKのインストールを行う。


<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/035a1915-bafd-fec7-c992-f72f3ebcbfd4.png" />

[Cloud SDKのドキュメント](https://cloud.google.com/sdk/docs)に接続し、Google Cloud SDKのインストールを選択する。


<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/b64d07d9-b1a5-6a5b-c3c3-135efcc6ff43.png" />

windowsのタブを選択し、Cloud SDKをダウンロードしてインストールする。インストールは基本的にデフォルト設定のままで行う。

<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/bde4856d-f0c0-dcd7-3688-d75cf41eb3b8.png" />

インストール後に、Start Google Cloud SDK ShellとRun `gcloud init` to configure the Cloud SDKのチェックを付けてFinishボタンを押す。

<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/99ce6e9b-ef9d-9e4e-08a6-b2739011aea5.png" />

コンソールでログインを要求されるので、 y を入力して enterを押す。

<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/ec616240-c8d7-8c8f-4638-ee06cc17581a.png" />

ブラウザが表示されて許可を求めるためにGoogleアカウントの選択が表示される。使用するアカウントを選択する。

<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/678a1917-8315-0630-4856-88df1f697ac7.png" />

次に表示される画面で許可を押す。

<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/f72726f3-15b1-7a55-c43b-963e6e0f9474.png" />

コンソールでは、プロジェクトを選択するところで止まっているので、先に作成したプロジェクトを選択する。

次に、container registryにアップロードをするdockerにgoogleの権限を与えるために、[gcloud 認証情報ヘルパー](https://cloud.google.com/container-registry/docs/advanced-authentication?hl=ja#gcloud-helper)を参考に設定を進めていく。

```
gcloud auth login
```
gcloudにログインするために、上記のコマンドを実行する。表示されるブラウザでは、先ほどと同様にgoogleアカウントを選択して、許可を押す。

```
gcloud auth configure-docker
```
gcloudコマンドで、dockerを構成する。

<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/293e411c-d40c-18b2-f3c8-dd734a50daec.png" />

このような感じに実行できていれば、たぶん、大丈夫。

```
docker push gcr.io/[google cloud project id]/[docker image name]
```
あらためてdockerコマンドでdocker imageをcontent registryにアップロードする。

<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/c8574377-f15e-9427-24d1-06a96b556a81.png" />

docker pushコマンドの結果として、このような感じで表示される。

<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/485071fc-3950-3e49-f123-9bfe2f2d35ee.png" />

Google Cloud Platformのcontainer regstryで確認すると、websocket_serverという名前のimageが追加されている。


## Cloud Runの設定

```
gcloud run deploy websocket-server --image gcr.io/[google cloud project id]/websocket_server
```
gcloudコマンドによって、container regstryにアップしたdocker imageからサービスを構成する。deploy websocket-serverのwebsocket-serverはcloud run上のサービス名となる。このサービス名では _ (アンダースコア）が使用できないため、- (ハイフン) としている。

<img width="600px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/31e63c7d-5e6b-fe91-55f8-f0142f312c04.png" />

target platformは[1] Cloud Run (fully managed)を、projectを有効化するかには y を回答する。

<img width="600px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/d6438f9d-37c0-24cc-333c-10254fac3fed.png" />

リージョンにはus-central1を、unauthenticated invocationsを聞かれた場合には y を選択する。

<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/d1ae12b3-f401-7950-a6e2-bd3538dbb683.png" />

google cloud platformのcloud runで確認すると、websocket-serverという新しいサービスが登録されている。この時点で既に動作している。URLがサービスのURLであるため、動作確認のために控えておく。

## 実行確認

### websocketの送受信

<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/27241907-1da0-03ce-192c-371671a61285.png" />

先に示したwebsocketクライアント用のhtmlファイルの接続先urlを、cloud runのサービスurlに変更し、プロトコルもwsからwssに変えてブラウザで表示する。

<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/51767561-9f10-dbd3-9ffe-e137cd36a7a7.png" />

送信したデータが、全てのクライアントに返ってくることを確認する。また、クラウド上のwebsocketサーバなので、例えば、スマートフォンから参照しても同様の結果を得ることができる。


### 心拍データの送受信


<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/0c13b0fe-9f21-a5ba-cb2e-0a8044eff87e.png" />

fitbit studioのcompanion / index.js の wsUriを cloud runのサービスURLに変更する。

<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/146cb1b7-a459-e882-4ca3-d940fb56b6ad.png" />

fitbit studioでfitbitデバイス、fitbit appのcompanionを起動すると、websocketサーバ経由でリアルタイムに心拍データを取得することができる。

<!-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -->
# Cloud Runのタイムアウト対策

次のは、websocketが5分でタイムアウトしたときにクライアント側から接続し直すjavascriptである。ついでに、受信時にリストを下に増やすのではなく、上に追加する形で最新が常に一番上になるように変更している。

```html:websocket_client.html
<!DOCTYPE html>
<!--------------------------
    websocket client html
    ・フォームからサーバにデータを送信できる。
    ・サーバから受信したデータを表示する。
-->
<html>
<head>
    <title>My Heart Rate</title>
</head>

<body>
    <label for="test_form">テストデータの送信</label>
    <form action="" onsubmit="sendTextInput(event)" id="test_form">
        <input type="text" id="messageText" autocomplete="off" />
        <button>Send</button>
    </form>
    <ul id='messages'>
    </ul>
    <script>
        const url = "wss://_______________________________________/xx";
        //const url = "ws://localhost/ws";

        function sendTextInput(event) {
            const input = document.getElementById("messageText");
            sock.send(input.value);
            event.preventDefault();
        }
        const sock = new function () {
            const self = this;
            let ws = undefined;
            self.init = () => {
                if (ws && ws.readyState === WebSocket.OPEN) {
                    ws.close();
                }
                ws = new WebSocket(url);
                ws.onmessage = (event) => {
                    const messages = document.getElementById('messages');
                    const message = document.createElement('li');
                    const content = document.createTextNode(event.data + " [" + (new Date()) + "]");
                    message.appendChild(content);
                    messages.prepend(message);
                };
                ws.onclose = (event) => {
                    console.log("closed and re-init");
                    self.init();
                };
            }
            self.send = (msg) => {
                if (ws.readyState === WebSocket.OPEN) {
                    ws.send(msg);
                } else {
                    console.log("cannot send " + msg + " [" + ws.readyState + "]");
                }
            };
            self.init();
        }
    </script>
</body>
</html>
```

sock変数では、wsという名前のwebsocketのインスタンスを保持し、それをサーバに接続するinitという名称のメソッドを有する。そのinitメソッドは、websocketが接続断されたときのイベント onclose の中で再度呼び出される。

<!-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -->
# 参考にさせて頂いたサイト

[Windows 10 HomeへのDocker Desktop (ver 3.0.0) インストールが何事もなく簡単にできるようになっていた (2020.12時点)](https://qiita.com/zaki-lknr/items/db99909ba1eb27803456)

[FastAPIでWebSockets](https://qiita.com/satto_sann/items/a62cb030e5c631eb74ad)