※続編はこちら
このあと、まずユーザー Rating を Totonoi Score に反映する補正ロジックを実装し、さらにその後のログを振り返った記事も書きました。
- Totonoi ScoreにユーザーRatingを反映させてみた —— 客観と主観をつなぐ補正ロジック
- サウナの客観スコアに主観Ratingを足してみた。その後のログで見えたこと
- TotonoiScoreの計算ロジックを見直した話
はじめに
サウナが好き!
だからログとれるサウナウォッチが欲しい……!
と思ったものの、普段からスマートウォッチを使っているし、
サウナに入る前にわざわざ付け替えるのも面倒。
しかも、その間のログは普段のスマートウォッチに残らない。
2個つける?
鍵+スマートウォッチ+サウナウォッチ?
……邪魔すぎる。
それなら
「今使っているスマートウォッチにサウナアプリを入れればよくない?」
と思い立ちました。
ちょうどChatGPT Plusを使っていたので、
Codexを使って作ってみよう! と開発を開始。
時計側アプリ作成 → Web Viewer作成 → スマホ版アプリ作成
という流れで作ったので、その順番で書いていきます。
ちなみに今回使った
Garminの Monkey C、Webアプリ開発、スマホアプリ開発はすべて未経験。
正直、ゼロからのスタートでした。
それでもここまで形にできたのが、生成AIの凄さだと感じています。
⚠ 注意事項
高温下でのスマートウォッチの使用は推奨されていません。
故障のリスクがあることをご承知おきください。
開発環境
- VSCode
- Garmin SDK
- Codex
- ChatGPT(以下チャッピー)
- Garmin Instinct 2
- Androidスマートフォン
仕様設計
まずは仕様を考えるところから。
これもチャッピーと相談しながら、自分の欲しい機能をリストアップしました。
- タイマー機能(サウナ・水風呂・休憩を管理)
- タイマー編集機能
- ログ機能
ととのいは感覚の世界ですが、
せっかくなら「記録」としても残したい。
そこから自分好みに仕様を組み立てていきます。
仕様が固まったら「到達点」を決めて、そこに向かって実装していきます。
ここが曖昧だとAIも曖昧なコードを書きます。
逆に仕様が明確だと、驚くほどちゃんと動きます。
実装指示の出し方
チャッピーにこう依頼します。
「Codexで仕様をもとに実装指示を出すので、その指示文を書いてください」
すると実装指示を出してくれるので、それをCodexに貼り付けて実行。
※「コードブロックで出して」と指示するとコピペが楽なのでおすすめです。
Codexは推論レベル高モードで使っていました
ほとんど自動で作ってくれるので、人間がやることは
- ビルド結果確認
- デバッグ
- 実機確認
くらいです。
「コードを書く」よりも
「仕様を考える」方がメイン作業になります。
Garmin(Monkey C)で苦戦した点
Garminは Monkey C という独自言語で書かれています。
Codexでもそこそこエラーが出ます。
特に気をつけたのは:
- 三項演算子を使わない
- 1行ifでも必ず
{}を付ける - 型まわりを曖昧にしない
AIは万能ではなく、
“言語のクセ”を明示してあげると成功率が一気に上がります。
「API仕様書を読んで」と言っても改善されないことも多々ありました。
動作確認と改善
仕事終わりに少しずつ進め、
実機動作確認までできるようになるまで約2週間。
実機で不満点を洗い出し
→ Codexに修正指示
→ 再ビルド
を繰り返し、満足できるものになりました。
※思い立って作り始めたころはCodexがなく、
チャッピーにコードを書いてもらってコピペしていたので時間がかかりました。
今の環境ならもっと早くできると思います。
実際に使ってみて追加した機能
- 省電力対応
- 動作中タイマーの増減機能
すると次の欲望が生まれます。
ログをもっと見やすくしたい……!
「あの日のととのい、何セット目が一番よかったっけ?」
「時計側でしかログが見られないのは不便。」
「アプリ起動 → ログ確認 → 終了 この動線が地味に面倒。」
そこで次のステップへ。
Web Viewer作成
課題
Garminは制限が強く、外部アプリから直接アクセスが難しい。
「ログを後から振り返れる形」にしたい。
サウナって、その場の体験は最高なんですが
数日後に「あの日めちゃくちゃ良かったよな…」となっても感覚しか残っていない。
それをちゃんとデータとして残したい。
しかし、Garmin Connectには一般開発者向けの公開APIは見当たりませんでした。
手間を考えると、公式経由でどうにかするよりも、
連携している外部サイト(Intervals.icu)から取得する方がシンプルだなと思い、
そちらのルートを選びました。
※Intervals.icu を選んだのは、もともと利用していたサービスであり、
個人利用であれば API キーによる FIT 取得が容易だったためです。
具体的には、以下の流れです。
- 時計側でFITファイル生成(時計)
- Garmin Connectへ同期(時計 → Garmin Connect)
- 外部サイトへ自動同期(Garmin Connect → 外部サイト)
- 外部サイトのAPIを叩きFITを取得(外部サイト → PC)
「ルートを作ればいい」という発想です。
FIT生成の実装
「時計側でFITファイルを作る」部分をチャッピー+Codexで実装。
シミュレータではFIT確認ができないため実機検証が必要でしたが、大きな問題なく完成。
FITのDeveloper Fieldも扱えるようにして、
phaseやsetIndexなどを保持し、後段で再構築できるようにしています。
(心拍ログとフェーズ情報を紐付けられるよう、Developer Fieldで独自フィールドを持たせています)
外部サイトへワークログが同期された瞬間はうれしかった。
Viewerの作成
次はWebベースのViewer。
仕様相談 → Codex実装。
某有名サウナログサービスを参考にして、項目を作成。
表示項目:
- Date
- Duration
- Max/Min心拍
- Totonoi Score(ドーナツグラフ表示)
- 心拍グラフ
- 各セットごとのサマリー
FITは直接読めないため、一度JSONへ変換して描画。
ととのいを「数値」「グラフ」「セット単位」で振り返れる構成にしました。
実際のログ表示はこんな感じです。
ととのいを“感覚”ではなく“データ”で振り返れるようになりました。
※ここにスクショ挿入
スコアは0〜100で正規化しており、色分岐は閾値ベースで実装しています。
心拍変動や滞在時間を重み付けし、セット単位でスコア化しています。
スコア帯によってリングの色が変わるようにしており、
グレー → ピンク → グリーン → ブルー → ゴールド と段階的に変化します。
Totonoi Scoreとは?
Totonoi Scoreは、心拍数の推移や各フェーズ時間などから
独自に算出しているスコアです。
「今日どれだけととのったか」を数値化したもの。
以下、ちょっと数式です(長いので折りたたみました)。興味のある方はどうぞ。
Totonoi Score の計算式(クリックで展開)
各セットのDepth/Speed/Stability を 0〜100 に正規化し、\text{SetScore}
=
\operatorname{clip}
\left(
O
+ A \cdot \text{Depth}
+ B \cdot \text{Speed}
+ C \cdot \text{Stability},
\, 0, 100
\right)
で算出する。
Depth/Speed/Stabilityについては
\text{Depth}
=
100 \cdot
\operatorname{clip}
\left(
\frac{
\left(
\frac{P95_{\text{sauna}} - Min_{\text{rest}}}{P95_{\text{sauna}}}
- D_0
\right)
}{
D_1
},
\, 0, 1
\right)
\text{Speed}
=
100 \cdot
\operatorname{clip}
\left(
\frac{
T_{\text{bad}} - t_{\text{recover}}
}{
T_{\text{bad}} - T_{\text{good}}
},
\, 0, 1
\right)
\text{Stability}
=
100 \cdot
\operatorname{clip}
\left(
\frac{
S_{\text{bad}} - \sigma_{\text{rest}}
}{
S_{\text{bad}} - S_{\text{good}}
},
\, 0, 1
\right)
\cdot
\left(
R_0
+
R_1 \cdot
\operatorname{clip}
\left(
\frac{
D_{\text{rest}}
}{
T_{\text{stab}}
},
\, 0, 1
\right)
\right)
セッション最終値は
\text{SessionScore}
=
\frac{
\sum_i
\left(
\text{SetScore}_i \cdot conf_i
\right)
}{
\sum_i conf_i
}
とする。
※ $conf_i$ は「セット i のスコア信頼度(0〜1)」です。
心拍データの品質が低いほど値を小さくし、
最終スコア計算において、そのセットの影響を弱めるようにしています。
具体的には、
- サンプル不足
- 休憩時間不足
- 心拍欠損や過度なばらつき
- 回復時間が取得できないケース
などに応じてペナルティを差し引き、
信頼度を動的に算出しています。
記号の意味:
A/B/C(各係数)、O(オフセット)、
D0/D1(Depth正規化)、T_good/T_bad(Speed閾値)、
S_good/S_bad(Stability閾値)、T_stab(時間基準)、
P95_sauna(サウナ中心拍95%値)、Min_rest(休憩中最小心拍)、
R0/R1(時間補正係数)。
数式の係数は「えいや」で決めています。
少し理屈っぽく見えますが、
「どれだけ心拍が大きく落ち、どれだけ早く落ち着き、どれだけ安定していたか」
を数値化したものです。
※ Depth は P95_sauna − Min_rest をベースに算出しています。
※ Speed は回復時間 t_recover を指標としています。
※ Stability は休憩中の心拍ばらつき σ_rest を利用しています。
その体験を心拍データから指標化し、重み付けして0〜100に正規化したスコアです。
心拍変動や回復指標に関する論文をチャッピーと一緒にリサーチし、
それを参考にロジックを組み立てました。
今後は、Viewer上で入力するRating結果も加味して
計算式を進化させていければと思っています。
ちなみに「ととのい値」にすると。
某有名サウナログサービスと被ってしまうので、
せめて英語にしました
さらに欲が出る
FITも自動DLできるようにすればよくない?
もちろんCodexで実装。
最終仕様:
- FIT自動DL
- Location登録
- Rating
- Date
- Duration
- Max/Min心拍
- Totonoi Score
- 心拍グラフ
- 各セットごとのサマリー
ここまで約1週間。
ととのいを、感覚だけでなく以下のようにデータとして残せるようになりました。
※この後出てくるスマホ版とはTOTONOIの計算式違うのでスコアが低くなっています。

