Garminウォッチ用ウォッチフェイスを作る(その7)

実機で動かすと予期せぬ結果に

前回ローパワーモードでも秒の更新を行うべくパーシャルアップデート処理を行うやり方について書きました。
alasixosaka.hatenablog.com
シミュレータでうまく動いたので、出来た出来たと喜んでいたのですが、実機(Forerunner255)で動かしてみるととんでもない事に。
ローパワーモードに入ってフルアップデートからパーシャルアップデートに移るところまでは良かったのですが、そこから秒が00になってフルアップデートが実行されると画面が真っ暗に。そしてパーシャルアップデートに再び移って、秒だけが表示され続けるという現象になってしまいました。
実機での動作をシミュレータで再現できなければシミュレータの意味がないと思うんですが、バグなんでしょうか?

原因を探る

とにかく大事なのは実機の動作なので何とかしなければいけません。
思い当たるのは、秒表示をレイアウトを使った表示からdrawText()を使った表示に変えたときに、フォントのバックグラウンドが青で、フォアグラウンドが黒になってしまっていたことで、どこかで指定してやらなければ同じようなことが起こっていてフォアグランドが黒で表示されてしまったのかと。
そこで、またサンプルプログラムを見直して、onUpdate()の中に下記のような記述があることを見つけました。

dc.clearClip();
dc.setColor(bgColor,bgColor);
dc.clear();
dc.setColor(timeColor,Graphics.COLOR_TRANSPARENT);

APIを読むとclearClip()というのは、パーシャルアップデートの所で指定した更新するための矩形の領域を全画面に戻す関数のようです。
ですので、全画面に戻して、黒を指定して、画面を全クリアして、もう一度カラーを指定する。という動作のようです。
よくわからないのは、doTime()関数の中で、パーシャルアップデートだろうが、フルアップデートだろうが通る処理の所で、上の最後の行と同じ処理をしていて、色の指定はできているはずと思うのですが...。とにかく、この部分をonUpdate()の最初の部分に入れたら実機でもちゃんと動くようになりました。

最終版のプログラム

ということで、ようやく実機でもパーシャルアップデートをうまく動かすことができるようになったので、本チャンのプログラムを修正します。

パーシャルアップデートする部分としない部分を切り分ける

以前作った本チャンのウォッチフェイスでは、月、日、曜日、時、分、秒、心拍数、歩数、バッテリー残量、通知の有無、スマホとの接続の有無を表示していました。
alasixosaka.hatenablog.com
このうち、月、日、曜日、時、分については1分に1回の更新で問題ないのでパーシャルアップデートの対象としません。また、バッテリー残量も毎秒更新する必要もないのでこれも対象外とします。そして、歩数も時計を着けてじっとしているときや時計をはずしているときなどローパワーモードに入るシチュエーションでは更新されないので対象外で問題ないと思います。また、通知に関しても通知が来ればアクティブモードに入るのでパーシャルアップデートは不要と考えます。そうすると、残りは、秒と心拍数とスマホとの接続の有無ということになります。この3つだけをパーシャルアップデートの対象としました。
パーシャルアップデートでは更新する領域を矩形で指定しないといけないので、この3つを1列に並べるように配置を変えます。つまり、秒の位置を少し下げて、やや左に寄せ、その左に心拍数、秒の右にスマホとの接続を示すブルートゥースマークを表示するようにしました。また、心拍数用のハートマークは更新の必要がありませんので矩形表示の外に置いておくことにしました。
それぞれの表示座標は、ハートマークが(90, 150)、心拍数の値が(130, 160)、秒の表示が(180, 160)、ブルートゥースマークが(210, 150)としました。括弧の中はそれぞれのx座標とy座標です。グラフィックスとテキストの表示のy座標が10ピクセルずれているのはテキスト表示が縦方向にセンタリングしていて、グラフィックスはしていないためです。
更新する矩形の設定は、左上の座標が(130, 148)で幅と高さはそれぞれ、100と26です。これでパーシャルアップデートの30ms以内に処理が収まりました。

心拍数への対応

心拍数の取得については、再三紹介している参考サイトに書いてあったのですが、
take4-blue.com
本来はActivity.getActivityInfo().currentHeartRateでとってきた値が現在の心拍数ですが、この方の使っているForAthlete 45ではActivityMonitor.getHeartRateHistoryでとってきた履歴の値でしかうまくいかなかったとのことで、始めはその通りにしていたのですが、実機で試してみるとリアルタイムでの更新ができていないようでした。
そこで、ここのブログに書いてある通り、まず、Activity.getActivityInfo().currentHeartRateを試み、取れなかったときにActivityMonitor.getHeartRateHistory、それでもダメな時は"---”を返すようにdoHeartrate()という関数を作りました。これでリアルタイムに心拍が更新されるようになりました。ウォッチフェイスでの心拍数の毎秒更新にも機種依存があるようです。上のブログではForAthlete 45ではリアルタイムの心拍更新はアクティビティ中でしか行えないとのこと。
主な修正箇所を書いておきます。まず、onUpdate()から

    function onUpdate(dc as Dc) as Void {
        System.println("full update!");
        dc.clearClip();
        dc.setColor(bgColor,bgColor);
    	dc.clear();
    	dc.setColor(timeColor,Graphics.COLOR_TRANSPARENT);
        // Get and show the current time
        var clockTime = System.getClockTime();
        var now = Time.now();
        var nowM = Time.Gregorian.info(now, Time.FORMAT_MEDIUM);
        var nowS = Time.Gregorian.info(now, Time.FORMAT_SHORT);
        var batt = System.getSystemStats().battery;
        //var hearts = ActivityMonitor.getHeartRateHistory(1, true).next().heartRate;
        var info = System.getDeviceSettings();
        var timeString = Lang.format("$1$:$2$", [clockTime.hour, clockTime.min.format("%02d")]);
        var timeString2 = Lang.format("$1$", [clockTime.sec.format("%02d")]);
        var dayString = Lang.format("$1$/$2$ $3$", [nowS.month, nowS.day, nowM.day_of_week]);
        var battString = Lang.format("$1$%", [batt.format("%02d")]);
        //var heartString = Lang.format("$1$", [hearts]);
        var stepstring = Lang.format("$1$", [ActivityMonitor.getInfo().steps.format("%5d")]);
        //System.println (hearts);
        var view = View.findDrawableById("TimeLabel") as Text;
        //var view2 = View.findDrawableById("Label") as Text;
        var view3 = View.findDrawableById("DayLabel") as Text;
        var view4 = View.findDrawableById("BatLabel") as Text;
        //var view5 = View.findDrawableById("HeartLabel") as Text;
        var view6 = View.findDrawableById("shoeLabel") as Text;
        view.setText(timeString);
        //view2.setText(timeString2);
        view3.setText(dayString);
        view4.setText(battString);
        //if (hearts != ActivityMonitor.INVALID_HR_SAMPLE){
        //    view5.setText(heartString);
        //}else{
        //    view5.setText("---");
        //}
        //if (hearts == ActivityMonitor.INVALID_HR_SAMPLE){
        //    heartString="---";
        //}
        var heartString=doHeartrate();
        view6.setText(stepstring);
        //BAT1.draw(dc);
        // Call the parent onUpdate function to redraw the layout
        View.onUpdate(dc);
        //if (info.phoneConnected) {
        //    dc.drawBitmap(50,195, BT);
        //}
        doTime(dc,timeString2,heartString,info.phoneConnected,true);
        if (info.notificationCount > 0) {
            dc.drawBitmap(80,195, mes);
        }
        //if (batt>=50){
            //dc.drawBitmap(140,210, BAT4);
        //}else{
            //dc.drawBitmap(140,210, BAT1);
        //}
        dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_BLACK);
        dc.drawRectangle(140, 202, 20, 10);
        dc.fillRectangle(140, 202, 20*batt/100, 10);
        dc.drawBitmap(90,150, heart);
        dc.drawBitmap(80, 220, shoe);
    }

上にも書いたように、はじめに矩形で指定していたクリップを解除して全画面に戻して、消去、カラーの指定を行います。
また、パーシャルアップデートで毎秒更新する秒、心拍数、スマホとの接続に関する表示はコメントアウト(これらの処理はdoTime()関数にうつしているため)。
心拍数はdoHeartrate()という関数を呼び出して結果をhartString に格納しています。
また、doTime()関数を呼び出して、秒、心拍数、ブルートゥースマークの表示を行います。それ以外の表示はこのonUpdate()の中で処理しています。

次にonPartialUpdate()です。

    function onPartialUpdate(dc as Dc) as Void {
        System.println("partial update!");
        var clockTime = System.getClockTime();
        var timeString2 = clockTime.sec.format("%02d");
        var heartString=doHeartrate();
        var info = System.getDeviceSettings();
        doTime(dc,timeString2,heartString,info.phoneConnected,false);
    }

