Fitbit Versa 3 / Sense の文字盤を作る

# はじめに
スマートウォッチ、アクティビティトラッカーでは聞きなじみの多い「Fitbit」。Fitbit Versa や Sense といったスマートウォッチでは、[ギャラリーにある膨大な種類の文字盤から](https://gallery.fitbit.com/ja-jp/clocks)好きなものを選んで、自分好みのスマートウォッチへと変えることができます。また、自分でも JavaScript や TypeScript を使って作ることができます。

# 自分で作るための 2 つの手法
## [Fitbit Studio](https://studio.fitbit.com/) を使う
一番オーソドックスなやり方かと思います。ウェブ上でコーディングができ、データは Fitbit のサーバー上に保存されます。特別準備がなくても、WiFi 接続の準備ができている手持ちの Fitbit もしくは PC にインストールしてある Fitbit OS Simulator ([Windows 版](https://simulator-updates.fitbit.com/download/latest/win), [Mac 版](https://simulator-updates.fitbit.com/download/latest/mac)) があればサイドロードもできたりします。
一方で、少し試した感じでは入力補完が利かなさそうなので、そのあたり頼りにしてるような僕みたいな人には向かないかもしれないです。
開発環境だけの用意なので、ギャラリーへ公開する際は生成物をダウンロードして別ページでおこなう必要があります。ワンストップでいけるわけじゃないんですね…。

## ローカル環境コマンドラインを叩いて作る
PC ローカルでお好きなエディターを使って開発することもできます。
Node.js をインストールしておき、コマンドを入力することで環境を作ることができます。[^1]
また、Fitbit Studio でできるデバイスへのサイドロードなどはコマンドでもできます。

今回はこちらのやり方で説明します。

# 環境を準備する
必要なのは以下の通りです。

* [Fitbit Studio](https://studio.fitbit.com/) にアクセスし開発者として登録しておく
  * 利用規約への同意が最初に必要です。ログインは Fitbit アカウントで OK です。コマンドライン上でビルドする際に、このアカウント情報を要求されます。
* Fitbit Versa 3 または Sense
  * 今回は Fitbit OS 5.x (SDK 5.0 以上) の 2 デバイスに対してのおはなしですが、 Versa 2 などの OS 4.x (SDK 4.x) も作ることが可能です。
  * デバイスにサイドロードする際は、デバイスがインターネットに接続できるよう準備をしておいてください。また、デバイスの「設定」から「開発者用ブリッジ接続」を選んで「オンにする」のトグルをオンにし、開発者モードを有効にしておきます。
* Fitbit OS Simulator
  * [Windows 版](https://simulator-updates.fitbit.com/download/latest/win)と [Mac 版](https://simulator-updates.fitbit.com/download/latest/mac)があります。上記デバイスを持っていない人でもこれで開発が可能ですし、自分のデバイスにいきなり突っ込むのもなー…っていう方にも。
* [Node.js](https://nodejs.org/ja/)
  * Windows 環境では入ってるケースあまりないと思うので入れます。バージョンはよっぽど古くなければ大丈夫なはず。
* (参考) Fitbit アプリとスマートフォン
  * [Companion と呼ばれる機能](https://dev.fitbit.com/build/guides/companion/)を作成するようにした場合、デバイスにサイドロードする際は、デバイスと紐付けをしたスマートフォンもあわせて必要です。
  * 文字盤を作る際に必要となるケースでは、設定項目を作りたいときなどには Comapnion を使うことになります。

# プロジェクトを作る
ターミナルなどを立ち上げ、プロジェクトを生成したいディレクトリへ移動し、以下のコマンドを入力します。

```
npx create-fitbit-app [プロジェクト名]
```

Versa 3 / Sense よりも前のデバイスを対象としたものを作りたい場合は、[オプションで SDK バージョンを指定することができます](https://www.npmjs.com/package/create-fitbit-app)。ただし、4.x と 5.x とでは互換性がないため、Versa 2 と Versa 3 / Sense 両方に対応した文字盤を作ることはできません。

```
npx create-fitbit-app test-clock-3 --sdk-version 4.3.1
```

[プロジェクト名] がディレクトリ名となり、中に必要なファイルが準備されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314249/c231f339-96bb-744e-3677-b6c1bb6cf66c.png)
しばらくすると作るアプリの種類やアプリ名などが聞かれます。まずアプリの種類は、今回は文字盤を作るので `clockface` を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314249/70e8f663-5ca7-93d1-593d-731bd1dda857.png)
次にアプリ名ですが、プロジェクト名と同じであればこのまま Enter キーで次へ進んでもかまいませんが、ここで好きなアプリ名へ変更することもできます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314249/8e31d4de-b704-6b13-1a09-82acecfe7967.png)
Companion コンポーネントを作るかどうかを聞かれます。文字盤の設定画面を作るときなどには `Y` を入力します。今回は作らないので `N` で OK です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314249/0b1f790b-95cd-d991-4482-175af56a6297.png)
最後にどのデバイス向けに作るかですが、今回は Versa 3 と Sense しか選べない上、文字盤の作成に際してどちらも変わりはないので、どちらも対応としておいて大丈夫です。
SDK 4.x 対応のデバイスに向けて作る場合、[Versa シリーズと Ionic とではディスプレイ解像度が違うなど差異があること](https://dev.fitbit.com/build/guides/multiple-devices/)に注意してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314249/d0f0a2eb-887e-1bbd-49c8-8cd5f6e1cd46.png)
これでできあがりました。修正したい場合は、プロジェクト内にある `package.json` を編集します。

これでもう作ることは可能ですが、作成したプロジェクトのディレクトリへ移動し、以下のコマンドを入力することで TypeScript への変換および入力補完に対応します。

```
npx fitbit-sdk-types
```

# とりあえず作ってみる
文字盤なのでとりあえず時間を出すところから始めます。 `resources/index.view` が文字盤の表示になります。SVG っぽい何かで書けますが、ひとまずこんな具合にしておきます。

```text:index.view
<svg viewport-fill="black">
	<text id="timeText" font-size="30" fill="white" text-length="10"/>
</svg>
```

続いてコード側です。メインになるのは `app/index.ts` なので、ここに以下のようにコードを入力します。

```typescript:index.ts
import clock from "clock";
import document from "document";

const timeText = document.getElementById("timeText") as TextElement;

clock.granularity = "seconds";
clock.addEventListener("tick", () => {
    const date = new Date();
    timeText.text = `${date.getHours()}:${date.getMinutes()}.${date.getSeconds()}`;
});
```

割と雑ですがこれで OK です。

## 実際に確認してみる
これで一旦確認してみます。まず、サイドロードしたいデバイスか、Fitbit OS Simulator を準備します。どちらかだけ起動・有効にしておいてください。
ターミナルでプロジェクトのディレクトリへ移動し、以下のコマンドを入力します。

```
npx fitbit
```

初回起動の際、開発者アカウントとしてのログインを求められます。ブラウザーが自動で立ちあがり、ログイン済みであればそのままログインが完了します。この状態で

```
build
```

を入力するとビルドができます。そこから

```
install
```

を入力することで、現在接続が待機しているデバイス・シミュレーターのどちらかに自動接続し、サイドローディングがおこなわれます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314249/224935bb-2c44-607b-1df9-b73b5d1b5b32.png)
これでできあがりです。やりましたね！
なお、 `build` `install` をいっぺんにできる `bi` コマンドもあります。作っては試しを繰り返すならこのコマンドの方が楽です。

## 心拍数や歩数なども取得してみる
スマートウォッチといえばこれらセンサーへのアクセスですよね。SDK ではセンサーへのアクセスも用意されています。
使用したいセンサーはそれぞれユーザーの許可が必要になります[^2]ので、 `package.json` に必要なパーミッションを宣言しておく必要があります。

```json:package.json
"fitbit": {
  (省略)
  "requestedPermissions": [
    "access_activity",
    "access_heart_rate"
  ],
  (省略)
}
```

今回は Activity として別ファイルにクラスとして作成します。 `app/Activity.ts` を作成し、以下のようなコードを入力します。

```typescript:Activity.ts
import { me } from "appbit";
import { today } from "user-activity";
import { HeartRateSensor } from "heart-rate";
import clock from "clock";

export class Activity {
    public stepsCallback: (steps: string) => void;
    public heartRateCallback: (rate: string) => void;

    private deniedText = "DENIED";
    private hrm: HeartRateSensor = null;

    public initialize() {
        // 歩数の取得許可チェック
        if (me.permissions.granted("access_activity")) {
            clock.granularity = "seconds";
            clock.addEventListener("tick", this.updateSteps);
        } else {
            this.stepsCallback(this.deniedText);
        }

        // 心拍数の取得許可チェック
        if (HeartRateSensor && me.permissions.granted("access_heart_rate")) {
            this.hrm = new HeartRateSensor();
            this.hrm.addEventListener("reading", this.updateHeartRate);
            this.hrm.start();
        } else {
            this.heartRateCallback(this.deniedText);
        }
    }

    private updateSteps = () => {
        const step = (today.adjusted.steps || 0);
        this.stepsCallback(step.toString());
    }

    private updateHeartRate = () => {
        const rate = this.hrm.heartRate;

        if (rate == null || isNaN(rate)) {
            this.heartRateCallback("---");
        } else {
            this.heartRateCallback(rate.toString());
        }
    }
}

export default Activity;
```

そして `index.view` に 2 つの Text オブジェクトを作り、それぞれ `stepText` `hrmText` として、 `app/index.ts` は以下のように追記します

```typescript:index.ts
import clock from "clock";
import document from "document";
import Activity from "./Activity";

// さっきの時計の部分は省略

const stepText = document.getElementById("stepText") as TextElement;
const hrmText = document.getElementById("hrmText") as TextElement;

const activity = new Activity();

activity.stepsCallback = x => stepText.text = "Steps: " + x;
activity.heartRateCallback = x => hrmText.text = "HRM: " + x;

activity.initialize();
```

あとは npx fitbit した後にコマンドで `bi` を入力してサイドローディングすると、歩数と心拍数が表示されるようになります。シミュレーター上では、心拍数は任意の数字を入れることができますが、歩数はシミュレーター側で自動生成です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314249/4cca46f8-b78d-c69b-df35-1f87f21754cf.png)
時計としてはあまりにも雑ですが、おおよその構成要素としては満足できるかと思います。ここから、画像を作ったりしてそれっぽく作り替えていけば、オリジナルな文字盤ができるというわけです。
あとはバッテリーの残量や充電状態も取得することができます。こちらは今回省略しますが、[Power API のリファレンス](https://dev.fitbit.com/build/reference/device-api/power/)をもとに実装すればなんとなく作れると思います。

## 実際に作ってみた例がこちら
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314249/4cf2069c-0385-de3f-82ef-de247958b6eb.png)
アナログ時計は[公式のガイドに作り方が載っているので、こちらを参考にして](https://dev.fitbit.com/build/guides/clockfaces/#analog-clock)みてください。

## 注意点
- [フォントは現状システムフォント以外許可されていません。](https://dev.fitbit.com/build/guides/user-interface/css/#fonts)また、システムフォントはプロポーショナルなため、ガチガチなデザインを組むと破綻する可能性が大きいです。固定幅な文字が必要な場合は、画像化して配置し 1 文字ずつ割り当てていくなどの工夫が必要です。
- 今回のコードでは、心拍数の取得をオンにしたままにしています。バッテリーの余計な消費を防ぐために、[デバイスの画面が表示されているかどうか](https://dev.fitbit.com/build/reference/device-api/display/)や、[手首に装着されているかを確認できる](https://dev.fitbit.com/build/reference/device-api/body-presence/) API と組み合わせて、必要なときにだけオンになるようにする工夫をしておくとよいと思います。
- Versa 3 や Sense に搭載されている [SpO2 センサーへのアクセスは許可されていません。フォーラムによれば医学的規制らしい…。](https://community.fitbit.com/t5/SDK-Development/SDK-Enable-Expose-SPO2-Oxygen-Saturation-Sensor-for-Development/m-p/4716520/highlight/true#M14322)
- 今回は時間や歩数の取得に [Clock API](https://dev.fitbit.com/build/reference/device-api/clock/) を使いました。ディスプレイの状態などを監視し自動でコールバックの発火を制御する便利なものですが、最小更新単位が秒なので、アナログ時計のスムーズ秒針を作りたいなどには不向きです。一方で `setInterval` の使用は制限されていないので、秒よりも短い単位で更新したいときは、自分での制御は必要ですが利用も可能です。

## ギャラリーへの公開
[Gallery App Manager](https://gam.fitbit.com/apps) から公開ができます。初めてアクセスした際、開発者名を求められますが、**一度設定すると変更ができない**そうなので、適当な名前をつけて後で悲しい目に遭わないように気をつけてください。
準備ができたら、[Create app] をクリックし、ギャラリーに公開するアプリ名、アプリの種類を選択します。今回は文字盤なので `Clockface` で OK
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314249/9c643279-1886-61d5-08a9-aba2b269b9a3.png)
概要の設定ページに飛びます。
`Tags` は自分が作ったものに近いものを選んでチェックします。
`General details` には、サポート問い合わせ先のメールアドレスまたはウェブサイト URL のどちらかを入力します。どちらかしか入力できないようになっています。
`Localized details` にはアプリの詳細を記載します。デフォルトの英語はこのまま消せないので、雑に書いておきます。ただし、**どの言語も140文字以上の入力が必要**です。なので、日本語で「シンプルな時計です」のような説明だけでは保存すらできません。
`Manage versions` に実際にビルドしたファイルを配置します。作ったものは `build/app.fba` として保存されていますので、これをドラッグ＆ドロップします。横にあるチェックボックスは、このアプリに含まれる権利がすべてクリアになっていることを表明するためのものです。おおよそ大丈夫だと思いますが、チェックを入れないとアップロードを受け付けてくれません。
ファイルをアップロードすると今度はスクリーンショットをアップロードすることができるようになります。スクリーンショットは、文字盤が起動している状態でターミナルから以下のコマンドを入力すると、プロジェクトのディレクトリの直下に png ファイルとして保存されます。

```
npx fitbit
screenshot
```

ここまでおこなうと、非公開状態でギャラリーに存在するようになります。ページ上部の `Link` にあるリンクがアプリのギャラリーページへのリンクになっているので、知っている人だけこの文字盤をインストールすることが可能です。
これをギャラリーの一覧にも載せる公開状態にするには、ページ右上の `Submit for Review` を押して審査に提出する必要があります。

# まとめ
これで、開発の準備からギャラリーへのアップロードといった一連のサイクルができるようになりました。あとはリファレンスなどを駆使し、自分だけの文字盤を作るだけです。
ぜひチャレンジしてみてください。

# 参考文献たち
- https://dev.fitbit.com/
- https://dev.fitbit.com/build/guides/sensors/heart-rate/
- https://blogs.msmvps.com/bsonnino/2021/01/24/creating-a-fitbit-clock-face-with-vscode-and-typescript/

[^1]: WSL を使っての環境整備はサポートされていないようです (当初一生懸命頑張ってうまくいかなくて、ドキュメント見たらサポートしてない旨書かれてました…)
[^2]: ギャラリーからのインストール時にセンサーへの許可を操作するダイアログが表示されます