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