まず、秒の文字列をtimeString2に代入し、次にdoHeartrate()を呼び出して、心拍数をhaertStringに代入、ifnoにシステムの状態を代入し、それらを引数にdoTime()を呼び出しています。

次は、そのdoTime()です。

    function doTime(dc,time,hart,phone,isFull) {
     	//here is where real things happen.
     	//if it's a full update, just carry on, but if it's a partial, use setClip

     	if(!isFull) {
  			//set the clip so it's just the time.  To keep it simple, I do the entire screen width
  			dc.setClip(130, 148, 100, 26);
  			dc.setColor(bgColor,bgColor);
  			//clear anything that might show through from the previous time
  			dc.clear();     	
     	} 
     	dc.setColor(timeColor,Graphics.COLOR_TRANSPARENT);
     	dc.drawText(180,160,timeFont,time,Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER);
     	dc.drawText(130,160,hartFont,hart,Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER);
        if (phone) {
            dc.drawBitmap(210,150, BT);
        }
        //doing the clearClip() here doesn't work, so I did it in onUpdate()
     	//instead!
     	//dc.clearClip();
     }

まず、パーシャルアップデートの場合、消去する領域をsetClip()で指定し、消去するためsetColor()で黒を指定、clear()で消去しています。
その後は、パーシャルアップデート、フルアップデート共通で、setColor()で色の指定を元に戻し、drawText()で秒の表示、同じくdrawText()で心拍数を表示しています。また、phoneがTrueならブルートゥースマークを表示してスマホと接続されていることを示すようにしています。

最後がdoHeartRate()です。

     function doHeartrate() {
    	var currentHeartrate = Activity.getActivityInfo().currentHeartRate;
    	if (currentHeartrate == null) {
			currentHeartrate = ActivityMonitor.getHeartRateHistory(1, true).next().heartRate;
    	}
		if (currentHeartrate != null && currentHeartrate != ActivityMonitor.INVALID_HR_SAMPLE) {
			return currentHeartrate.format("%d");
		}
		else {
			return "---";
		}
    } 

ここでの処理は、上にも書いたように参考サイトの本来のやり方で記述されている通りです。
繰り返しになりますが、まず、Activity.getActivityInfo().currentHeartRate で現在の心拍数を取りに行きます。結果はcurrentHeartrate に格納されます。中身が空だった場合は、履歴の心拍を読みに行きます。ActivityMonitor.getHeartRateHistory(1, true).next().heartRate で履歴の心拍が得られます。ここでも得られなかったら"---"を返します。
こんな感じで表示されます。

ようやく完成

これで一応満足のいくウォッチフェイスができました。
それにしても、結果的にレイアウトをほとんど使わずに直接描画というやり方になってしまいました。たぶんパーシャルアップデートを使ってもレイアウトを使った表示をすることはできるのではないかと思います。今回は、とにかく完成させるのを優先したためにサンプルプログラムのやり方を踏襲してやってしまいました。あまりスマートでないような気がします。特に多数のウォッチに対応させるならレイアウトを使った方がスマートにできると思います。この辺はアンドロイドのプログラムに慣れていれば割と簡単に対応できるのではないかと思いました。
この次は、データフィールドにチャレンジしてみたいと思います。日本語の参考になるサイトがほとんどないのでどうなるかわかりませんが。

比叡山国際トレイルランニングの所要時間をシミュレーションする

来年5月の比叡山国際トレイルランニングレースの参加を目指しています。何といっても50kmで制限時間が11時間。50マイルで制限時間が11時間半という厳しいレース。ことしの弘法トレイルが44kmで制限時間12時間だったのを考えると相当に厳しいです。
そこで、実際のコースを走ってみてどのくらいかかるのか検討してみました。
これまで、50kmのコースを3度に分けて試走してみました。
alasixosaka.hatenablog.com
alasixosaka.hatenablog.com
alasixosaka.hatenablog.com
今回はその結果をもとに実際にレースでどのくらいの時間で走れるかシミュレーションしてみました。
3回の試走のタイムをコースから外れた時間を差し引いて、単純に足し算するとだいたい10時間12分かかっています。最初計算したときは10時間45分くらいかかっていて、こんなん絶対無理じゃんと思ったのですが、よく考えいると横川で拝観していた時の時間を差し引くのを忘れていました。
ということで何とかまともな時間で走ってきたということなのですが、もちろん、毎回毎回フレッシュな状態でスタートするので、本番でこんなタイムで走れるはずもないです。
そこで、疲れなどを考慮してタイムが落ちることを勘案し、試走時、特に1回目、2回目は暑かったのでレース本番とは異なることも考え、1回目、2回目のタイムはそれほどいじらず、3回目は冬場の走りやすい時期であったこと、前半結構飛ばしたことを考えて、タイム加算を少し多めにとって計算してみることにした。
また、過去の大会に参加された方のブログやYouTube動画なんかも参考にしました。
www.youtube.com
rashisabase.com
とくにYouTubeに動画をアップされている方のタイムテーブルがとても参考になりました。本当にこういうのをアップしてくださっている方には感謝しかありません。
ただ、この方は試走はされていなくて、23kmのコースを走った時のタイムや過去の経験からタイムを出されていたようで、想定と実際が結構違っている箇所があったのでその辺はその方の実際のタイムと自分のタイムを見比べながらシミュレーションしました。
どちらの方も強調されているのが、前半根本中堂前まで戻ってくるまでのタイムがポイントとのこと。YouTubeの方は根本中堂で4時間8分。ブログの方はほぼ4時間。ちなみに、YouTubeの動画の方は横川のエイドでタイムアウト、ブログの方は完走されています、どちらも今年のレースです。
また、YouTubeの動画の中で他のランナータイムもチェックしておられますが、4時間20分を越えるとほぼ完走できていない(動画上で確認できたのは完走者が1名だけ)。
ブログの方の記事でも4時間15分過ぎたら後半飛ばさないと間に合わないと係の人に言われていたとのことなのでだいたい当たっているのかなと思います。
ところで、自分のタイムはというと単純な足し算で4時間20分かかってました。すでにピンチですね。
ちなみに、ブログを参考にさせていただいた方も、YouTubeに動画をアップされています。ただ、タイムラインを検討する上では動画よりもブログの方が参考になりました。
結論から言うとシミュレーション上のタイムは10時間37分で収まっています。じゃあ、後半はタイムの見積もりがだいぶ甘いのではないかということになるのですが、確かに試走の3回目は頑張って走ったのですが、とはいえ、下りの区間でタイムがkmあたり1分から1分半くらい遅くなるくらいではないかと見込んでいます。これは過去のレース経験から。なのでそんなに甘々でもないのかなと思うのですが。むしろ前半が暑かったせいとコースがわかり難いせいでちんたら走ってしまったというのが実際のような気がします。でも、さすがに4時間は無理なような気もしますが。普通に流れに乗って走って、まあいいとこ4時間10分くらいでしょうか。

もう少し、区間別にタイムを詳しく見ていきます。
まずスタートから第一エイドのロテルド比叡までで9.7kmです。ここは試走の時は2時間19分かかりました。2回ほど道に迷って立ち止まっているのと、ここはエイドがあって休憩を取る場所なので、プラスマイナスを考慮してここまでの目標時間は2時間20分としました。
次が坂本に降りていくところです。試走の時は1回目は坂本に降りて終了、2回目は坂本からスタートしたので、日吉東照宮の下を自分的なチェックポイントとしました。ここまでで、第一エイドから1時間7分かかっています。まあ、タイムを増減する要素があまりないのでそのままの1時間7分としました。
次が、坂本から第二エイドの根本中堂までです。ここの登りは53分で登っています。ただ、暑かったとはいえ元気いっぱいの状態で登っていますので、本当はもう少しかかりそうなんですが、そうすると根本中堂着が4時間20分を越えてしまいますので、目標は53分で登ることにして根本中堂着までをトータル4時間20分としました。ちなみにエイドでの休憩時間もなしなので、この目標はかなりハードルが高そうです。それでも、大会当日はエイドの人に頑張らないとタイムオーバーするよと言われそうですが。距離は第一エイドのロテルド比叡からで8.9km。トータルで18.6km。
次がせりあい地蔵の第三エイドまでです。この区間は6.5kmで1時間22分かかっています。ここは、拝観券の売り場で境内を走らないように注意されたので歩いた分遅かったと思いますが、試走の時は坂本スタートで疲れが少ないので実際のレースの時のペースダウンを考えてトントンくらいと考えて試走と同じタイムにしています。
その次はコース図上では小野山の予備関門ですが、試走の時は仰木峠で2回目を終了していますので、一旦仰木峠までで区切ることにします。仰木峠までは、試走の時は34分でした。少し余裕を見てこの区間は40分としました。
次が小野山予備関門までですが、距離が短く試走の時は16分。想定タイムは17分としました(単に端数を切り上げただけです)。せりあい地蔵から3.5km、トータル28.5kmです。
次が南庄給水所で区間の距離が4.4km。試走のタイムは43分。想定タイムは少し余裕を見て45分。
その次が仰木の第4エイド。区間距離は3.7km、トータルで36.7km。試走のタイムは36分。想定タイムは38分。
次が元三大師のウォーターステーションで区間距離は3.8km。試走のタイムは23分。ここは飛ばした区間なので想定タイムは30分としました。
次が横川の第5エイド。区間距離は3.3km。トータルで43.8km。試走の時のタイムは55分。ここは一度道を間違えて短時間のコースアウトをしているのと、木段の登りの途中でおにぎりを食べたので、疲れてタイムは落ちてきているはずですが、ロスタイムもあったので、本番の想定タイムも同じ時間の55分としました。まあ1時間でもいいかもしれませんが。
最後、ゴールまでですが、距離は6.2km。試走のタイムは1時間でした。疲れとエイドでの休憩を考えて想定タイムは1時間10分としました。試走の時も途中飛ばした影響で相当疲れていたので、下りは多少遅く、登りはそんなに変わらないタイムで行けるような気がしています。ですので、10分増しは結構余裕を見てのタイムです。
表にまとめるとこんな感じになります。