スマホアプリ作成
サウナ好きなら「サウナイキタイ」に投稿したくなっちゃう。
でもPCから投稿は面倒。
じゃあスマホアプリにしよう。
ということでチャッピーとCodexで(以下略)
方針
- WebViewer仕様に準拠
- 将来的にスマホ最適化UIへ発展可能
Codexに「既存WebViewerベースで作成」と指示。
プロジェクト作成から静的解析まで自動でやってくれ、一発ビルド成功。
正直すごい。
細かいUI調整をして、
サウナイキタイ投稿用にも使えるように。

制作期間:約2日。
使用感
Instinct 2 でしばらく試してみました。
熱で落ちることもなく普通に動いています。
物理ボタン操作なので、水風呂でも誤タッチなし。
ボタン操作でタイマーの延長・短縮ができるようにしたことで、
没入感を損なわずに、そのままサウナに集中できるのが地味に便利です。
省電力対応後は、1時間使っても 2〜3% 程度の消費。
(最初は 15%/h くらい減っていて焦りました)
正直、想像以上に実用的です。
ただし、繰り返しますが高温環境での使用は非推奨です。
今後
個人用なので配布予定はありませんが、やりたいことはまだまだあります。
- UIの洗練
- Totonoi ScoreへのRating取り込み
- 時計側アプリのブラッシュアップ
最後に
生成AIを使って開発してみて感じたこと。
- 未経験言語でも作れる
- 自分用ツールなら十分すぎる
- 開発スピードが圧倒的に速い
ただし、
エラーが出たときに
「エラーメッセージをそのまま投げるだけ」では堂々巡りになることも。
そういう時は
「これは〇〇が原因じゃない?」
と仮説を立てて投げると、一気に解決することがある。
つまり、
AIにおんぶにだっこではダメ。
人間側の理解力も大事。
「自分のととのいを自分で可視化できる体験」は、
本来とても感覚的なものです。
それを数値化していくと、
「あぁこの時めっちゃ体調良かったよな」や
「確かにこの日そこまで気持ちよくなかったな」
と振り返ることができるので、
サウナの楽しみ方が、もう一段増えた気がしています。
ただ、理論調査・数式生成・整理といった部分については人間がやるより万倍速いんじゃないかなと思います。
自力じゃ正直、Totonoi Scoreの数式をここまで組み立てられなかったと思います。
生成AI時代の個人開発、
想像以上に現実的でした。
また何か作っていこうと思います。
(ちなみにこの記事の校正もチャッピーと一緒に整理しました。)

