1年の計は元旦にあり

あけましておめでとうございます。
今年は、子供が大学受験ということで、久しぶりに我が家で過ごす正月となりました。なので、あまり正月気分がないですが。
とはいえ、1年の計は元旦にありとか。ということで、昨年は何を書いたか見返してみると、目標にトレランで50km、ウォーキングで100kmと書いていましたが、結局どちらも実行することはできずじまいでした。ウォーキングは結局イベントに参加せず。個人的にも長距離のウォーキングはやらずじまいでした。トレランの方は弘法トレイルの44kmを完走したので、まあ近い線まで行ったかなというところ。
さて、今年の目標は、やっぱり、トレラン50kmにウォーキング100kmとしておきます。トレランでは、今年は比叡山国際トレイルランの50kmにエントリーする予定です。制限時間が11時間と厳しいので完走できるかどうかはわかりませんが、今年の第一目標として完走を目指したいと思っています。
また、来年にはUTMFのKAIにチャレンジしたいと思っているので、それに向けて秋頃に100kmウォーキングをやりたいと思っています。今のところ、木曽路の100kmが時期的にちょうどよいかなと思っています。

プロフィールを更新しました。

昨年で還暦になって、今年は61になりますが、プロフィールがブログを始めたときのまま、還暦までに100kmを目指すことになっていたので、更新しました。
プロフィールの所にも書きましたが、最近はトレランで100kmはちょっとあきらめモードです。というのは、走っていると胃がなかなか食べ物を受け付けないので長時間のレースとなると最後まで持たないような気がしています。年のせいなのか、元々そういう体質なのかわかりませんが、たいてい長距離のレースをすると、終わってしばらくするとお腹がすいてくるというパターンで、半日程度のレースなら何とかなるのですが、丸1日かかりそうな100km程度のレースとなるとそんな感じではハンガーノックに陥って動けなくなるのではと思っています。まあ、やってみないとわかりませんが、とりあえず100kmウォーキングをやってみてその時の体の反応を確かめようかとは思っています。

今日は、とっても短い文章になってしまいましたが、本年もどうかよろしくお願いします。

やればできる! 5分/km

比叡山国際トレイルを完走するためにスピードをもう少しつけないといけないということで、久々にペース走をやってみました。
ペース走は本当に久しぶりです。たぶん、ロードレースにまだ出ていた30代の頃以来ではないでしょうか?
GarminのForerunner255はペース走の機能があるのでこれを使ってみました。さすがは、ランニング用ウォッチ。
やり方は簡単です。
Garmin Connect(アプリでもPCでもどちらも可)で、トレーニングと計画→ワークアウトと進んで、ランワークアウトを作成、そうするとウォームアップとクールダウンに挟まれたランワークアウトができるので、ワークアウトを編集、タイプの所でペースを選択し、ペースを設定し、目標の距離を入力すれば出来上がりです。今回はペースを6分/km、距離を5kmとかなりぬるめの設定にしました。ペースを6分/kmとしたのは、最終的には15kmくらい同じペースで走り続けられるようにと思ったからで、とりあえず今回は手始めとして5km走ってみようということでした。本当に久しぶりなので。

作成したワークアウト

ワークアウトができたら、デバイスに転送し、アクティビティメニューでランを選択、UPボタン長押しで、トレーニング→ワークアウトと進んで、さっき作ったワークアウトを選択してスタートすれば始まります。
自分の場合は平地で走るときはだいたいいつも近所の河原に行って走るので、そこまでの行きをウォームアップ、帰りをクールダウンとしました。スタートするとウォームアップが始まります。河原に着いたところでラップボタンを押すと、ペース走の始まりです。
GPSで距離とペースを測るので、今のペースが速いか遅いかを判断してガーミンが教えてくれます。ところが、始めガーミンに遅い、遅いと言われてペースを上げて走ったら5分/kmくらいのペースになってしまいました。どうも、ペースを設定するときにきっちり6分/kmにしたくて、幅を持たせずに最低と最高をどちらも6分/kmにしたのが良くなかったようです。少し幅を持って5分50秒~6分10秒という感じにした方が良かったのではないかと思います。
それにしても、最近は5分/kmで走り続けるということは全くしていなかったので最初はきついと思ったのですが、ペースに乗ってしまうとそのままのペースで走ってしまいました。5km持つのかなと思いつつ、まあいいやと思って走っていると5kmまでほぼ全てのラップで5分/km以内で走り切ることができました。