距離

区間距離

想定時刻1

想定時刻2

スタート

09:00:00

09:20:00

ロテルド比叡

9.68

11:20:00

11:40:00

坂本駅

12:27:00

12:47:00

根本中堂

18.59

8.91

13:20:00

13:40:00

せりあい地蔵

25.06

6.47

14:42:00

15:02:00

仰木峠

15:22:00

15:42:00

小野山予備関門

28.54

3.48

15:39:00

15:59:00

南庄給水所

32.98

4.44

16:24:00

16:44:00

仰木AS

36.7

3.72

17:02:00

17:22:00

元三大師WS

40.53

3.83

17:32:00

17:52:00

横川AS

43.84

3.31

18:27:00

18:47:00

ゴール

50

6.16

19:37:00

19:57:00
想定時刻の1が第一ウェーブの時、2が第二ウェーブの時の想定です。まあ、持ちタイムが遅いので第二ウェーブは確定だと思いますが。
関門時間は記入してませんが、第二エイドの根本中堂が14時。小野山の予備関門が17時40分。第五エイドの横川が19時です。
第二ウェーブに入るとスタートが20分遅くなっても関門時間が同じという不利な状況になります。
想定タイム上は問題なく通過できることになっていますが、一番厳しいのがやはり第五エイドの横川。ここでの余裕はわずか13分。タイムが伸びないとここでタイムオーバーになる公算が大ですね。
ただ、第二ウェーブで19時にここを出たとしても、ゴールまでの残り時間は1時間20分なので、厳しいことに変わりないと思いますね。
また、もし仮に第一ウェーブに入ったとして、関門ぎりぎり第五エイドを通過という状況ならゴールまで1時間しかなく、かなり頑張らないと厳しいことになりそうです。ただ、第二ウェーブなら最後まで何とか走れる人なら1時間20分はかからないような気もするので、第二ウェーブには厳しい関門時間というのは間違いなさそうです。
手元の計算では横川の第五エイドまでが約44kmと弘法トレイルとほぼ同じ距離です。この時は9時間48分かかっています。なので同じタイムで走ったとすると第二ウェーブならタイムオーバーでここで失格です。その時は途中で右ひざが痛くなって最後は歩いていたので、最後まで走っていればもう少しいいタイムでゴール出来てたとは思うのですが。ちなみに弘法トレイルの獲得標高は約3000mです。比叡山国際の場合は公称で3700m。なので登りももう少し多いです。ただ、ガーミンコネクトにGPXファイルを読み込んだところ獲得標高は3069mと表示されました。どちらが正しいんでしょうね。ガーミンの表示を信じると何とかなりそうな気もしますが、公称どおり3700mあるとすると弘法トレイルの時よりも頑張らないとだめということになります。まあ、コース的には林道を走れる区間が結構あるのでやっぱりそこをどれだけ走れるかがカギになりそうですね。そこをちんたらしているようでは完走は無理ということでしょうか。
スピードを試算すると、第二エイドの根本中堂まで20kmとして、4時間だと、ちょうどkmあたり12分です。やはりこれは自分にはちょっとオーバーペース気味の速度のような気がします。4時間10分だと12.5分、4時間20分だと13分という計算になります。過去のレースから考えるといいところ4時間10分くらいというところでしょうか?
また、第五エイドの横川まで、手元の計算では44kmでしたが、ブログなんかを見ていると45kmと書いてあって(公式のルート図には何故か距離が書いてない)、厳し目に見て45kmあるとすると、第二ウェーブを想定してここまで9時間40分で来ないといけないと考えるとkmあたりのペースが12.9分くらいということになります。なのでイーブンペースで走れれば前半はトレイルが主体でアップダウンも多くまた、スタート直後の渋滞もあるのでスピードが遅いと考えると、前半はkmあたり13分でも、後半林道を走ってペースアップできればゴール出来るのかなと思ったりします。要は後半まで持つスタミナとスピードですね。
なので、前回の試走の時も書きましたが、もう少しスピードをつける練習が必要なようです。もちろん登りの練習もですが。
個人的には要項にフルマラソン4時間以内でないと完走できないと書いてあるのが気に食わなくて、若いころはサブ4で走ったこともありましたが、今の走力では到底無理なのはわかっていますし、参加資格の4時間半も無理そうですから。それでもこんな鈍足でも完走できるぞというのを見せたくて、意地でも完走してやるという気持ちでいるのですが。まあ、あと半年頑張ってトレーニングして完走したいと考えています。

Garminウォッチ用ウォッチフェイスを作る(その6)

なんだかんだ言ってこのシリーズも6回目まで来てしまいました。何回もかかるということはいろいろ苦労しているという証拠なんですが。
前回は、完成したウォッチフェイスを実機(Forerunner255)で試したところローパワーモードに入ってしまって毎秒更新が止まってしまうというところまで書きました。
alasixosaka.hatenablog.com
今回は、なんとか秒針を毎秒更新することができるようになったのでその辺のところを書いてみます。
今回の記事は失敗談が大半なので、結論を知りたい方は途中を読み飛ばして最後の方を見てください。
前回、ローパワーモードに入ると、毎秒更新にはonPartialUpdate()という関数しか使えなくなるということは書きました。そして、実際に試したところ、パーシャルアップデートがすぐに使えなくなって困ったということまで書きました。
これまでは、日本語のサイトで参考になるサイトがあったので、割とスムーズに開発が進められてきましたが、英語のサイトも見ないと進めなくなってしまいました。
日本語のサイトとしては、以前にも参考にしたこちらのサイトに、関連する記事がありました。
take4-blue.com
ただ、恥ずかしながら記事に書かれている内容が半分程度しか理解できませんでした。わかる人にはわかるのでしょうが、自分には記述が簡潔すぎて特に処理の部分が理解できませんでした。仕方がないので、英語のサイトを探してガーミンのフォーラムからこちらのサイトを見つけました。
forums.garmin.com
ここにはもう少し詳しい解説が書いてあり、サンプルプログラムも置いてあったので、さっそくサンプルプログラムをダウンロードして動かしてみました。

それでも止まる

サンプルプログラムを動かしたところ、やっぱり現象は同じで1分経過後の次の59秒の所でとまってしまいました。ただ、このサンプルプログラムの優秀なところは、パーシャルアップデートの処理にかかった時間を表示してくれることです。それが最初の参考サイトにも書いてあったWatchFaceDelegateというやつなのですが、使い方がわからず、このサイトのプログラムを見てようやくわかりました。それまでは、英語のサイトやガーミンのAPIを読んで、onPowerBudgetExceededという関数があるのを知り、とりあえず、メインのクラスにこの関数を追加してみました。この関数はパーシャルアップデートの時に、処理時間が設定の30msを越えると発動することになっていて、関数内にSystem.printlnでコメントを書いておけば、関数が発動されたことがわかるはずでした。しかし、ただ書いただけではうまく動かず、一定の作法があることがサンプルプログラムを読んでようやくわかりました。
やり方はこうです。
まず、XXXXview.mc内のメインのクラス(例えば、class XXXXview extends Ui.WatchFace:XXXXはプロジェクト名)というメインのクラスの下にDelegate用の新しいクラスを作り、その中に、次のように記載します。

class XXXXDelegate extends Ui.WatchFaceDelegate
{

	function initialize() {
		WatchFaceDelegate.initialize();	
	}

    function onPowerBudgetExceeded(powerInfo) {
        Sys.println( "Average execution time: " + powerInfo.executionTimeAverage );
        Sys.println( "Allowed execution time: " + powerInfo.executionTimeLimit );
    }
}

