Garminウォッチ用のデータフィールドを作成する

新しく買ったGarmin Forerunner255用に自作のウォッチフェイスを作成しました。いろいろ苦労したのですが、何とか満足のいくウォッチフェイスができました。
alasixosaka.hatenablog.com
ウォッチフェイスの作成に使ったConnectIQはウォッチフェイス以外にもウィジット、アクティビティ、データフィールドの作成などオリジナルのアイテムを色々と作ることができます。
今回は、データフィールドの作成にトライしてみました。
データフィールドを作成しようと思ったのは、アマゾンのレビューにも出ていますが、アクティビティ中の経過時間の表示が、時、分、秒で表示されるのですが、何故か時の表示だけとても小さい文字になっているのです。
www.amazon.co.jp
上のリンクの写真はForerunner955のものですが、Forerunner255でも同様でした。
そこで、自分でデータフィールドを作ればこの問題は解決するのではないかと考えました。
あと、Forerunner955では表示されるみたいですが、Forerunner255ではナビゲーション中の高低図が表示されません。(Forerunner255で高低図が表示されることがわかりました)これもオリジナルのデータフィールドを作ればなんとかなるのではと思っていますが、こちらの方ははるかに難易度が高そうです。ですので、まずは、普通のデータフィールドを作成し、時間の表示を何とかしたいと思います。

はじめは結構戸惑った。

前回の最後に書いたように、ウォッチフェイスの作成については日本語の参考サイトがいくつかったのでその助けを借りてなんとか完成させることができました。
ところが、データフィールドについてはほとんど日本語の”先生”がいないのです。仕方がないので、英語のサイトやAPI を見ながら進めていきました。
結論から言うと、ウォッチフェイスができれば、あまり問題なくデータフィールドも作成することができます(複雑なことをしなければ)。
一番参考になったのはこちらのサイトです。
starttorun.info
データフィールド作成のためのチュートリアルで、ベースはこのチュートリアルのLayout with dc.draw() callsを参考にしました。ウォッチフェイスの時にも書きましたが、文字列、画像などを配置するときにlayout.xmlを使うのか、直接drawコマンドを使うのか2通りの方法があります。layout.xmlを使う方がフレキシビリティーが高く、いろいろなウォッチに対応させやすいという反面、手続きが面倒というデメリットがあります。自分専用に作る場合は手間を考えるとdrawコマンドを使った方が簡単にできます。ただ、デフォルトのひな形はlayoutを使うようになっていたので、始めは、上記のサイトの存在を知らなくて、ひな形を使っていろいろ試していて、うまくいかず、随分時間を使ってしまいました。

出だしの部分でつまずく

上にも書いたようにまず、ひな形を使ってデータフィールドのプロジェクトを作ってみたのですが、まずはデータフィールド用のプロジェクトを作るところでつまずいてしまいました。データフィールドの作成は、Visual Stuido CodeのMonkey C:New projectで作成するのですが、その時、data fieldを選ぶと、simple と complexの2つの選択肢が出てきます。最初、simpleでは表示するデータが1つだけのデータフィールドを作成でき、complexでは2つ以上のデータを表示するデータフィールドを作成できるものと思っていました。また、シミュレーターで、データフィールドを動かすと、data fieldsのタグからlayoutを選択すると表示するデータ項目数を変更することができて、1から6まで選ぶことができます。このこともあり、complexでは2つ以上のデータを表示するためのものと思ってしまいました。なので、始めは、complexを選択し、出来たlayout.xmlを見て、layout idが5つもあるので、これはデータを5個表示する用のひな形だと勝手に思い込んでいました。ひな形を動かすと、心拍数を表示するデータフィールドができているのですが、このとき、シミュレータで先ほどのdata fieldsタブから4のレイアウトを選ぶとこんな感じに全部同じ表示になってしまいます。

4つとも同じ表示に

