Garminウォッチフェイスの作成-部分更新への対応

質問があったのでGarminのPartialUpdateの実装についてまとめてみた。

似たようなことをこちらにも記述はしてある。

画面の更新頻度についての仕様

ガーミンのウォッチフェイスを作るにあたった画面の更新頻度について注意する必要がある。

基本としては次のような感じになる。

  • 低電力モードでは画面の更新周期は1分単位。
    更新タイミングは毎分0秒時。
  • 高電力モードでは画面の更新周期は1秒単位。
  • 低電力モードから高電力モードへの移行は、時計を見る動作などをした時に発生し、その後一定期間(通常は10秒のようだ)経過後、低電力モードに戻る。
  • 高電力モードを維持し続けるのは無理。

これに端末のAPIレベル2.3.0の場合、部分更新(PartialUpdate)というのが使えるようになる。
この部分更新は低電力モードの時に動作し、以下の様な仕様になっている。

  • 毎分0秒以外(1~59秒)時にonPartialUpdateを呼び出す。
  • onPartialUpdateでの処理時間が一定値を超えた場合、部分更新機能が停止する。

補足1:APIレベルについて

どの端末がどのAPIレベルになっているかは以下ページが参考になる。

Compatible Devices | Connect IQ | Garmin Developers
Garmin Connect IQ Programmer's Guide

ちなみに私が持っている(いた)Forerunner45は1.4.0、vívoactive HRは2.4.0、部分更新の評価用でシミュレーションで実行していたのはvívoactive 4でこちらは3.3.0だった。

補足2:低電力モードに移る10秒

仕様として決まっているのかどうかは不明だけど、以下クラスヘルプ内に記載がある。

Toybox.WatchUi.WatchFace

After this period in high power mode (typically about ten seconds), the system will call onEnterSleep() to notify the application that it is preparing to enter low power mode.

補足3:部分更新機能が停止する処理時間はどれくらい

これに関して仕様として記載されている箇所を探し出すことはできなかった。

この情報は、WatchFaceDelegateのonPowerBudgetExceededが呼び出されたときに呼び出しパラメータのWatchFacePowerInfoのexecutionTimeLimitが該当するのだと思う。

これに関してシミュレーションでちょっと確認したところ30ミリ秒ということが分かった。

結構厳しいな。

補足4:API 2.3.0未満でも秒更新しているウォッチフェイスなくない?

多分それはシステムで用意されているウォッチフェイスだと思う。

それらウォッチフェイスは、Monkey Cを使い作ったものではなく、処理時間も短くできているので秒更新可能に作っている、という見解がメーカーから出ている。

実装方法

自分が作ったウォッチフェイスでは、部分更新につても対応をしている。
どのような感じでクラスを設計し実装しているかにつて紹介したい。

「秒」単位に更新可能かどうかについては以下の様な場合分けができ、それを実現するようなクラス構成としている。

  • 高電力モード
    秒周期で呼び出されるので「秒」に関する表示の処理を実行する。
  • 低電力モードかつonPartialUpdateの呼び出しが可能となっている場合
    「秒」に関する表示の処理を行う。この時の呼び出しはほぼ「00秒」。
  • 低電力モードかつonPartialUpdateの呼び出しが不可能となっている場合
    「秒」に関する表示の処理を行わない処理にする。

クラス構成

PlantUML Syntax:</p>
<p>class WatchFace<br />
class WatchFaceDelegate<br />
class AppBase<br />
class app<br />
class base {<br />
function doViewSec();<br />
function onEnterSleep();<br />
function onExitSleep();<br />
function onUpdate(dc);<br />
}<br />
class partial {<br />
function doViewSec();<br />
function onPartialUpdate(dc);<br />
}<br />
class delegate {<br />
var partialUpdatesAllowed;<br />
function onPowerBudgetExceeded(powerInfo);<br />
}</p>
<p>AppBase <|– app<br />
WatchFace <|– base<br />
base <|– partial<br />
partial “1” o– delegate : delegate<br />
WatchFaceDelegate <|– delegate<br />
app “1” *– base : view<br />
app “0..1” *– delegate : delegate</p>
<p>