function initialize()内でWatchFaceDelegete.initialize()としてまず初期化し、function onPowerBudgetExceed(powerinfo)として関数を書いて、その中にコメントを記述します。powerinfo.executionTimeAverageは実際に1回のパーシャルアップデートの処理にかかった1分間の平均。powerinfo.executionTimeLimitがパーシャルアップデートの1回当たりの処理に許されている時間です。
ただ、これだけは動いてくれなくて、今まで何のためにあるのだろうと思っていたもう一つのファイルXXXXapp.mcを修正する必要があります。修正するのはこの中のgetInitialViewの部分です。デフォルトでは次のようになっているはずです。

function getInitialView() as Array<Views or InputDelegates>? {
        return [ new XXXXView() ] as Array<Views or InputDelegates>;
}

これをサンプルプログラムに倣って次のように書き換えます。

    function getInitialView() {
		if( Toybox.WatchUi.WatchFace has :onPartialUpdate ) {
		//Sys.println("del");    
        	return [ new XXXXView(), new XXXXDelegate()  ];
        } else {
        	return [ new XXXXView() ];
        }        
    }    

Sys.printlnの所はコメントアウトされてますし要らないのですが、ウォッチがパーシャルアップデートに対応していたら、XXXXView()とXXXXDelegate()を返しなさい。対応してない場合はXXXXView()のみを返しなさいということのようです。自分のようにパーシャルアップデートに対応しているForerunner255専用のウォッチフェイスを作ろうとしている場合は、if文もなしでいいと思います。

サンプルプログラムを動かすと、例によってローパワーモードに入って次のフルアップデートごの1分後にパーシャルアップデートが停止してこの関数が発動して時間が表示されます。許される時間は上にも書いたように30msです。実際にかかった時間は約34msでした。ほんの少しオーバーして止まっていることがわかります。
実際に30msというのはどの程度のものでしょうか? まあ、1秒に比べれば約1/333でとても短いのですが、例えばクロックが8MHzのArduino Unoクラスのマイコンを考えると、1クロックが0.125μsですので、240000クロック分の時間ということになります。実際にはArduinoに使われているチップではメインクロックの1/4がCPUクロックに供給される仕様になっているし、処理も1命令1クロックということではないですが、それでもまあまあの処理はできそうです。
もちろんウォッチの中身がわからないですし、どのくらいのクロックで動いているのか想像もつかないですが、結構いろいろな処理はできそうな気がします。

自分で組んだプログラムではもっと処理時間が長い

今度は、前回の最後に作った、デフォルトの時、分表示プログラムに秒表示を足したプログラムに先ほどのルーチンを足して実行してみました。
始めはシンプルに時、分の表示に倣ってパーシャルアップデートの部分をこんな感じにしていました。

    function onPartialUpdate(dc as Dc) as Void {
        System.println("partial update!");
        var clockTime = System.getClockTime();
        var timeString = Lang.format("$1$", [clockTime.sec.format("%02d")]);
        var view = View.findDrawableById("TimeLabel2") as Text;
        view.setText(timeString);
        View.onUpdate(dc);
    }

これで測ってみるとなんと平均の時間は約60msもかかっていました。ただ、サンプルプログラムでは、パーシャルアップデートでも時、分からまとめて更新していますし、いろいろな条件判断も入っていて処理時間は長そうだったのですが。
そこで、サンプルプログラムを見て、秒を書き込むところを下記のようにシンプルに改めてみました。

    function onPartialUpdate(dc as Dc) as Void {
        System.println("partial update!");
        var clockTime = System.getClockTime();
        var timeString = clockTime.sec.format("%02d");
        dc.drawText(120,190,timeFont,timeString,Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER);
    }

だいぶ短くなりました。大きな違いは秒の表示をlayoutを使って表示するのか、直接描画するのかという違いになります。ただ、これをするととても面白いことになります。

秒の表示が変なことに

まず、数字が黒抜きというんですか、つまりバックグラウンドが青で、フォアグラウンドが黒の反転してしまった文字が表示されます。そして、最初のプログラムでは数字が更新されるとちゃんと書き換わっていたのに、こちらのプログラムではどんどん上書きされて行ってしまいます。

ようやく気付いたパーシャルアップデートの意味

ここまで試して、ようやくパーシャルアップデートの意味するところが理解できました。つまり、画面を部分的に書き換えているのがパーシャルアップデート。まるまる書き換えているのがフルアップデートということのようです。過去に電子ペーパーの実験をした時もパーシャルアップデートとフルアップデートの違いがあったなあとようやく思い出しました。そこで、サンプルプログラムをもう一度よく見ると、時間の更新処理はdoTime()という関数で行っていて、次のようになっています。

function doTime(dc,time,isFull) {
     	//here is where real things happen.
     	//if it's a full update, just carry on, but if it's a partial, use setClip

     	if(!isFull) {
  			var y=centerH-(timeHeight/2);
  			//set the clip so it's just the time.  To keep it simple, I do the entire screen width
  			dc.setClip(0, y, width, timeHeight+1);
  			dc.setColor(bgColor,bgColor);
  			//clear anything that might show through from the previous time
  			dc.clear();     	
     	} 
     	dc.setColor(timeColor,Gfx.COLOR_TRANSPARENT);
     	dc.drawText(centerW,centerH,timeFont,time,Gfx.TEXT_JUSTIFY_CENTER | Gfx.TEXT_JUSTIFY_VCENTER);
     	//doing the clearClip() here doesn't work, so I did it in onUpdate()
     	//instead!
     	//dc.clearClip();
     }

ここで、3つの引数を使っています。dcというのはこのプログラムの中核をなすインスタンスのようです(よくはわかっていませんが)。それから、timeというのは時、分、秒を表す文字列です。そして、isFullというのがフルアップデートかパーシャルアップデートかの判定に使う変数でこれがTrueならフルアップデート、Falseならパーシャルアップデートです。プログラムを見てわかるようにフルアップデートの時はif文の所は実行されずに、ただ、カラーを指定して、時、分、秒を書き込んでいるだけです。これに対して、パーシャルアップデートの時は、消去する範囲をdc.setClip()で指定してdc.setColor()で色を指定して、dc.clear();で消去しています。これをやらないと(つまりこれから書き込むエリアをあらかじめ消去しないと)、drawText()でただ上書きされて、さっきのような変な表示になってしまうということです。また、色の指定もここで行っているので、さっきのプログラムは指定していなかったので反転してしまったのだと思います。フルアップデートの場合は、一番下のコメントに書かれているように、このルーチン内では全画面消去ができなかったようで、onUpdate関数の中で、このルーチンに来る前にdc.clear();を実行していて全画面消去をしているようです。

処理にかかる時間は更新するエリアの広さに影響される

つまり、自分で作ったプログラムは毎度毎度全画面更新をしていたので時間がかかっていて、サンプルプログラムは画面を部分的に更新しているから短かったのだと気づきました(それでもタイムオーバーはしていますが)。
なら、サンプルプログラムで更新するエリアをもっと小さくしてやれば時間内に収まるはず。
更新エリアの指定は、dc.setClipの引数で指定しています。引数は順番に0, y, width, timeHeight+1の4つです。yはディスプレイの縦方向の真ん中からフォントの高さの1/2を引いた数です。widthはディスプレイの横幅、timeHeightは時間表示に使うフォントの高さです。APIによるとこの4つの引数は初めの2つが消去するエリアの左上の座標。次が消去するエリアの横幅、最後が消去するエリアの縦ということになっています。つまり、横幅はディスプレイ幅のめいっぱい、縦方向はフォントの高さ+1のエリアを消去するということです。
最もシンプルに更新エリアを小さくする方法はフォントサイズを小さくすることです。デフォルトではFONT_NUMBER_HOTが使われていたので、数字用フォントで一番小さいFONT_NUMBER_MILDに変えてみました。すると、見事に時間内に収まったようで、秒針が止まることなく動き続けました。

自分で作ったプログラムを動かしてみる

ようやくパーシャルアップデートのやり方が分かったので、テストのために作った時、分、秒だけを表示するプログラムを修正してみました。
プロジェクト名はpartialupdateです(そのものずばり)。メインのクラスだけ載せておきます。

class partialupdateView extends WatchUi.WatchFace {
    var timeFont;
    var bgColor=Graphics.COLOR_BLACK;
	var timeColor=Graphics.COLOR_WHITE;

    function initialize() {
        WatchFace.initialize();
    }

    // Load your resources here
    function onLayout(dc as Dc) as Void {
        setLayout(Rez.Layouts.WatchFace(dc));
        timeFont=Graphics.FONT_LARGE;
    }

    // Called when this View is brought to the foreground. Restore
    // the state of this View and prepare it to be shown. This includes
    // loading resources into memory.
    function onShow() as Void {
    }