当然、5つでも同じことで、全部表示が同じです。
これは、layout.xmlでlayout id が全部同じラベルを持っているからそうなるのだと思って、それぞれ違うラベルに変えてみたものの結果は同じで、ここで、ああでもないこうでもないと試行錯誤やWEBサイトの検索を繰り返していました。
そのうちに、データフィールドのMonkey Cプログラムを公開しているサイトを見て、どうしているのかを調べてみると、そんな複雑なことはせずに、自分で勝手に区切りを入れて、その区切りに合うようにテキストを配置しているだけだと気づきました。
たしかに、シミュレータでもウォッチの実機でも、データフィールドを最初から区切っていくことができるのですが、よくよく考えると、そうすることのメリットは、ユーザー側でどこに、どのデータを割り付けるのかを指定できることくらいしかなく、自分で設計して、決めたデータを決めた場所に配置するだけなら、自分で勝手に区切ってその区切り応じて配置した方がはるかに簡単だということにようやく気付きました。まあ、最初から4つなら4つに区切ったデータフィールドに対応してプログラムを書く方法もあるのかもしれませんが、今のところそのやり方を見つけられていません。でも、自分で勝手に区切ればよいのであれば何も難しいことはなく、ウォッチフェイスでやったように、どこに何を配置するかを決めて、フォントサイズなんかを決めてやればいいだけです。区切りの線が欲しければ自分でこれも書いてやればいいだけの話。

プロジェクトの作り方

まあ、ここまでがいつもの前置き(失敗談)で、ここからが本番です。
先のリンクのチュートリアル layout with dc.draw() callsに従ってプロジェクトを作ります。
作り方は、Visual Studio Code でF1、Monkey C: New Project からデータフィールドを選択します。ここで、上にも書いたようにsimple と Complexの選択肢が出てきますが、complexを選びます。データフィールドの名前を適当に入れるとひな形ができます。このとき、上にも書いたようにlayout.xmlを使うひな形できるので、チュートリアルに従い余分なファイルを削除します。

  • まず、layout.xmlを含むフォルダを削除します。これは、Visual Studio Code上で行います。以下も同じです。
  • 次にxxxxBackground.mcを削除します。xxxxはプロジェクト名です。
  • 次に、xxxxView.mcの中の、function onLayout() を削除します。
  • 次に、同じくxxxxView.mcの中のfunction onUpdate()の中身を空にします。

これで準備完了です。あとは自分の好きなようにレイアウトしてデータを配置するだけです。

まず、データ3つのデータフィールドを作ってみる。

トレラン中に絶対に見る情報として、自分の場合は、トータル距離、経過時間、現在時刻の3つです。老眼であることも考えてとりあえず3つのデータに絞って大きめのフォントで見やすいデータフィールドを目指します。もちろん、この3つのデータを並べて表示するだけなら、ウォッチ上で操作して、新規のデータフィールドを作成し、それぞれにトータル距離、経過時間、現在時刻を割り当てれば済む話で、なにもわざわざオリジナルのデータフィールドを作る必要はないはずですが、上の方にも書いたように、そうすると経過時間の時、分、秒の時のフォントがなぜかとっても小さなフォントで表示されてしまい、老眼で読むのはとてもつらいことになってしまいます。もちろん、1時間なのか、2時間なのかは見なくてもわかることが大半だし、現在時刻を見て逆算すれば仮にフォントが小さくて読めなくても見当はつくのですが、疲れてきて、頭が白くなってきた状態でいろいろ考えるのは面倒なので、大きく表示できるのなら表示したい。ところが、ウォッチを操作して作るデータフィールドはフォントの指定ができないという欠点があり、字を大きくすることができないのです。なので、自分でフォントサイズを指定できるオリジナルのデータフィールドの出番です。

配置を決める

配置は、上から順に、トータル距離、経過時間、現在時刻としました。そして、一応仕切りの線を入れてみました。線の色は、ウォッチを操作して作るデータフィールドと区別するために青のラインにしてみました。フォントのサイズは、一番大きい真ん中の経過時間の時と分がFONT_NUMBER_HOT。秒はそれほど重要ではないので少し小さめのFONT_NUMBER_MILDとしました。トータル距離はFONT_NUMBER_MEDIUM。時刻は表示桁数が多いので小さめのFONT_NUMBER_MILDとしています。また、それぞれのラベルは、FONT_TINYを使いました。

今回作ったデータフィールド

プログラム

プログラムの本体xxxxView.mc (xxxxはプロジェクト名)を順番に説明していきます。drawコマンドを使っていますので、本体以外はmanifest.xmlとxxxxApp.mcの2つのファイルだけで、どちらもデフォルトから手を加えていませんので説明は省略します。

モジュールの読み込み

冒頭の部分です。

import Toybox.Activity;
import Toybox.Graphics;
import Toybox.Lang;
import Toybox.WatchUi;

4つのモジュールを読み込んでいますが、この部分はデフォルトのままです。いまだにimportとusingの区別がわかっていません。ネットでよく見るソースコードではたいていusingを使っています。どちらも大差ないようですがとりあえずデフォルトのままにしてあります。

変数