baseのdoViewSecは先に示した3つの項目の「秒」を表示させるかどうかについての問い合わせ関数で、この情報を決定させるため、baseのonEnterSleep, onExitSleep、delegateのpartialUpdatesAllowedを使っている。
baseのonUpdateはdoViewSecで判断して「秒」の表示を行うか否かを判断し表示形式を決定するようにする。

またbase/partialの生成切り替えとdelegateの生成はapp内で判別実施している。

WatchFaceDelegateについて

部分更新が可能な場合に、部分更新で高負荷となり部分更新ができないということを検知可能なクラスになる。

コード

APIレベルは何?という判断はできないのでapp内ではonPartialUpdateというクラスメソッドを保持しているかどうかで処理を切り替えている。
またonPartialUpdateでWatchFaceDelegateを利用しているクラスの実装としてサンプルなど多くのコードではそれをグローバル変数として置いておくケースが多いのだけど、個人的には嫌いだったので初期化情報のパラメータとして渡すようにしている。

class base extends WatchUi.WatchFace {
    private var doViewSec_ = false;		// 高電力モード時に秒を描画するためのフラグ
    // 低電力モードに入った場合に呼び出される関数
    function onEnterSleep() {
    	doViewSec_ = false;
    }

    // 高電力モードに入った場合に呼び出される関数
    function onExitSleep() {
    	doViewSec_ = true;
    }

    // 秒の情報を表示するかどうかbool型を返す
    function doViewSec() {
    	return doViewSec_;
    }
}
class partial extends base {
    private var delegate;

    function initialize(obj) {
        delegate = obj;
    }

    // 秒描画をチェックするメソッドをオーバーライドしている
    function doViewSec() {
    	return base.doViewSec() || delegate.partialUpdatesAllowed;
    }
}
// 電力消費量のウォッチ用
class delegate extends WatchUi.WatchFaceDelegate {
    var partialUpdatesAllowed;

    function initialize() {
	WatchUi.WatchFaceDelegate.initialize();
	partialUpdatesAllowed = true;
    }

    function onPowerBudgetExceeded(powerInfo) {
        partialUpdatesAllowed = false;
    }
}
class app extends Application.AppBase {
	var view_;
	var delegate_;
	
    function initialize() {
        AppBase.initialize();
    }

    // Return the initial view of your application here
    function getInitialView() {
    	view_ = null;
    	delegate_ = null;
	if (WatchUi.WatchFace has :onPartialUpdate) {
	    delegate_ = new delegate();
	    view_ = new partiall(delegate_);
	    return [ view_, delegate_ ];
	}
	else {
	    view_ = new base();
	    return [ view_ ];
	}
    }
}

部分更新部の処理

この部分の処理に関しては、結局それぞれのウォッチフェイスによるとしか言いようがない。

私が作成したウォッチフェイスでは以下の赤丸部分の「秒」部分のみを部分更新対象としている。

部分更新の流れとしては次のようになる。

  1. 更新か所を特定する。
    この部分をdc.setClipでクリッピングする。
  2. グラフィックを更新する。

注意点としてはクリッピングする領域が矩形しか対応していないということ。

デジタル時計であれば簡単に対応は可能だけど、アナログ時計の秒針を使う場合は結構面倒かもしれない。

個人的にはアナログ時計が好きなのだけど、上の理由だけでデジタルのウォッチフェイスにした。

補足:部分更新の処理時間を見るには

シミュレーター起動後、低電力モードに変更し、Watchface Diagnosticsで確認できる。

ここで表示されるTotal Timeが参考値。値は多分μ秒になるのでこの値が30000以下になる様に処理の最適化をする必要がある。

参考

部分更新でonUpdateを更新箇所だけクリッピングして呼び出す、ということも可能。

私のウォッチフェイスで実行した場合、どんなパフォーマンスとなったか示しておく。

処理方法 トータル時間
onUpdate 110077
クリッピング後onUpdate 63010
部分更新としてチューニングしたもの 12110

クリッピング後onUpdateを呼び出すものに関して、Watchface Diagnosticsで詳細を確認したところ、実行時間とグラフィック時間に変化はなく、表示時間が減っていた。

ただ上の表を見てわかる通り、「クリッピング後onUpdate」でも63ミリ秒かかっているので30ミリ秒にはまだ足りない結果になっている。

コメント

タイトルとURLをコピーしました