    // Update the view
    function onUpdate(dc as Dc) as Void {
        // Get and show the current time
        System.println("full update!");
        var clockTime = System.getClockTime();
        var timeString = Lang.format("$1$:$2$", [clockTime.hour, clockTime.min.format("%02d")]);
        var view = View.findDrawableById("TimeLabel") as Text;
        var timeString2 = clockTime.sec.format("%02d");
        view.setText(timeString);

        // Call the parent onUpdate function to redraw the layout
        View.onUpdate(dc);
        doTime(dc,timeString2,true);
    }

    function onPartialUpdate(dc as Dc) as Void {
        System.println("partial update!");
        var clockTime = System.getClockTime();
        var timeString = clockTime.sec.format("%02d");
        doTime(dc,timeString,false);
    }

    function doTime(dc,time,isFull) {
     	//here is where real things happen.
     	//if it's a full update, just carry on, but if it's a partial, use setClip

     	if(!isFull) {
  			//set the clip so it's just the time.  To keep it simple, I do the entire screen width
  			dc.setClip(185, 128, 30, 25);
  			dc.setColor(bgColor,bgColor);
  			//clear anything that might show through from the previous time
  			dc.clear();     	
     	} 
     	dc.setColor(timeColor,Graphics.COLOR_TRANSPARENT);
     	dc.drawText(200,140,timeFont,time,Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER);
     	//doing the clearClip() here doesn't work, so I did it in onUpdate()
     	//instead!
     	//dc.clearClip();
     }

時刻表示用のフォントはFONR_LARGEを使っています。秒更新用にサンプルプログラムと同じく、doTimeという関数を作ってほぼ同じ処理をしています。違いはサンプルプログラムでは時、分、秒を全部更新していたのに対し、パーシャルアップデートでの更新を秒のみにして、時、分はフルアップデートの時のみ更新するようにしたところです。
フォントの具体的なピクセル数がわからなかったので消去する範囲は試行錯誤で決めました。
秒の表示位置は前に作ったウォッチフェイスと同じ場所にして、x=200, y=140としました。消去するエリアはx=185, y=128が左上で、そこから横に30、縦に25ピクセルとするとちょうどよい感じでした。

パーシャルアップデートで秒の更新ができた。

フォントの色はサンプルプログラムのまま使ったので白色になっています。

まとめ