モジュールの読み込みの次からプログラムの本体が始まります。プロジェクト名はdatafield2としていますので、ファイル名がdatafield2View.mcでクラス名定義でdatafield2View extends WatchiUi.DataFieldとデータフィールド関連の関数を呼び出せるように宣言しています。その次がプログラム全体で使う変数群です。

class datafield2View extends WatchUi.DataField {
    hidden var mValue as Numeric;
    hidden var valueFormat = "%d";	
    hidden var label = "距離";  // intial value for the label
    hidden var label2 = "時刻";
    hidden var label3 = "タイム";
    hidden var clockTime;
    hidden var eTime;
    hidden const ZERO_TIME = "0:00";
    hidden const ZERO_TIME2 = "00";
    hidden var distanceUnits = System.UNIT_METRIC;
    hidden var distance;
    hidden const ZERO_DISTANCE = "0.0";
    hidden var kmOrMileInMeters = 1000;

最初の2つ、mValueとvalueFormat はデフォルトの変数ですが、後の方で書くようにデータフィールドの区切りが1つでない時にエラーにならないように一応残してあります。本来の機能には不要です。
その次の3つがラベル用の変数で、それぞれ、文字列の距離、時刻、タイムです。
clockTimeが現在時刻用の変数で、eTimeが経過時間用の変数。ZERO_TIMEとZERO_TIME2はアクティビティーがスタートする前の初期の状態で画面に表示するための定数です。distanceUnitsは距離を表示するための単位(今回はkm)。distanceは距離用の変数。ZERO_DISTANCEは経過時間の時と同じく初期状態表示用の定数です。初期状態では変数にnullが入ってしまうので定数を指定してnullの時に表示するようにしています。kmOrMileInMetersは距離をkmに換算するための計算用の値です。距離はmで得られるのでkmに換算するのに1000で割って計算します。

初期化関数

デフォルトのままです。mValueを使わない場合は2行目は不要です。

function initialize() {
    DataField.initialize();
    mValue = 0.0f;
}
計算

どうもこのcomputeという関数と、後で出てくるonUpdateの関係が良くわかっていないのですが、とりあえず、アクティビティー中の更新されたデータをここで変数に入れるように使うようです。なので、現在時刻、経過時間、距離をそれぞれの該当の関数に代入しています。経過時間は最初info.elapsedTimeとしていたのですが、こうするとポーズを入れても時間がどんどん進んでしまいます。自分の場合、ランやトレランで休憩したときにポーズして時計を止めることはしないのでどちらでもよいのですが、一応ポーズすると時間が止まるinfo.timerTimeの方を選んでいます。

function compute(info as Activity.Info) as Void {
        // See Activity.Info in the documentation for available information.
        
    clockTime = System.getClockTime();
    //eTime = info.elapsedTime;
    eTime = info.timerTime;
    distance = info.elapsedDistance;
        
}
Obscure
function isSingleFieldLayout() {
    return (DataField.getObscurityFlags() == OBSCURE_TOP | OBSCURE_LEFT | OBSCURE_BOTTOM | OBSCURE_RIGHT);
}

これもよくわかっていないのですが、APIを読むとgetObscurityFlagsの説明は以下のようになっています(googleで日本語に翻訳)。

隠されている画面領域を取得します。
非長方形の画面では、画面の特定の部分が隠れます。たとえば、丸いスクリーンは正方形のスクリーンの角を効果的に切り落とします。このメソッドは、デバイス上の隠された画面領域に一致する、WatchUi.DataField.OBSCURE_* 定数によって定義された列挙値の合計を返します。このメソッドの使用は、 onUpdate()の呼び出し中にのみ有効です。

なので、ウォッチのような丸いスクリーンだと結果がtrueになるのかと思われます。この関数は後でonUpdateの所で呼び出しています。

アップデート

onUpdateは長いので少しずつ説明します。
まず、関数内で使用する変数の定義です。画面の幅をwidth、高さをheightに代入しています。また、textCenterには、テキストを真ん中に寄せる設定を入れています。backgroundColorには背景色を入れています(今回は白)。
次の2行が画面消去で、その次の3行が画面の区切り線の描画です。色は青です。

function onUpdate(dc as Dc) as Void {
    var width = dc.getWidth();
    var height = dc.getHeight();
    var textCenter = Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER;
    var backgroundColor = getBackgroundColor();
    // set background color
    dc.setColor(backgroundColor, Graphics.COLOR_TRANSPARENT);
    dc.fillRectangle (0, 0, width, height);
    // set foreground color
    dc.setColor(Graphics.COLOR_DK_BLUE, Graphics.COLOR_TRANSPARENT);
    dc.drawLine(0,85,width,85);
    dc.drawLine(0,165,width,165);
        
    dc.setColor((backgroundColor == Graphics.COLOR_BLACK) ? Graphics.COLOR_WHITE : Graphics.COLOR_BLACK, Graphics.COLOR_TRANSPARENT);     

次が距離と時刻と経過時間の表示の部分です。
まず、現在時刻をtimeStringの文字列に代入します。
durationは経過時間の時、分、表示用の変数で、duration2が経過時間の秒、表示用の変数です。秒は小さいフォントで表示するので変数を分けています。
その次が画面全体のレイアウトの部分です。先ほど定義したisSingleFieldLayoutを呼び出して、trueならif文の中を実行してレイアウト表示をします。距離、経過時間、時刻のラベルを表示しています。現在時刻の表示もここで行います。
distStrは距離用の変数です。その次のif(distance==Null)の所が上で説明したアクティビティースタート前で距離がゼロの時Nullになっているときの処理です。elseの所が距離が何等かあるときの処理で距離数をkmに直して先ほどのdistStrという変数に文字列で代入しています。その下のdrawコマンドが距離の表示です。
その下からが経過時間表示の処理で、eTimeに何らかの数値が入っていれば経過時間に文字列を代入します。このとき、上で書いたように時、分と秒で分けて文字列を作成します。最後のelseの処理は数値がなくNullの場合で、初期値用の文字列を代入します。
最後の2行のdrawコマンドで経過時間を表示しています。

    var timeString = Lang.format("$1$:$2$:$3$", [clockTime.hour, clockTime.min.format("%02d"), clockTime.sec.format("%02d")]);
    var duration;
    var duration2;
    // do layout
    if (isSingleFieldLayout()) {
        dc.drawText(width/2, 30, Graphics.FONT_TINY, label, textCenter);
        dc.drawText(width/2, 180, Graphics.FONT_TINY, label2, textCenter);
        dc.drawText(width/2, 210, Graphics.FONT_NUMBER_MILD, timeString, textCenter);
        dc.drawText(width/2, 100, Graphics.FONT_TINY, label3, textCenter);
        dc.drawText(190, 70, Graphics.FONT_TINY, distanceUnits == System.UNIT_METRIC ? "km" : "mi", textCenter);
        var distStr;
        System.println(distance);
        if (distance == null) {
            distStr = ZERO_DISTANCE;
             }
        else {
            var distanceKmOrMiles = distance / kmOrMileInMeters;
            distStr = distanceKmOrMiles.format("%.1f");
        }
        dc.drawText(width/2, 60, Graphics.FONT_NUMBER_MEDIUM, distStr, textCenter);
        if (eTime != null && eTime > 0) {
            var hours = null;
            var minutes = eTime / 1000 / 60;
            var seconds = eTime / 1000 % 60;
                
            if (minutes >= 60) {
                hours = minutes / 60;
                minutes = minutes % 60;
            }
            
            if (hours == null) {
                duration = "0:"+ minutes.format("%02d");
            } else {
                duration = hours.format("%d") + ":" + minutes.format("%02d");
            }
            duration2 = seconds.format("%02d");
        } else {
            duration = ZERO_TIME;
            duration2 = ZERO_TIME2;
    } 
    dc.drawText(width/2, 130, Graphics.FONT_NUMBER_HOT, duration, textCenter);
    dc.drawText(220, 140, Graphics.FONT_NUMBER_MILD, duration2, textCenter);

最後は上にも書いたようにおまけですが、isSingleFieldLayoutがFalseの時(データフィールドが複数に区切られているとき)の処理で、label(ここでは距離)とmValueを表示しています。mValueは何も計算していないので常にゼロです。ここはレイアウトの紐づけを間違わなければ本来不要のはずです。

    
    } else {
        dc.drawText(width / 2, 5 + (height - 55) / 2, Graphics.FONT_TINY, label, textCenter);
        dc.drawText(width / 2, (height - 23) - (height - 55) / 2 - 1, Graphics.FONT_NUMBER_HOT, mValue.format(valueFormat), textCenter);
    }

ちなみにシミュレータでデータフィールドのレイアウトを3分割にするとこんな感じになります。

ウォッチ側で3分割を選択した場合。

やっぱり3つとも同じ表示になってしまいます。

だいぶ長くなったので今回はここまでにしようと思います。次回は、実機に作ったデータフィールドを転送するやり方と、また別のデータフィールドについて書いてみたいと思います。