今回のペース走のラップ

1-2kmの所で5分/kmを少し超えていますが、最初の1kmが設定よりも早すぎたので6km/分に落とそうと思って少しペースダウンしたので少し超えています。結果的にはこの時も一回遅いと言われてペースを上げてまた5分/kmよりも早いペースに戻っています。河原を走っているので多少のアップダウンがあるのですが、基本的に行きが下りで帰りが登りなので、帰りの方がちょっとペースが速い感じです。これは、体がこのペースに慣れて少し楽になったからだと思います。今日の感じだと5kmよりももう少し同じペースで走れそうな感じでした。5分/kmは今の自分にはかなり速いペースと思っていましたが、やればできるもんだと、ちょっと自信がつきました。まあ、昔は4分/kmでハーフマラソンくらいは走っていたことを考えるとずいぶん遅くなったという気がしますが、年を考えれば十分できたかと思います。なので、最初6分/kmで距離を伸ばしていこうかと思っていましたが、5分/kmでもう少し頑張ってみようかと思っています。

比叡山国際トレイルランの日程が発表されていた

来年の比叡山国際トレイルランの日程が発表されていました。公式ホームページを何度か確認していて2023年バージョンから全然変わってなかったので、来年本当にやるのか心配になっていました。今年は昨年の12月23日から申し込みが開始されていて、その日を今年は過ぎてしまっていたもので。
ホームページの下の方にあるFacebookのリンクをクリックするとFacebookのページの方に予告が出ていました。
それによると、開催日は5月11日(土)、エントリーの開始は1/26(金)の夜8時となっていました。石舞台100の前日ですね。今年よりも1か月以上申込期間が後ろ倒しになっていました。色々と準備の都合もあるのでしょうが、ちょっと心配しました。

護王神社に行ってきました。

金曜日に京都市内に出張したのですが、たまたま通りすがりにあったので、護王神社に行ってきました。

護王神社

写真を取り忘れたのでパンフレット写真を載せておきます。
場所は、烏丸通り沿い、御所の西で、丸太町と今出川の間です。

神社の案内とアクセス

こちらもパンフレットを写したものです。
パンフレットに書いてあるように、和気清麻呂公を祭った神社で、清麻呂宇佐神宮にご神託を聞きに行くときに、足萎えだったのがイノシシに助けられて治ったという故事にちなんで、足腰の神様となっているとのことです。境内には、狛犬ならぬ狛猪があったり、イノシシの置物がたくさん置いてあったりとイノシシパラダイス状態でした。
足腰にご利益があるということで、足腰の無事を祈願したあと、お守りを買って帰りました。

護王神社で買った足腰に効くというお守り。

今年は腰、膝の痛みに悩まされたので、今度からレースの時はこのお守りをもって走りたいと思います。

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

前回で完成と書いてしまいましたが、どうも問題があったようです。
alasixosaka.hatenablog.com
前回のウォッチフェイスを実機にインストールしてしばらく動かしていたら、いつの間にやらパーシャルアップデートが止まっていました。
何かのタイミングで規定時間の30msを越えてしまったらしく、秒針が止まっていました。手に持つとアクティブになってフルアップデートに入るのでしばらく動くのですがローパワーモードに入ると秒針が止まるという状態になりました。
そこで、以前ConnectIQストアからダウンロードしたウォッチフェイスの動作を確認したところ、スマホとの接続に関してはフルアップデートの時のみ情報を見に行っているようでした。
つまり、System.getDeviceSetting()を実行するとタイミングによっては30msを越えてしまうのではないかということがわかりました。まだ、推定の段階で実証できていませんが。実機でもスマホ側のBluetoothを入れたり、切ったりしてスマホとの接続を入、切して、問題なく表示されて秒針も止まらないことを確認していたのですが、System.getDeviceSetting()では色々な情報をゲットするので情報の種類や量がその時によって違ってしまい、所要時間が異なるのではないかと推定しています。
ですので、一旦、スマホとの接続確認はフルアップデートの場合のみにして、onUpdate()の所でチェックして表示するように変更し、しばらく様子を見ることにしました。

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()関数。