  • ガーミンウォッチはアクティブモードとローパワーモードがあり、ウォッチフェイスを動かしているとき、何もしないと10秒程度でローパワーモードに入る
  • ローパワーモードでは画面の更新は基本的に1分に1回(アクティブモードは1秒に1回)。ただし、ウォッチの機種によってはパーシャルアップデートという1秒に1回更新するモードが用意されている。
  • パーシャルアップデートでは画面の一部分のみを更新し、1回の更新に使える処理時間は30ms。1分間の平均で1回あたり30msを越えたらパーシャルアップデートは使えなくなる。
  • パーシャルアップデートの処理時間を知るには、新しく”プロジェクト名Delgete”というクラスを作成し、そこにonPowerBudgetExceeded()という関数を使うと30msを越えたときに関数が実行される(詳しくはこの記事の中ほどを参照)。または、シミュレーターのWatchface Diagnosticsで知ることができる(詳しくは参考サイトを参照)。
  • パーシャルアップデートの処理時間は主に書き換えるエリアの大きさに依存する。全画面を書き換えるのは無理(たぶん)。
  • 書き換えるエリアの指定はdc.setClip(x1,y1,x2,y2)という関数で指定する。x1, y1はそれぞれ書き換えるエリアの左上のx,y座標。x2, y2は書き換えるエリアの幅と高さ。
  • 消去にはdc.clear()という関数を使う。
  • テキストを書き込むときは、dc.drawText()関数を使う。書き込む位置やフォントはここで指定する。
  • 書き込むテキストの色はその前に指定しておく。dc.setColor()関数。

比叡山国際トレイル50kmコースの試走(後半)

12/9に比叡山国際トレイル50kmコースの残りの試走に行ってきました。
夏の暑い時に前半を走ろうと行ってみたものの暑さに負けて途中でリタイア。1回目は根本中堂から坂本まで
alasixosaka.hatenablog.com
しばらくたって続きを走りに行って、2回目は坂本から仰木峠までと試走しました。
alasixosaka.hatenablog.com

今回は残りの部分、仰木峠からゴールの根本中堂まで走ってきました。
電車と地下鉄、バスを乗り継いで、大原の戸寺バス停へ。土井しば漬け本舗の本店が目の前にある(写真撮り忘れました)。
時刻は朝の9時。バスを降りると、数名の中高年ハイカーがたむろしている。京都駅から来たバスに乗ってきたのかな。
自分が準備をしている間に出発していった。
こちらも準備を済ませて出発する。
今回の装備は、冬場ということで、ザックは大きめのレイドライトのResponsiv24。ウェアは上が、ファイントラックのドライレイヤーにモンベルのウィックロンZEOの長袖シャツ、その上にクロスランナー。下は、クロスランナーの長ズボン。靴は、久しぶりにHOKAのスピードゴートを試した。今回は、足の爪対策として、アマゾンで買ったシリコンゴム製のプロテクターを着けてみた。

似たようなのがアマゾンでいっぱい売られていて、実際に買ったのは上のものとは違うメーカーのものだが、中身は同じものかもしれない。
これを付けると、5本指ソックスが履けなくなるので、今回は、足袋型の靴下を履いた。SYNというやつ。
シリコンのプロテクターを着けてみた感想は、着け心地は悪くなく走っていても気にならなかった。肝心の効果の方は、多少あるかなというくらい。今回は両方の足の親指にはめて走ったが、左の方は内出血せずに済んでいるみたい。右の方は着けても内出血はしている感じ。ただ、いつもだと翌日はかなり腫れて痛いがそこまで痛みは感じないのでそれなりに効果はあるのかなという感じ。
食料は、いつものおにぎり2つ。行動食として、アンドゥー2つ、エナジージェル1つ、一本満足バー1つ。予備として、ナッツを少々と柿の種一袋。水分は1.5Lのハイドレーションにクエン酸と塩(ぬちまーす)を溶かした水。
緊急時用にエマージェンシーシート、ヘッドライト。それに、最近熊があちこちで出没しているので熊鈴。
今回は新しく買ったガーミンのForerunner255にコースを入れてを持って行った。
また、今回は本番を想定してランニングポーチを着けてみた。ポーチと言っても大きな入れ物があるわけではなくベルト状のもので、うすっぺたいものなら入るというやつ。そこに補給食を入れておいた。レイドライトのザックは前ポケットが少なく、今回はハイドレーションを使ったので空いていたが、ソフトフラスクを入れるとそのほかのものがあまり入らない。本番はサロモンのADVSKINを使う予定でポケットはいっぱいあるけど結構深くて前回のダイヤモンドトレイルの時に使いづらかった。補給食や、エイドで水をもらったときに溶かして使う塩やクエン酸の入った容器をポーチに入れておけば出し入れしやすいかと思って使ってみた。使った感想は胸のポケットよりもずっと使いやすくまた、締め付けも気にならないので本番でも使ってみようと思っている。
戸寺のバス停からすぐに山に入っていく。まずは前回のゴールの仰木峠まで登っていく。ここは京都一周トレイルのコースになっているところ。前回は東海自然歩道の方を降りて野村別れまで行ったが、おそばを食べるのが目的だったので、今回はまだ行ったことないルートを選んでみた。東海自然歩道は以前にも東海自然歩道をたどって二ノ瀬から大津京まで行ったときに通っているし。
alasixosaka.hatenablog.com
この道は地図にはボーイスカウト道と書いてあってどういうことかなと思ったら。ボーイスカウトの人たちが整備をしているらしく、木の碑が立っていた。

ボーイスカウト連盟の碑

しばらく登ると東海自然歩道と合流。

東海自然歩道と合流

またしばらく登ってようやく仰木峠に到着。

仰木峠

ここまでにバス停にいたハイカーさんたちに追いつくかと思ったら誰もいなかった。彼らは山登りのような恰好をしていたけど山に登ったわけではかなったみたい。また、この日は予想では最高気温が20℃くらいになるという12月とは思えない気候で、3枚着ていた上を暑くて1枚脱いだ。
さて、ここからようやくトレランのコースイン。
仰木峠からは比良山地の方へ続く尾根道をしばらくたどる。この辺りはアップダウンも緩くて比較的走りやすい。しばらく行くと尾根の右手が開けて琵琶湖が見えた。

仰木峠の少し北の尾根上から琵琶湖を望む

しばらく尾根沿いに進む。
大尾山の手前でいったん林道と合流する。しばらく林道を進むが、林道が尾根から外れて行ってちょっと気を付けないとと思っていたら案の定、林道が行き止まりになっていた。どうやら途中で尾根道に入りなおさないといけないところを見逃したみたい。仕方ないので、尾根道まで斜面をよじ登る。
しばらく尾根道を進むと、大尾山の山頂に到着。仰木峠から大尾山に登ってくる途中で、登りを歩きながら一本満足バーを食べて最初の補給をした。ただ、結構食べ応えがあって1回で食べきれずに3回に分けて食べた。もう少し小さいやつの方が食べやすくて良いかもしれない。

大尾山山頂

そこから少し進んだところで比良山系への縦走ルートとお別れして右に折れる。

縦走ルートと別れて右の枝尾根を降りる

ここの下りがすごかった。途中にロープがいくつも張ってあって劇下りの連続。季節柄下に落ち葉が大量にあって滑るし、2度ほどしりもちをついてしまった。1月の石舞台100を思い出してしまった。たぶんここがコース一の難所。
途中にパラグライダー場があった。

パラグライダー場
眺めは良い

ようやく下り終えて林道に出るが、出口がわからず、適当に崖のようなところから降りてしまった。

林道に出たところ。右手の崖のようなところから降りてきた。

メインのトレイルはここまでで、今日のコースはあとは主に林道や舗装路を走る。
しばらく林道を走って右に折れるが、ここの分岐はあっさり通過してしまった。ガーミンにコースアウトの警告をされて気づいた。ガーミンのコースアウト警告はCOROSに比べるとやや遅いが、しょっちゅう警告されるCOROSよりはこれくらいの方がいいように思う。

林道の分岐。はじめは気づかずに通過してしまった。

ここからは林道をひた走る。ただ、本チャンではここまででかなり走ってきていてしかも大きなアップダウンを繰り返しているのでかなり疲れていることが想像される。この日は最後の登りで疲れた状態でどのくらい走れるかを試す意味もあって頑張って飛ばして走った。

途中の林道の分岐。ここもちょっと行き過ぎたがガーミンの警告が来る前に気づいた。

頑張って走ろうと思えば走れるが、自分のようなロートルのしかも、遅いランナーにはかなりきつそう。この辺りは上位と下位でかなりタイム差がつきそうなポイントに思えた。

仰木のエイドステーションのあたり。この辺をガンガン走れるとタイムはかなり縮めることができるのだろうが。

仰木のエイドステーションのあたりで、アンドゥーを補給。
林道をガンガン走って仰木の里の方へ下っていく。今回はとにかく下りきるまで頑張って走ってみた。仰木の里に下りたところで折り返し。ここには元三大師ウォーターステーションが設置されていることになっている。折り返すと当然登り、流石に登りは全然走れなかった。

仰木の里で折り返してここで左に分岐。来るときは右の道から降りてくる。

さて、いい加減くたばったところで横川の登りにかかる。はじめは林道を緩やかに登る(これはこれで結構きつい)が、途中で木段が現れてきついのぼりになる。

横川の手前まで木段の道が続く

ここがかなりきつかった。本番ではここまで40km以上走ってきているのでもうヘロヘロになっているに違いないここはかなりこたえると思った。
この日も下りで飛ばしたのがこたえてやっぱりヘロヘロ。おまけに補給のタイミングを間違えて登り坂の途中でハンガーノックになりかけて、エイドのある横川まで頑張ろうと思っていたが、こらえきれなくて途中で止まっておにぎりを頬張る。
木段を上り切ってようやく比叡山ドライブウェイ脇の東海自然歩道と合流したときはほっとした。

木段を上り切って東海自然歩道と合流
ドライブウェイの下をくぐる

ドライブウェイくぐってしばらく登ると3差路に出る。ここでまた、コースミス。横川中堂の文字に惹かれて左に行ってしまった。本当は右に行って駐車場の方に行かないといけない。

ここは右。駐車場の方へ行く。

横川駐車場から横川の境内に少し入ったところが最後のエイドステーション。ただ、その手前に拝観受付がある。中に入らないといけないので今回はお金を払って中に入る。ついでに、横川の中堂と元三大師堂を拝観した。

横川中堂。下から見たところ
元三大師堂
おみくじ発祥の地の碑

元三大師堂はおみくじ発祥の地だそうで、以前テレビでやっていたのを思い出した。ここのおみくじはお坊さんが願い事や悩み事を聞いて、お坊さんが祈祷してから引くという独特のものらしい。
横川には初めて来たが、横川地区は根本中堂のある東塔からかなり離れていて訪れる人も少なくひっそりとしていい雰囲気だった。お参りしている人もかなり信心深い人たちのようで一生懸命お参りしていたのが印象的だった。
横川の参拝が少し休憩になって、ちょっと回復。ここから坂本へ降りる登山道を行く。ここからゴールまでは約5kmだが、そのうち約3.5kmは延々と続く下りをまた走ることになる。タイムリミットが迫っていると疲れていてもいやが応なく走ることになるだろう。ただ最後の坂は地獄になると思うが。

登山道を離れて、東塔方面に登り返す。

最後の登りは印象としては横川への登りよりもきつくなかったが、時間と争っているときにこの最後の登りはかなりきついはず。誰かのブログに嘆きの坂と書いてあったが、時間は迫るが走ろうと思っても走れないという状況なら嘆きの坂と呼ばれるのも納得する。
坂を上り切って延暦寺会館脇を抜けて根本中堂前へ出てゴール。

根本中堂前でゴール

途中で横川を参拝していたのでトータルで5時間ほどかかった。実際に走っていたのは4時間半くらい。平均でkmあたり11分を切っていた。最近12分とか13分台だったので、久々に走りとしてはいい走りができた。その分しんどかったけど。
横川で買った参拝券が東塔でも使えるということなので、せっかくなので根本中堂を拝観した。現在は大修理中で外側はビニールのシートで覆われていた。まあ、修理中の状態を見学するのは今しかないと思うのでそれも一興かと思うが。通常は中は撮影禁止になっているが、今回は修理中の様子は撮ってもいいということなので写真を撮らせてもらった。

修理中の様子、鉄柱の奥がメインの屋根
こちらも修理中の屋根。手前側に当たる部分

帰りは坂本までケーブルカーで降りた。
延暦寺の所にケーブルカーの案内が出ていて、毎時0分と30分発と書いてあった。時計を見ると出発時刻まであと7分。1本逃すと30分待つことになるので、走って駅まで行った。帰りはゆっくりと歩いて帰るつもりだったのにここでも走る羽目になるとは。
しかも、坂本まで降りてからJRの駅まで歩いていくと、ちょうど湖西線の列車が来るところで、ここでも走って飛び乗った。
なんともバタバタの帰り道でもう少しゆったりとしたかったがタイミングなので仕方ないか。

これで、比叡山国際トレイルの試走は終了。3回に分けて走ったので疲労とかそういうのはわからない部分も多いがコースの状況は大体把握することができた。細かいシミュレーションは後日することにして、全体の印象としては、前半はきつい上り下りのトレイルコース、後半はひたすら林道を走るコースという印象。たしかに要項にフルマラソンを4時間切る走力が必要と書いてあるわけがわかったような気がした。特に後半の林道部分で走れないとタイム的にはきついことになるのではないかという気がした。最近あまりスピードを出す練習をしていないが、本番に向けてはちょっとスピード練習を取り入れていった方がいいのかもしれない。
まだ、エントリーは開始していないが、今年の要項を見ると50kmの部の参加要件がトレイルランで40km以上の完走というのになっていた。たしか、以前は35kmだったと思ったがまたハードルが上がっている。まあ、弘法トレイルを完走しているので40kmでも問題はないのだが。35kmなら、六甲縦走トレイルの38kmでOKと思っていたのでいつの間にという感じ。

今回のルートです。約25km。

Garmin ウォッチ用ウォッチフェイスを作る(その5)

前回まででウォッチフェイスの表示についてはほぼ完成しました。
alasixosaka.hatenablog.com
今回は、フォントをオリジナルに変えてみようと思います。
やり方は、いつも参考にしているmayoさんのブログに詳しく書いてあります。
note.com
ここでは簡単に概略だけを書いておきます。ほぼ参考サイトのやり方通りで動きます。
まず、フォントデータを用意するためにBMfontというソフトをダウンロードします。
インストールは不要です。ダウンロードしたファイルのbmfont64.exeをクリックすると起動します(OSが64bitの場合、32bitならbmfont32.exeを選ぶ)。起動したら、ガーミンウォッチに表示したいフォントを選びます。
色々あって悩みますが、今回は、HPSimplifiedというフォントを選びました。その辺は好みだと思うのですが、自分的には明朝系のフォントよりもゴチック系のフォントの方が好みです。また、このフォントはパソコンがHPだからインストールされているのかもしれません(確認はしてませんが)。
とりあえず、フォントが決まったらサイズを適当な大きさにします。文字盤のメインの表示だと96pixelくらいがちょうどよいようです。それも好みの問題ですが。
フォントとサイズが決まったら、save bitmap as で適当な名前を付けて保存します。このとき、参考サイトだと拡張子がfntのファイルとpngのファイルができると書いてありますが、自分の場合デフォルトではfntとtgaの拡張子のファイルができました。どちらでも特に問題はなく、fntのファイルだけあればよいようです。
できたフォントを、プロジェクトのresourceフォルダにfontsというフォルダを作って格納します。そして、そのフォルダにfont.xmlを作って、

<fonts>
    <font id = "HP" filename="HP.fnt"/>
</fonts>

のように記述します。ファイル名は先ほどダウンロードしたfntファイルのもの。font id はプログラム中で使う識別子です。
次に、layout.xmlで作ったフォントを表示したい部分のフォント名をfont = @Fonts.HPというように修正します。
これで、オリジナルフォントが表示されるようになります。

フォントがオリジナルに変更された

実機で試す

さて、結構お気に入りのウォッチフェイスができたので実機で試してみました。
やり方はあちこちに書いてあるので今更ですが、ガーミンウォッチをパソコンに接続し、プロジェクトのbinフォルダのxxxx,prg(xxxxはプロジェクト名)というファイルをウォッチのGarmin>Appsというフォルダにコピーして格納します。ケーブルを取り外すとそのまま新しいウォッチフェイスが動きます。

Low Power Modeにはまる

これでお気に入りのウォッチフェイスができて、めでたしめでたしと思いきや、様子が変。秒針(デジタルなので正確には秒表示というべきか)がすぐに止まってしまう。時計を動かすとまた動き出すが、しばらくするとまた止まる。この動きはどっかで見たような。そう、COROSとおなじではないか。COROSも節電のために秒針は常に動いておらず腕を動かすと動き出して、しばらくすると止まる。たしかに、前にも書いたようにスマートウォッチってそういうもんでしょと言われればそれまでなんですが、特に有機ELを表示に使っているウォッチなんかは表示そのものが消えてしまったりする。ただ、個人的には普段使いの時計として使うなら秒針は常に動いていて欲しいと思っているし、現に、ガーミンではデフォルトでインストールされているウォッチフェイスだと秒針は常に動いている。また、ConnectIQからインストールしたウォッチフェイスでも秒針は常に動いていた。ということは何か方法があるはず。
色々調べると、ウォッチの種類によるようなのですが、アクティブモードとローパワーモードがあってアクティビティー中や腕を動かしたときはアクティブモードに入って毎秒更新になり、アクティビティー中でない通常のスマートウォッチモードの時は10秒後にローパワーモードに入って節電をするらしい。
つまり、プログラム中のonUpdate()はアクティブモードでは1秒毎に動作し、ローパワーモードでは1分毎に動作する。ローパワーモードで1秒ごとに動作するには別にonPartialUpdate()という関数が用意されていて、ローパワーモードの時に1秒毎に呼び出される。
ということで早速やってみた。
基本に戻って、新しいプロジェクトを作成し、秒の表示の部分をonPartialUpdate()関数を作ってその中に書いてみた。
シミュレータではsettingからLow Power Modeを選んでチェックを入れるとローパワーモードに入る。
これで万事解決と思ったらそうは問屋が卸さない。しばらくすると秒針が59秒で止まる。
ちなみに、onUpdate()の所と、onPartialUpdate()の所に、System.println("full update"); System.println("partial update");と書いて動作を確認してみた。この辺はArduinoみたいで結構原始的。一応デバッグモードも用意されているが、ブレークポイントで止まってくれるくらいで、その時の変数の状態などは全然表示してくれないので、原始的な方法がデバッグには必要なようだ。
コメントを見て、動作を見てみると、しばらくはパーシャルアップデートが動いているが、1分経過後の次のフルアップデートのタイミングで必ず止まる。
実機でも試してみたが動作は同じ。これは完全に行き詰ったか、と思ったが、実際に毎秒動くプログラムを書いている人がいるので絶対に何か解決策があるはず。色々探してようやく解決策が見つかった。ただそれも簡単ではなくいろいろ試行錯誤が必要で、その辺の詳しくはまた次回に書いてみたいと思います。

Garmin ウォッチ用ウォッチフェイスを作る(その4)

前回は、日付と曜日、バッテリー容量の表示などを行いました。
alasixosaka.hatenablog.com
だいぶやり方もわかってきたので、残りの表示を一気に片付けてしまいます。

表示したい情報を決める。

表示する情報としては、

  • 日付と曜日
  • 時、分、秒
  • バッテリー残量
  • 心拍数
  • 歩数
  • スマホとのBluetooth接続の有無
  • スマホへのメッセージの有無

の7つにする。そのうち、3つ目までは前回のブログで完成しているので、今回は残りの4つを表示させてみることにする。

情報の取得

表示するための情報の取得については、いつものこちらのサイトを参考にしました。
take4-blue.com
心拍数に関しては

ActivityMonitor.getHeartRateHistory(1, true).next().heartRate

で取得できる。取得できなかった場合は、
ActivityMonitor.INVALID_HR_SAMPLE がTrueになる。
歩数は、

ActivityMonitor.getInfo().steps

Bluetooth接続の有無と、メッセージの数はそれぞれ

System.getDeviceSettings().phoneConnected
System.getDeviceSettings().notificationCount

で取得することができます。

アイコンを決める

前回も紹介したこちらのサイトから適当なアイコンを選んでダウンロードしました。
www.mingcute.com

プログラムの全文

いきなりですが、全文公開です。

layout.xml

まずレイアウトから

<layout id="WatchFace">
    <label id="TimeLabel" x="center" y="60" font="Graphics.FONT_NUMBER_THAI_HOT" justification="Graphics.TEXT_JUSTIFY_CENTER" color="Graphics.COLOR_BLUE" />
    <label id="Label" x="200" y="140" font="Graphics.FONT_LARGE" justification="Graphics.TEXT_JUSTIFY_CENTER" color="Graphics.COLOR_BLUE" />
    <label id ="DayLabel" x="center" y="30" font="Graphics.FONT_LARGE" justification="Graphics.TEXT_JUSTIFY_CENTER" color="Graphics.COLOR_WHITE" />
    <label id ="BatLabel" x="180" y="195" font="Graphics.FONT_TINY" justification="Graphics.TEXT_JUSTIFY_CENTER" color="Graphics.COLOR_WHITE" />
    <label id ="HeartLabel" x="90" y="170" font="Graphics.FONT_TINY" justification="Graphics.TEXT_JUSTIFY_CENTER" color="Graphics.COLOR_WHITE" />
    <label id ="shoeLabel" x="150" y="220" font="Graphics.FONT_TINY" justification="Graphics.TEXT_JUSTIFY_CENTER" color="Graphics.COLOR_GREEN" />
</layout>

TimeLabelというのが時と分を表示する部分で、単なるLabelが秒の表示です。このあたりはまだテストプログラムの名残です。
DayLabelが日付と曜日、BatLabelがバッテリー残量。HeartLabelが心拍数、shoeLabelが歩数です。

drawable.xml

次がdrawable(画像関係)

<drawables>
    <bitmap id="LauncherIcon" filename="launcher_icon.png" />
    <bitmap id="heart" filename="heart_fill.png" />
    <bitmap id="BT" filename="bluetooth_line.png" />
    <bitmap id="mes" filename="message_2_fill.png" />
    <bitmap id="shoe" filename="shoe_line.png" />
</drawables>

LauncherIconはデフォルトのままで弄っていません。heartが心拍数を表すハートのアイコン。BTがbluetoothのアイコン。mesが通知を知らせるアイコン。shoeが歩数表示のための靴のアイコンです。画像ファイル(png形式)はdrawableフォルダに入れておきます。

プログラム本体

プログラム本体のWatchfaceView.mcの部分です。長いですが。

import Toybox.Graphics;
import Toybox.Lang;
import Toybox.System;
import Toybox.WatchUi;
import Toybox.Time;

var BT;
var mes;
var heart;
var shoe;

class watchfaceView extends WatchUi.WatchFace {

    function initialize() {
        WatchFace.initialize();
    }

    // Load your resources here
    function onLayout(dc as Dc) as Void {
        heart = WatchUi.loadResource(Rez.Drawables.heart);
        BT = WatchUi.loadResource(Rez.Drawables.BT);
        mes = WatchUi.loadResource(Rez.Drawables.mes);
        shoe = WatchUi.loadResource(Rez.Drawables.shoe);
        setLayout(Rez.Layouts.WatchFace(dc));
    }

    // Called when this View is brought to the foreground. Restore
    // the state of this View and prepare it to be shown. This includes
    // loading resources into memory.
    function onShow() as Void {
    }

    // Update the view
    function onUpdate(dc as Dc) as Void {
        
        // Get and show the current time
        var clockTime = System.getClockTime();
        var now = Time.now();
        var nowM = Time.Gregorian.info(now, Time.FORMAT_MEDIUM);
        var nowS = Time.Gregorian.info(now, Time.FORMAT_SHORT);
        var batt = System.getSystemStats().battery;
        var hearts = ActivityMonitor.getHeartRateHistory(1, true).next().heartRate;
        var info = System.getDeviceSettings();
        var timeString = Lang.format("$1$:$2$", [clockTime.hour, clockTime.min.format("%02d")]);
        var timeString2 = Lang.format("$1$", [clockTime.sec.format("%02d")]);
        var dayString = Lang.format("$1$/$2$ $3$", [nowS.month, nowS.day, nowM.day_of_week]);
        var battString = Lang.format("$1$%", [batt.format("%02d")]);
        var heartString = Lang.format("$1$", [hearts]);
        var stepstring = Lang.format("$1$", [ActivityMonitor.getInfo().steps.format("%5d")]);
        //System.println (hearts);
        var view = View.findDrawableById("TimeLabel") as Text;
        var view2 = View.findDrawableById("Label") as Text;
        var view3 = View.findDrawableById("DayLabel") as Text;
        var view4 = View.findDrawableById("BatLabel") as Text;
        var view5 = View.findDrawableById("HeartLabel") as Text;
        var view6 = View.findDrawableById("shoeLabel") as Text;
        view.setText(timeString);
        view2.setText(timeString2);
        view3.setText(dayString);
        view4.setText(battString);
        if (hearts != ActivityMonitor.INVALID_HR_SAMPLE){
            view5.setText(heartString);
        }else{
            view5.setText("---");
        }
        view6.setText(stepstring);
        //BAT1.draw(dc);
        // Call the parent onUpdate function to redraw the layout
        View.onUpdate(dc);
        if (info.phoneConnected) {
            dc.drawBitmap(50,195, BT);
        }
        if (info.notificationCount > 0) {
            dc.drawBitmap(80,195, mes);
        }
        if (batt>=50){
            //dc.drawBitmap(140,210, BAT4);
        }else{
            //dc.drawBitmap(140,210, BAT1);
        }
        dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_BLACK);
        dc.drawRectangle(140, 202, 20, 10);
        dc.fillRectangle(140, 202, 20*batt/100, 10);
        dc.drawBitmap(50,170, heart);
        dc.drawBitmap(80, 220, shoe);
    }

    // Called when this View is removed from the screen. Save the
    // state of this View here. This includes freeing resources from
    // memory.
    function onHide() as Void {
    }

    // The user has just looked at their watch. Timers and animations may be started here.
    function onExitSleep() as Void {
    }

    // Terminate any active timers and prepare for slow updates.
    function onEnterSleep() as Void {
    }

}

ここまでその1から読んでいただければそんなに難しくないのでわかると思いますが、ポイントだけ簡単に説明しておきます。
まず、画像を表示するための変数を4つ定義します。

var BT;
var mes;
var heart;
var shoe;

の部分がそうです。
次にfunction onLayoutで

    function onLayout(dc as Dc) as Void {
        heart = WatchUi.loadResource(Rez.Drawables.heart);
        BT = WatchUi.loadResource(Rez.Drawables.BT);
        mes = WatchUi.loadResource(Rez.Drawables.mes);
        shoe = WatchUi.loadResource(Rez.Drawables.shoe);
        setLayout(Rez.Layouts.WatchFace(dc));
    }

の部分で、画像ファイルと紐づけをします。
あとは、画面の更新のfunction onUpdateの部分で表示を制御してやります。前回から追記した部分を中心に説明すると、変数heartsに心拍数が入ります。
Bluetooth接続の有無とメッセージの数は、一旦、変数infoで受けておいて、後で、info.phoneConnectedとinfo.notificationCountからそれぞれの情報を得ています。
心拍数の数値は、文字列のbattStringに代入し、歩数の数値はstepStringに代入します。それぞれ変数view5、view6を使って表示をします。
心拍数は取得できていない時があるので、ActivityMonitor.INVALID_HR_SAMPLEがfalseの時だけ表示し、trueの時は、"---”を表示するようにしています。この辺は参考サイトのやり方を踏襲しています。
また、info.phoneConnectedがtrueならBluetoothのアイコンを表示し、info.notificationCountが1以上であればメッセージアイコンを表示します。

        if (info.phoneConnected) {
            dc.drawBitmap(50,195, BT);
        }
        if (info.notificationCount > 0) {
            dc.drawBitmap(80,195, mes);
        }

の部分がそうです。
こんな感じで表示されます。

一応全部入りで表示してます。

受け取ったメッセージの数を表示することもできますが、今回はしていません。使いながらあった方がいいと思えば追加するかもしれません。
次回は、フォントをオリジナルに変更しようと思っています。

Garmin ウォッチ用ウォッチフェイスを作る(その3)

前回は、文字表示について、時、分を大きな文字で表示し、その下に秒の表示、更に、上部に月、日、曜日を表示するところまでやりました。
alasixosaka.hatenablog.com
今回は、グラフィックスを使ってバッテリーの残量表示をしてみたいと思います。
まずは、バッテリーの残量を取得しないといけません。いつもお世話になっているこのサイトによると、
take4-blue.com
バッテリーの残量は、System.getSystemStats().battery で取得することができそうです。
そこでまず、例によってlayout.xmlを編集して下の一行を追加します。

<label id ="BatLabel" x="180" y="210" font="Graphics.FONT_TINY" justification="Graphics.TEXT_JUSTIFY_CENTER" color="Graphics.COLOR_WHITE" />

ラベルのIdはBatLabelでフォントは小さくするためTINYを使いました。
次にfunction onUpdateに次の一行を追加します。

var batt = System.getSystemStats().battery;

これで、変数battにバッテリーの残量が入ります。
そして、

var battString = Lang.format("$1$%", [batt.format("%02d")]);

var view4 = View.findDrawableById("BatLabel") as Text;

更に

view4.setText(battString);

を追記すると右下に小さくバッテリー残量が表示されます。

バッテリー残量が表示された

グラフィックスを追加する

確かにこれで残量が数字で表示されるようになったんですが、やっぱりアイコンチックなものが欲しい。
そこで、数字の左にアイコンを表示するようにしてみた。
アイコンはフリーの素材のがいろいろネットに落ちているので好きなのを選んでダウンロードして使えばよいが、自分はここのサイトのを使ってみた。
www.mingcute.com
サイズが豊富なのと色も自由に変更できるのが良い。
色々サイズを試してみたが、24pxelがちょうどよいようだ。バッテリーのアイコンを適当にダウンロードして、drawableのフォルダに入れる。
参考にさせていただいているmayoさんのこちらのサイトにも画像を表示する方法が書いてあるが、実はここに書いてある、layout.xmlにビットマップを追記する方法だと常に表示されることになってしまう。つまり、書いたり消したりが自由にできない。
note.com
どうも、文字盤の背景なんかを表示する方法のようだ。
バッテリーの残量表示は残量によってアイコンを切り替えたいので、常に表示は困る。試しに、2つのアイコンを登録してみたが、2つのアイコンが重なって表示され、後に書いた方で上書きされてしまった。また、スマホbluetoothで接続されているかどうかをアイコンで表示したり、メッセージがスマホに来ているかどうかをアイコンで表示したりするのにも必要に応じてアイコンを書いたり消したりする必要がある。
別の方法はないかと探したところ、connect IQのフォーラムに、下記の記事を見つけた。
forums.garmin.com
この方法によると、ビットマップをdrawableに登録して表示するらしい。Androidでもdrawableにビットマップを登録して表示していたので、そうなんじゃないかと思っていたがやっぱりそうだった。具体的には下記の手順で追記をすればよい。
まず、drawableにビットマップを登録する。デフォルトではランチャーアイコンが登録されてるはずなので、その下に次の文を追記する。ファイル名は先ほどdrawableフォルダにダウンロードしたアイコンのファイル名。

<bitmap id="BAT1" filename="battery_1_line.png" />

それから、watchfaceView.mcに戻って、ビットマップ表示のための変数を定義する。ここではBAT1とした。

var BAT1;

次に、function onLayoutに下記の文を追記する。

BAT1 = WatchUi.loadResource(Rez.Drawables.BAT1);

最後に、function onUpdateのView.onUpdate(dc);の後に

dc.drawBitmap(140,210, BAT1);

を追記する。これでアイコンが表示される。表示したくない時は最後に追記した文を実行しなければよい。if文などで判断して表示するしないを決定すればよい。

バッテリーのアイコンが表示された

図形描画で対応させてみる

上の方法で、バッテリー残量によってアイコンの表示を切り替えて表示しようと最初は考えていたが、APIを読んでいると、四角形を描画するコマンドが用意されているのを発見した。アイコンで何段階かに分けてアイコンを切り替えていく方法もよいが、図形描画を使えばもっと細かく視覚的に表示が可能になる。
しかも、描画の方が簡単で手間が少ない。
やり方はfunction onUpdateの最後に下記の3文を追記するだけです。

dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_BLACK);
dc.drawRectangle(140, 217, 20, 10);
dc.fillRectangle(140, 217, 20*batt/100, 10);
残量がグラフィカルに表示された

だいぶコツがつかめてきた。この調子で残りの表示部分も完成させてみたいと思っています。